server: Move server files to 'server' directory.

26  git mv mvcc wal auth etcdserver etcdmain proxy embed/ lease/ server
   36  git mv go.mod go.sum server
This commit is contained in:
Piotr Tabor
2020-10-20 23:05:17 +02:00
parent eee8dec0c3
commit 4a5e9d1261
316 changed files with 0 additions and 0 deletions

16
server/auth/doc.go Normal file
View File

@@ -0,0 +1,16 @@
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package auth provides client role authentication for accessing keys in etcd.
package auth

163
server/auth/jwt.go Normal file
View File

@@ -0,0 +1,163 @@
// Copyright 2017 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"crypto/ecdsa"
"crypto/rsa"
"errors"
"time"
jwt "github.com/dgrijalva/jwt-go"
"go.uber.org/zap"
)
type tokenJWT struct {
lg *zap.Logger
signMethod jwt.SigningMethod
key interface{}
ttl time.Duration
verifyOnly bool
}
func (t *tokenJWT) enable() {}
func (t *tokenJWT) disable() {}
func (t *tokenJWT) invalidateUser(string) {}
func (t *tokenJWT) genTokenPrefix() (string, error) { return "", nil }
func (t *tokenJWT) info(ctx context.Context, token string, rev uint64) (*AuthInfo, bool) {
// rev isn't used in JWT, it is only used in simple token
var (
username string
revision uint64
)
parsed, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != t.signMethod.Alg() {
return nil, errors.New("invalid signing method")
}
switch k := t.key.(type) {
case *rsa.PrivateKey:
return &k.PublicKey, nil
case *ecdsa.PrivateKey:
return &k.PublicKey, nil
default:
return t.key, nil
}
})
if err != nil {
t.lg.Warn(
"failed to parse a JWT token",
zap.String("token", token),
zap.Error(err),
)
return nil, false
}
claims, ok := parsed.Claims.(jwt.MapClaims)
if !parsed.Valid || !ok {
t.lg.Warn("invalid JWT token", zap.String("token", token))
return nil, false
}
username = claims["username"].(string)
revision = uint64(claims["revision"].(float64))
return &AuthInfo{Username: username, Revision: revision}, true
}
func (t *tokenJWT) assign(ctx context.Context, username string, revision uint64) (string, error) {
if t.verifyOnly {
return "", ErrVerifyOnly
}
// Future work: let a jwt token include permission information would be useful for
// permission checking in proxy side.
tk := jwt.NewWithClaims(t.signMethod,
jwt.MapClaims{
"username": username,
"revision": revision,
"exp": time.Now().Add(t.ttl).Unix(),
})
token, err := tk.SignedString(t.key)
if err != nil {
t.lg.Debug(
"failed to sign a JWT token",
zap.String("user-name", username),
zap.Uint64("revision", revision),
zap.Error(err),
)
return "", err
}
t.lg.Debug(
"created/assigned a new JWT token",
zap.String("user-name", username),
zap.Uint64("revision", revision),
zap.String("token", token),
)
return token, err
}
func newTokenProviderJWT(lg *zap.Logger, optMap map[string]string) (*tokenJWT, error) {
if lg == nil {
lg = zap.NewNop()
}
var err error
var opts jwtOptions
err = opts.ParseWithDefaults(optMap)
if err != nil {
lg.Error("problem loading JWT options", zap.Error(err))
return nil, ErrInvalidAuthOpts
}
var keys = make([]string, 0, len(optMap))
for k := range optMap {
if !knownOptions[k] {
keys = append(keys, k)
}
}
if len(keys) > 0 {
lg.Warn("unknown JWT options", zap.Strings("keys", keys))
}
key, err := opts.Key()
if err != nil {
return nil, err
}
t := &tokenJWT{
lg: lg,
ttl: opts.TTL,
signMethod: opts.SignMethod,
key: key,
}
switch t.signMethod.(type) {
case *jwt.SigningMethodECDSA:
if _, ok := t.key.(*ecdsa.PublicKey); ok {
t.verifyOnly = true
}
case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
if _, ok := t.key.(*rsa.PublicKey); ok {
t.verifyOnly = true
}
}
return t, nil
}

204
server/auth/jwt_test.go Normal file
View File

@@ -0,0 +1,204 @@
// Copyright 2017 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"fmt"
"testing"
"go.uber.org/zap"
)
const (
jwtRSAPubKey = "../tests/fixtures/server.crt"
jwtRSAPrivKey = "../tests/fixtures/server.key.insecure"
jwtECPubKey = "../tests/fixtures/server-ecdsa.crt"
jwtECPrivKey = "../tests/fixtures/server-ecdsa.key.insecure"
)
func TestJWTInfo(t *testing.T) {
optsMap := map[string]map[string]string{
"RSA-priv": {
"priv-key": jwtRSAPrivKey,
"sign-method": "RS256",
"ttl": "1h",
},
"RSA": {
"pub-key": jwtRSAPubKey,
"priv-key": jwtRSAPrivKey,
"sign-method": "RS256",
},
"RSAPSS-priv": {
"priv-key": jwtRSAPrivKey,
"sign-method": "PS256",
},
"RSAPSS": {
"pub-key": jwtRSAPubKey,
"priv-key": jwtRSAPrivKey,
"sign-method": "PS256",
},
"ECDSA-priv": {
"priv-key": jwtECPrivKey,
"sign-method": "ES256",
},
"ECDSA": {
"pub-key": jwtECPubKey,
"priv-key": jwtECPrivKey,
"sign-method": "ES256",
},
"HMAC": {
"priv-key": jwtECPrivKey, // any file, raw bytes used as shared secret
"sign-method": "HS256",
},
}
for k, opts := range optsMap {
t.Run(k, func(tt *testing.T) {
testJWTInfo(tt, opts)
})
}
}
func testJWTInfo(t *testing.T, opts map[string]string) {
lg := zap.NewNop()
jwt, err := newTokenProviderJWT(lg, opts)
if err != nil {
t.Fatal(err)
}
ctx := context.TODO()
token, aerr := jwt.assign(ctx, "abc", 123)
if aerr != nil {
t.Fatalf("%#v", aerr)
}
ai, ok := jwt.info(ctx, token, 123)
if !ok {
t.Fatalf("failed to authenticate with token %s", token)
}
if ai.Revision != 123 {
t.Fatalf("expected revision 123, got %d", ai.Revision)
}
ai, ok = jwt.info(ctx, "aaa", 120)
if ok || ai != nil {
t.Fatalf("expected aaa to fail to authenticate, got %+v", ai)
}
// test verify-only provider
if opts["pub-key"] != "" && opts["priv-key"] != "" {
t.Run("verify-only", func(t *testing.T) {
newOpts := make(map[string]string, len(opts))
for k, v := range opts {
newOpts[k] = v
}
delete(newOpts, "priv-key")
verify, err := newTokenProviderJWT(lg, newOpts)
if err != nil {
t.Fatal(err)
}
ai, ok := verify.info(ctx, token, 123)
if !ok {
t.Fatalf("failed to authenticate with token %s", token)
}
if ai.Revision != 123 {
t.Fatalf("expected revision 123, got %d", ai.Revision)
}
ai, ok = verify.info(ctx, "aaa", 120)
if ok || ai != nil {
t.Fatalf("expected aaa to fail to authenticate, got %+v", ai)
}
_, aerr := verify.assign(ctx, "abc", 123)
if aerr != ErrVerifyOnly {
t.Fatalf("unexpected error when attempting to sign with public key: %v", aerr)
}
})
}
}
func TestJWTBad(t *testing.T) {
var badCases = map[string]map[string]string{
"no options": {},
"invalid method": {
"sign-method": "invalid",
},
"rsa no key": {
"sign-method": "RS256",
},
"invalid ttl": {
"sign-method": "RS256",
"ttl": "forever",
},
"rsa invalid public key": {
"sign-method": "RS256",
"pub-key": jwtRSAPrivKey,
"priv-key": jwtRSAPrivKey,
},
"rsa invalid private key": {
"sign-method": "RS256",
"pub-key": jwtRSAPubKey,
"priv-key": jwtRSAPubKey,
},
"hmac no key": {
"sign-method": "HS256",
},
"hmac pub key": {
"sign-method": "HS256",
"pub-key": jwtRSAPubKey,
},
"missing public key file": {
"sign-method": "HS256",
"pub-key": "missing-file",
},
"missing private key file": {
"sign-method": "HS256",
"priv-key": "missing-file",
},
"ecdsa no key": {
"sign-method": "ES256",
},
"ecdsa invalid public key": {
"sign-method": "ES256",
"pub-key": jwtECPrivKey,
"priv-key": jwtECPrivKey,
},
"ecdsa invalid private key": {
"sign-method": "ES256",
"pub-key": jwtECPubKey,
"priv-key": jwtECPubKey,
},
}
lg := zap.NewNop()
for k, v := range badCases {
t.Run(k, func(t *testing.T) {
_, err := newTokenProviderJWT(lg, v)
if err == nil {
t.Errorf("expected error for options %v", v)
}
})
}
}
// testJWTOpts is useful for passing to NewTokenProvider which requires a string.
func testJWTOpts() string {
return fmt.Sprintf("%s,pub-key=%s,priv-key=%s,sign-method=RS256", tokenTypeJWT, jwtRSAPubKey, jwtRSAPrivKey)
}

15
server/auth/main_test.go Normal file
View File

@@ -0,0 +1,15 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package auth
import (
"testing"
"go.etcd.io/etcd/pkg/v3/testutil"
)
func TestMain(m *testing.M) {
testutil.MustTestMainWithLeakDetection(m)
}

42
server/auth/metrics.go Normal file
View File

@@ -0,0 +1,42 @@
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"github.com/prometheus/client_golang/prometheus"
"sync"
)
var (
currentAuthRevision = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Namespace: "etcd_debugging",
Subsystem: "auth",
Name: "revision",
Help: "The current revision of auth store.",
},
func() float64 {
reportCurrentAuthRevMu.RLock()
defer reportCurrentAuthRevMu.RUnlock()
return reportCurrentAuthRev()
},
)
// overridden by auth store initialization
reportCurrentAuthRevMu sync.RWMutex
reportCurrentAuthRev = func() float64 { return 0 }
)
func init() {
prometheus.MustRegister(currentAuthRevision)
}

35
server/auth/nop.go Normal file
View File

@@ -0,0 +1,35 @@
// Copyright 2018 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
)
type tokenNop struct{}
func (t *tokenNop) enable() {}
func (t *tokenNop) disable() {}
func (t *tokenNop) invalidateUser(string) {}
func (t *tokenNop) genTokenPrefix() (string, error) { return "", nil }
func (t *tokenNop) info(ctx context.Context, token string, rev uint64) (*AuthInfo, bool) {
return nil, false
}
func (t *tokenNop) assign(ctx context.Context, username string, revision uint64) (string, error) {
return "", ErrAuthFailed
}
func newTokenProviderNop() (*tokenNop, error) {
return &tokenNop{}, nil
}

192
server/auth/options.go Normal file
View File

@@ -0,0 +1,192 @@
// Copyright 2018 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"crypto/ecdsa"
"crypto/rsa"
"fmt"
"io/ioutil"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
const (
optSignMethod = "sign-method"
optPublicKey = "pub-key"
optPrivateKey = "priv-key"
optTTL = "ttl"
)
var knownOptions = map[string]bool{
optSignMethod: true,
optPublicKey: true,
optPrivateKey: true,
optTTL: true,
}
var (
// DefaultTTL will be used when a 'ttl' is not specified
DefaultTTL = 5 * time.Minute
)
type jwtOptions struct {
SignMethod jwt.SigningMethod
PublicKey []byte
PrivateKey []byte
TTL time.Duration
}
// ParseWithDefaults will load options from the specified map or set defaults where appropriate
func (opts *jwtOptions) ParseWithDefaults(optMap map[string]string) error {
if opts.TTL == 0 && optMap[optTTL] == "" {
opts.TTL = DefaultTTL
}
return opts.Parse(optMap)
}
// Parse will load options from the specified map
func (opts *jwtOptions) Parse(optMap map[string]string) error {
var err error
if ttl := optMap[optTTL]; ttl != "" {
opts.TTL, err = time.ParseDuration(ttl)
if err != nil {
return err
}
}
if file := optMap[optPublicKey]; file != "" {
opts.PublicKey, err = ioutil.ReadFile(file)
if err != nil {
return err
}
}
if file := optMap[optPrivateKey]; file != "" {
opts.PrivateKey, err = ioutil.ReadFile(file)
if err != nil {
return err
}
}
// signing method is a required field
method := optMap[optSignMethod]
opts.SignMethod = jwt.GetSigningMethod(method)
if opts.SignMethod == nil {
return ErrInvalidAuthMethod
}
return nil
}
// Key will parse and return the appropriately typed key for the selected signature method
func (opts *jwtOptions) Key() (interface{}, error) {
switch opts.SignMethod.(type) {
case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
return opts.rsaKey()
case *jwt.SigningMethodECDSA:
return opts.ecKey()
case *jwt.SigningMethodHMAC:
return opts.hmacKey()
default:
return nil, fmt.Errorf("unsupported signing method: %T", opts.SignMethod)
}
}
func (opts *jwtOptions) hmacKey() (interface{}, error) {
if len(opts.PrivateKey) == 0 {
return nil, ErrMissingKey
}
return opts.PrivateKey, nil
}
func (opts *jwtOptions) rsaKey() (interface{}, error) {
var (
priv *rsa.PrivateKey
pub *rsa.PublicKey
err error
)
if len(opts.PrivateKey) > 0 {
priv, err = jwt.ParseRSAPrivateKeyFromPEM(opts.PrivateKey)
if err != nil {
return nil, err
}
}
if len(opts.PublicKey) > 0 {
pub, err = jwt.ParseRSAPublicKeyFromPEM(opts.PublicKey)
if err != nil {
return nil, err
}
}
if priv == nil {
if pub == nil {
// Neither key given
return nil, ErrMissingKey
}
// Public key only, can verify tokens
return pub, nil
}
// both keys provided, make sure they match
if pub != nil && pub.E != priv.E && pub.N.Cmp(priv.N) != 0 {
return nil, ErrKeyMismatch
}
return priv, nil
}
func (opts *jwtOptions) ecKey() (interface{}, error) {
var (
priv *ecdsa.PrivateKey
pub *ecdsa.PublicKey
err error
)
if len(opts.PrivateKey) > 0 {
priv, err = jwt.ParseECPrivateKeyFromPEM(opts.PrivateKey)
if err != nil {
return nil, err
}
}
if len(opts.PublicKey) > 0 {
pub, err = jwt.ParseECPublicKeyFromPEM(opts.PublicKey)
if err != nil {
return nil, err
}
}
if priv == nil {
if pub == nil {
// Neither key given
return nil, ErrMissingKey
}
// Public key only, can verify tokens
return pub, nil
}
// both keys provided, make sure they match
if pub != nil && pub.Curve != priv.Curve &&
pub.X.Cmp(priv.X) != 0 && pub.Y.Cmp(priv.Y) != 0 {
return nil, ErrKeyMismatch
}
return priv, nil
}

View File

@@ -0,0 +1,141 @@
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"go.etcd.io/etcd/api/v3/authpb"
"go.etcd.io/etcd/pkg/v3/adt"
"go.etcd.io/etcd/v3/mvcc/backend"
"go.uber.org/zap"
)
func getMergedPerms(lg *zap.Logger, tx backend.BatchTx, userName string) *unifiedRangePermissions {
user := getUser(lg, tx, userName)
if user == nil {
return nil
}
readPerms := adt.NewIntervalTree()
writePerms := adt.NewIntervalTree()
for _, roleName := range user.Roles {
role := getRole(lg, tx, roleName)
if role == nil {
continue
}
for _, perm := range role.KeyPermission {
var ivl adt.Interval
var rangeEnd []byte
if len(perm.RangeEnd) != 1 || perm.RangeEnd[0] != 0 {
rangeEnd = perm.RangeEnd
}
if len(perm.RangeEnd) != 0 {
ivl = adt.NewBytesAffineInterval(perm.Key, rangeEnd)
} else {
ivl = adt.NewBytesAffinePoint(perm.Key)
}
switch perm.PermType {
case authpb.READWRITE:
readPerms.Insert(ivl, struct{}{})
writePerms.Insert(ivl, struct{}{})
case authpb.READ:
readPerms.Insert(ivl, struct{}{})
case authpb.WRITE:
writePerms.Insert(ivl, struct{}{})
}
}
}
return &unifiedRangePermissions{
readPerms: readPerms,
writePerms: writePerms,
}
}
func checkKeyInterval(
lg *zap.Logger,
cachedPerms *unifiedRangePermissions,
key, rangeEnd []byte,
permtyp authpb.Permission_Type) bool {
if len(rangeEnd) == 1 && rangeEnd[0] == 0 {
rangeEnd = nil
}
ivl := adt.NewBytesAffineInterval(key, rangeEnd)
switch permtyp {
case authpb.READ:
return cachedPerms.readPerms.Contains(ivl)
case authpb.WRITE:
return cachedPerms.writePerms.Contains(ivl)
default:
lg.Panic("unknown auth type", zap.String("auth-type", permtyp.String()))
}
return false
}
func checkKeyPoint(lg *zap.Logger, cachedPerms *unifiedRangePermissions, key []byte, permtyp authpb.Permission_Type) bool {
pt := adt.NewBytesAffinePoint(key)
switch permtyp {
case authpb.READ:
return cachedPerms.readPerms.Intersects(pt)
case authpb.WRITE:
return cachedPerms.writePerms.Intersects(pt)
default:
lg.Panic("unknown auth type", zap.String("auth-type", permtyp.String()))
}
return false
}
func (as *authStore) isRangeOpPermitted(tx backend.BatchTx, userName string, key, rangeEnd []byte, permtyp authpb.Permission_Type) bool {
// assumption: tx is Lock()ed
_, ok := as.rangePermCache[userName]
if !ok {
perms := getMergedPerms(as.lg, tx, userName)
if perms == nil {
as.lg.Error(
"failed to create a merged permission",
zap.String("user-name", userName),
)
return false
}
as.rangePermCache[userName] = perms
}
if len(rangeEnd) == 0 {
return checkKeyPoint(as.lg, as.rangePermCache[userName], key, permtyp)
}
return checkKeyInterval(as.lg, as.rangePermCache[userName], key, rangeEnd, permtyp)
}
func (as *authStore) clearCachedPerm() {
as.rangePermCache = make(map[string]*unifiedRangePermissions)
}
func (as *authStore) invalidateCachedPerm(userName string) {
delete(as.rangePermCache, userName)
}
type unifiedRangePermissions struct {
readPerms adt.IntervalTree
writePerms adt.IntervalTree
}

View File

@@ -0,0 +1,102 @@
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"testing"
"go.etcd.io/etcd/api/v3/authpb"
"go.etcd.io/etcd/pkg/v3/adt"
"go.uber.org/zap"
)
func TestRangePermission(t *testing.T) {
tests := []struct {
perms []adt.Interval
begin []byte
end []byte
want bool
}{
{
[]adt.Interval{adt.NewBytesAffineInterval([]byte("a"), []byte("c")), adt.NewBytesAffineInterval([]byte("x"), []byte("z"))},
[]byte("a"), []byte("z"),
false,
},
{
[]adt.Interval{adt.NewBytesAffineInterval([]byte("a"), []byte("f")), adt.NewBytesAffineInterval([]byte("c"), []byte("d")), adt.NewBytesAffineInterval([]byte("f"), []byte("z"))},
[]byte("a"), []byte("z"),
true,
},
{
[]adt.Interval{adt.NewBytesAffineInterval([]byte("a"), []byte("d")), adt.NewBytesAffineInterval([]byte("a"), []byte("b")), adt.NewBytesAffineInterval([]byte("c"), []byte("f"))},
[]byte("a"), []byte("f"),
true,
},
}
for i, tt := range tests {
readPerms := adt.NewIntervalTree()
for _, p := range tt.perms {
readPerms.Insert(p, struct{}{})
}
result := checkKeyInterval(zap.NewExample(), &unifiedRangePermissions{readPerms: readPerms}, tt.begin, tt.end, authpb.READ)
if result != tt.want {
t.Errorf("#%d: result=%t, want=%t", i, result, tt.want)
}
}
}
func TestKeyPermission(t *testing.T) {
tests := []struct {
perms []adt.Interval
key []byte
want bool
}{
{
[]adt.Interval{adt.NewBytesAffineInterval([]byte("a"), []byte("c")), adt.NewBytesAffineInterval([]byte("x"), []byte("z"))},
[]byte("f"),
false,
},
{
[]adt.Interval{adt.NewBytesAffineInterval([]byte("a"), []byte("f")), adt.NewBytesAffineInterval([]byte("c"), []byte("d")), adt.NewBytesAffineInterval([]byte("f"), []byte("z"))},
[]byte("b"),
true,
},
{
[]adt.Interval{adt.NewBytesAffineInterval([]byte("a"), []byte("d")), adt.NewBytesAffineInterval([]byte("a"), []byte("b")), adt.NewBytesAffineInterval([]byte("c"), []byte("f"))},
[]byte("d"),
true,
},
{
[]adt.Interval{adt.NewBytesAffineInterval([]byte("a"), []byte("d")), adt.NewBytesAffineInterval([]byte("a"), []byte("b")), adt.NewBytesAffineInterval([]byte("c"), []byte("f"))},
[]byte("f"),
false,
},
}
for i, tt := range tests {
readPerms := adt.NewIntervalTree()
for _, p := range tt.perms {
readPerms.Insert(p, struct{}{})
}
result := checkKeyPoint(zap.NewExample(), &unifiedRangePermissions{readPerms: readPerms}, tt.key, authpb.READ)
if result != tt.want {
t.Errorf("#%d: result=%t, want=%t", i, result, tt.want)
}
}
}

247
server/auth/simple_token.go Normal file
View File

@@ -0,0 +1,247 @@
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
// CAUTION: This random number based token mechanism is only for testing purpose.
// JWT based mechanism will be added in the near future.
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"strconv"
"strings"
"sync"
"time"
"go.uber.org/zap"
)
const (
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
defaultSimpleTokenLength = 16
)
// var for testing purposes
// TODO: Remove this mutable global state - as it's race-prone.
var (
simpleTokenTTLDefault = 300 * time.Second
simpleTokenTTLResolution = 1 * time.Second
)
type simpleTokenTTLKeeper struct {
tokens map[string]time.Time
donec chan struct{}
stopc chan struct{}
deleteTokenFunc func(string)
mu *sync.Mutex
simpleTokenTTL time.Duration
}
func (tm *simpleTokenTTLKeeper) stop() {
select {
case tm.stopc <- struct{}{}:
case <-tm.donec:
}
<-tm.donec
}
func (tm *simpleTokenTTLKeeper) addSimpleToken(token string) {
tm.tokens[token] = time.Now().Add(tm.simpleTokenTTL)
}
func (tm *simpleTokenTTLKeeper) resetSimpleToken(token string) {
if _, ok := tm.tokens[token]; ok {
tm.tokens[token] = time.Now().Add(tm.simpleTokenTTL)
}
}
func (tm *simpleTokenTTLKeeper) deleteSimpleToken(token string) {
delete(tm.tokens, token)
}
func (tm *simpleTokenTTLKeeper) run() {
tokenTicker := time.NewTicker(simpleTokenTTLResolution)
defer func() {
tokenTicker.Stop()
close(tm.donec)
}()
for {
select {
case <-tokenTicker.C:
nowtime := time.Now()
tm.mu.Lock()
for t, tokenendtime := range tm.tokens {
if nowtime.After(tokenendtime) {
tm.deleteTokenFunc(t)
delete(tm.tokens, t)
}
}
tm.mu.Unlock()
case <-tm.stopc:
return
}
}
}
type tokenSimple struct {
lg *zap.Logger
indexWaiter func(uint64) <-chan struct{}
simpleTokenKeeper *simpleTokenTTLKeeper
simpleTokensMu sync.Mutex
simpleTokens map[string]string // token -> username
simpleTokenTTL time.Duration
}
func (t *tokenSimple) genTokenPrefix() (string, error) {
ret := make([]byte, defaultSimpleTokenLength)
for i := 0; i < defaultSimpleTokenLength; i++ {
bInt, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
ret[i] = letters[bInt.Int64()]
}
return string(ret), nil
}
func (t *tokenSimple) assignSimpleTokenToUser(username, token string) {
t.simpleTokensMu.Lock()
defer t.simpleTokensMu.Unlock()
if t.simpleTokenKeeper == nil {
return
}
_, ok := t.simpleTokens[token]
if ok {
t.lg.Panic(
"failed to assign already-used simple token to a user",
zap.String("user-name", username),
zap.String("token", token),
)
}
t.simpleTokens[token] = username
t.simpleTokenKeeper.addSimpleToken(token)
}
func (t *tokenSimple) invalidateUser(username string) {
if t.simpleTokenKeeper == nil {
return
}
t.simpleTokensMu.Lock()
for token, name := range t.simpleTokens {
if name == username {
delete(t.simpleTokens, token)
t.simpleTokenKeeper.deleteSimpleToken(token)
}
}
t.simpleTokensMu.Unlock()
}
func (t *tokenSimple) enable() {
if t.simpleTokenTTL <= 0 {
t.simpleTokenTTL = simpleTokenTTLDefault
}
delf := func(tk string) {
if username, ok := t.simpleTokens[tk]; ok {
t.lg.Info(
"deleted a simple token",
zap.String("user-name", username),
zap.String("token", tk),
)
delete(t.simpleTokens, tk)
}
}
t.simpleTokenKeeper = &simpleTokenTTLKeeper{
tokens: make(map[string]time.Time),
donec: make(chan struct{}),
stopc: make(chan struct{}),
deleteTokenFunc: delf,
mu: &t.simpleTokensMu,
simpleTokenTTL: t.simpleTokenTTL,
}
go t.simpleTokenKeeper.run()
}
func (t *tokenSimple) disable() {
t.simpleTokensMu.Lock()
tk := t.simpleTokenKeeper
t.simpleTokenKeeper = nil
t.simpleTokens = make(map[string]string) // invalidate all tokens
t.simpleTokensMu.Unlock()
if tk != nil {
tk.stop()
}
}
func (t *tokenSimple) info(ctx context.Context, token string, revision uint64) (*AuthInfo, bool) {
if !t.isValidSimpleToken(ctx, token) {
return nil, false
}
t.simpleTokensMu.Lock()
username, ok := t.simpleTokens[token]
if ok && t.simpleTokenKeeper != nil {
t.simpleTokenKeeper.resetSimpleToken(token)
}
t.simpleTokensMu.Unlock()
return &AuthInfo{Username: username, Revision: revision}, ok
}
func (t *tokenSimple) assign(ctx context.Context, username string, rev uint64) (string, error) {
// rev isn't used in simple token, it is only used in JWT
index := ctx.Value(AuthenticateParamIndex{}).(uint64)
simpleTokenPrefix := ctx.Value(AuthenticateParamSimpleTokenPrefix{}).(string)
token := fmt.Sprintf("%s.%d", simpleTokenPrefix, index)
t.assignSimpleTokenToUser(username, token)
return token, nil
}
func (t *tokenSimple) isValidSimpleToken(ctx context.Context, token string) bool {
splitted := strings.Split(token, ".")
if len(splitted) != 2 {
return false
}
index, err := strconv.ParseUint(splitted[1], 10, 0)
if err != nil {
return false
}
select {
case <-t.indexWaiter(uint64(index)):
return true
case <-ctx.Done():
}
return false
}
func newTokenProviderSimple(lg *zap.Logger, indexWaiter func(uint64) <-chan struct{}, TokenTTL time.Duration) *tokenSimple {
if lg == nil {
lg = zap.NewNop()
}
return &tokenSimple{
lg: lg,
simpleTokens: make(map[string]string),
indexWaiter: indexWaiter,
simpleTokenTTL: TokenTTL,
}
}

View File

@@ -0,0 +1,70 @@
// Copyright 2017 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"testing"
"go.uber.org/zap"
)
// TestSimpleTokenDisabled ensures that TokenProviderSimple behaves correctly when
// disabled.
func TestSimpleTokenDisabled(t *testing.T) {
initialState := newTokenProviderSimple(zap.NewExample(), dummyIndexWaiter, simpleTokenTTLDefault)
explicitlyDisabled := newTokenProviderSimple(zap.NewExample(), dummyIndexWaiter, simpleTokenTTLDefault)
explicitlyDisabled.enable()
explicitlyDisabled.disable()
for _, tp := range []*tokenSimple{initialState, explicitlyDisabled} {
ctx := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(1)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
token, err := tp.assign(ctx, "user1", 0)
if err != nil {
t.Fatal(err)
}
authInfo, ok := tp.info(ctx, token, 0)
if ok {
t.Errorf("expected (true, \"user1\") got (%t, %s)", ok, authInfo.Username)
}
tp.invalidateUser("user1") // should be no-op
}
}
// TestSimpleTokenAssign ensures that TokenProviderSimple can correctly assign a
// token, look it up with info, and invalidate it by user.
func TestSimpleTokenAssign(t *testing.T) {
tp := newTokenProviderSimple(zap.NewExample(), dummyIndexWaiter, simpleTokenTTLDefault)
tp.enable()
defer tp.disable()
ctx := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(1)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
token, err := tp.assign(ctx, "user1", 0)
if err != nil {
t.Fatal(err)
}
authInfo, ok := tp.info(ctx, token, 0)
if !ok || authInfo.Username != "user1" {
t.Errorf("expected (true, \"token2\") got (%t, %s)", ok, authInfo.Username)
}
tp.invalidateUser("user1")
_, ok = tp.info(context.TODO(), token, 0)
if ok {
t.Errorf("expected ok == false after user is invalidated")
}
}

1334
server/auth/store.go Normal file

File diff suppressed because it is too large Load Diff

956
server/auth/store_test.go Normal file
View File

@@ -0,0 +1,956 @@
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"encoding/base64"
"fmt"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
"go.etcd.io/etcd/api/v3/authpb"
pb "go.etcd.io/etcd/api/v3/etcdserverpb"
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
"go.etcd.io/etcd/v3/mvcc/backend"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"google.golang.org/grpc/metadata"
)
func dummyIndexWaiter(index uint64) <-chan struct{} {
ch := make(chan struct{})
go func() {
ch <- struct{}{}
}()
return ch
}
// TestNewAuthStoreRevision ensures newly auth store
// keeps the old revision when there are no changes.
func TestNewAuthStoreRevision(t *testing.T) {
b, tPath := backend.NewDefaultTmpBackend()
defer os.Remove(tPath)
tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
if err != nil {
t.Fatal(err)
}
as := NewAuthStore(zap.NewExample(), b, nil, tp, bcrypt.MinCost)
err = enableAuthAndCreateRoot(as)
if err != nil {
t.Fatal(err)
}
old := as.Revision()
as.Close()
b.Close()
// no changes to commit
b2 := backend.NewDefaultBackend(tPath)
defer b2.Close()
as = NewAuthStore(zap.NewExample(), b2, nil, tp, bcrypt.MinCost)
defer as.Close()
new := as.Revision()
if old != new {
t.Fatalf("expected revision %d, got %d", old, new)
}
}
// TestNewAuthStoreBryptCost ensures that NewAuthStore uses default when given bcrypt-cost is invalid
func TestNewAuthStoreBcryptCost(t *testing.T) {
b, tPath := backend.NewDefaultTmpBackend()
defer b.Close()
defer os.Remove(tPath)
tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
if err != nil {
t.Fatal(err)
}
invalidCosts := [2]int{bcrypt.MinCost - 1, bcrypt.MaxCost + 1}
for _, invalidCost := range invalidCosts {
as := NewAuthStore(zap.NewExample(), b, nil, tp, invalidCost)
defer as.Close()
if as.BcryptCost() != bcrypt.DefaultCost {
t.Fatalf("expected DefaultCost when bcryptcost is invalid")
}
}
}
func encodePassword(s string) string {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(s), bcrypt.MinCost)
return base64.StdEncoding.EncodeToString([]byte(hashedPassword))
}
func setupAuthStore(t *testing.T) (store *authStore, teardownfunc func(t *testing.T)) {
b, tPath := backend.NewDefaultTmpBackend()
tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
if err != nil {
t.Fatal(err)
}
as := NewAuthStore(zap.NewExample(), b, nil, tp, bcrypt.MinCost)
err = enableAuthAndCreateRoot(as)
if err != nil {
t.Fatal(err)
}
// adds a new role
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test"})
if err != nil {
t.Fatal(err)
}
ua := &pb.AuthUserAddRequest{Name: "foo", HashedPassword: encodePassword("bar"), Options: &authpb.UserAddOptions{NoPassword: false}}
_, err = as.UserAdd(ua) // add a non-existing user
if err != nil {
t.Fatal(err)
}
tearDown := func(_ *testing.T) {
b.Close()
os.Remove(tPath)
as.Close()
}
return as, tearDown
}
func enableAuthAndCreateRoot(as *authStore) error {
_, err := as.UserAdd(&pb.AuthUserAddRequest{Name: "root", HashedPassword: encodePassword("root"), Options: &authpb.UserAddOptions{NoPassword: false}})
if err != nil {
return err
}
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: "root"})
if err != nil {
return err
}
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "root", Role: "root"})
if err != nil {
return err
}
return as.AuthEnable()
}
func TestUserAdd(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
ua := &pb.AuthUserAddRequest{Name: "foo", Options: &authpb.UserAddOptions{NoPassword: false}}
_, err := as.UserAdd(ua) // add an existing user
if err == nil {
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
}
if err != ErrUserAlreadyExist {
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
}
ua = &pb.AuthUserAddRequest{Name: "", Options: &authpb.UserAddOptions{NoPassword: false}}
_, err = as.UserAdd(ua) // add a user with empty name
if err != ErrUserEmpty {
t.Fatal(err)
}
}
func TestRecover(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer as.Close()
defer tearDown(t)
as.enabled = false
as.Recover(as.be)
if !as.IsAuthEnabled() {
t.Fatalf("expected auth enabled got disabled")
}
}
func TestCheckPassword(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
// auth a non-existing user
_, err := as.CheckPassword("foo-test", "bar")
if err == nil {
t.Fatalf("expected %v, got %v", ErrAuthFailed, err)
}
if err != ErrAuthFailed {
t.Fatalf("expected %v, got %v", ErrAuthFailed, err)
}
// auth an existing user with correct password
_, err = as.CheckPassword("foo", "bar")
if err != nil {
t.Fatal(err)
}
// auth an existing user but with wrong password
_, err = as.CheckPassword("foo", "")
if err == nil {
t.Fatalf("expected %v, got %v", ErrAuthFailed, err)
}
if err != ErrAuthFailed {
t.Fatalf("expected %v, got %v", ErrAuthFailed, err)
}
}
func TestUserDelete(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
// delete an existing user
ud := &pb.AuthUserDeleteRequest{Name: "foo"}
_, err := as.UserDelete(ud)
if err != nil {
t.Fatal(err)
}
// delete a non-existing user
_, err = as.UserDelete(ud)
if err == nil {
t.Fatalf("expected %v, got %v", ErrUserNotFound, err)
}
if err != ErrUserNotFound {
t.Fatalf("expected %v, got %v", ErrUserNotFound, err)
}
}
func TestUserChangePassword(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
ctx1 := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(1)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
_, err := as.Authenticate(ctx1, "foo", "bar")
if err != nil {
t.Fatal(err)
}
_, err = as.UserChangePassword(&pb.AuthUserChangePasswordRequest{Name: "foo", HashedPassword: encodePassword("baz")})
if err != nil {
t.Fatal(err)
}
ctx2 := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(2)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
_, err = as.Authenticate(ctx2, "foo", "baz")
if err != nil {
t.Fatal(err)
}
// change a non-existing user
_, err = as.UserChangePassword(&pb.AuthUserChangePasswordRequest{Name: "foo-test", HashedPassword: encodePassword("bar")})
if err == nil {
t.Fatalf("expected %v, got %v", ErrUserNotFound, err)
}
if err != ErrUserNotFound {
t.Fatalf("expected %v, got %v", ErrUserNotFound, err)
}
}
func TestRoleAdd(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
// adds a new role
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test-1"})
if err != nil {
t.Fatal(err)
}
// add a role with empty name
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: ""})
if err != ErrRoleEmpty {
t.Fatal(err)
}
}
func TestUserGrant(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
// grants a role to the user
_, err := as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test"})
if err != nil {
t.Fatal(err)
}
// grants a role to a non-existing user
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo-test", Role: "role-test"})
if err == nil {
t.Errorf("expected %v, got %v", ErrUserNotFound, err)
}
if err != ErrUserNotFound {
t.Errorf("expected %v, got %v", ErrUserNotFound, err)
}
}
func TestHasRole(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
// grants a role to the user
_, err := as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test"})
if err != nil {
t.Fatal(err)
}
// checks role reflects correctly
hr := as.HasRole("foo", "role-test")
if !hr {
t.Fatal("expected role granted, got false")
}
// checks non existent role
hr = as.HasRole("foo", "non-existent-role")
if hr {
t.Fatal("expected role not found, got true")
}
// checks non existent user
hr = as.HasRole("nouser", "role-test")
if hr {
t.Fatal("expected user not found got true")
}
}
func TestIsOpPermitted(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
// add new role
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test-1"})
if err != nil {
t.Fatal(err)
}
perm := &authpb.Permission{
PermType: authpb.WRITE,
Key: []byte("Keys"),
RangeEnd: []byte("RangeEnd"),
}
_, err = as.RoleGrantPermission(&pb.AuthRoleGrantPermissionRequest{
Name: "role-test-1",
Perm: perm,
})
if err != nil {
t.Fatal(err)
}
// grants a role to the user
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test-1"})
if err != nil {
t.Fatal(err)
}
// check permission reflected to user
err = as.isOpPermitted("foo", as.Revision(), perm.Key, perm.RangeEnd, perm.PermType)
if err != nil {
t.Fatal(err)
}
}
func TestGetUser(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
_, err := as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test"})
if err != nil {
t.Fatal(err)
}
u, err := as.UserGet(&pb.AuthUserGetRequest{Name: "foo"})
if err != nil {
t.Fatal(err)
}
if u == nil {
t.Fatal("expect user not nil, got nil")
}
expected := []string{"role-test"}
if !reflect.DeepEqual(expected, u.Roles) {
t.Errorf("expected %v, got %v", expected, u.Roles)
}
// check non existent user
_, err = as.UserGet(&pb.AuthUserGetRequest{Name: "nouser"})
if err == nil {
t.Errorf("expected %v, got %v", ErrUserNotFound, err)
}
}
func TestListUsers(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
ua := &pb.AuthUserAddRequest{Name: "user1", HashedPassword: encodePassword("pwd1"), Options: &authpb.UserAddOptions{NoPassword: false}}
_, err := as.UserAdd(ua) // add a non-existing user
if err != nil {
t.Fatal(err)
}
ul, err := as.UserList(&pb.AuthUserListRequest{})
if err != nil {
t.Fatal(err)
}
if !contains(ul.Users, "root") {
t.Errorf("expected %v in %v", "root", ul.Users)
}
if !contains(ul.Users, "user1") {
t.Errorf("expected %v in %v", "user1", ul.Users)
}
}
func TestRoleGrantPermission(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test-1"})
if err != nil {
t.Fatal(err)
}
perm := &authpb.Permission{
PermType: authpb.WRITE,
Key: []byte("Keys"),
RangeEnd: []byte("RangeEnd"),
}
_, err = as.RoleGrantPermission(&pb.AuthRoleGrantPermissionRequest{
Name: "role-test-1",
Perm: perm,
})
if err != nil {
t.Error(err)
}
r, err := as.RoleGet(&pb.AuthRoleGetRequest{Role: "role-test-1"})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(perm, r.Perm[0]) {
t.Errorf("expected %v, got %v", perm, r.Perm[0])
}
}
func TestRoleRevokePermission(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test-1"})
if err != nil {
t.Fatal(err)
}
perm := &authpb.Permission{
PermType: authpb.WRITE,
Key: []byte("Keys"),
RangeEnd: []byte("RangeEnd"),
}
_, err = as.RoleGrantPermission(&pb.AuthRoleGrantPermissionRequest{
Name: "role-test-1",
Perm: perm,
})
if err != nil {
t.Fatal(err)
}
_, err = as.RoleGet(&pb.AuthRoleGetRequest{Role: "role-test-1"})
if err != nil {
t.Fatal(err)
}
_, err = as.RoleRevokePermission(&pb.AuthRoleRevokePermissionRequest{
Role: "role-test-1",
Key: []byte("Keys"),
RangeEnd: []byte("RangeEnd"),
})
if err != nil {
t.Fatal(err)
}
var r *pb.AuthRoleGetResponse
r, err = as.RoleGet(&pb.AuthRoleGetRequest{Role: "role-test-1"})
if err != nil {
t.Fatal(err)
}
if len(r.Perm) != 0 {
t.Errorf("expected %v, got %v", 0, len(r.Perm))
}
}
func TestUserRevokePermission(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
_, err := as.RoleAdd(&pb.AuthRoleAddRequest{Name: "role-test-1"})
if err != nil {
t.Fatal(err)
}
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test"})
if err != nil {
t.Fatal(err)
}
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "foo", Role: "role-test-1"})
if err != nil {
t.Fatal(err)
}
u, err := as.UserGet(&pb.AuthUserGetRequest{Name: "foo"})
if err != nil {
t.Fatal(err)
}
expected := []string{"role-test", "role-test-1"}
if !reflect.DeepEqual(expected, u.Roles) {
t.Fatalf("expected %v, got %v", expected, u.Roles)
}
_, err = as.UserRevokeRole(&pb.AuthUserRevokeRoleRequest{Name: "foo", Role: "role-test-1"})
if err != nil {
t.Fatal(err)
}
u, err = as.UserGet(&pb.AuthUserGetRequest{Name: "foo"})
if err != nil {
t.Fatal(err)
}
expected = []string{"role-test"}
if !reflect.DeepEqual(expected, u.Roles) {
t.Errorf("expected %v, got %v", expected, u.Roles)
}
}
func TestRoleDelete(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
_, err := as.RoleDelete(&pb.AuthRoleDeleteRequest{Role: "role-test"})
if err != nil {
t.Fatal(err)
}
rl, err := as.RoleList(&pb.AuthRoleListRequest{})
if err != nil {
t.Fatal(err)
}
expected := []string{"root"}
if !reflect.DeepEqual(expected, rl.Roles) {
t.Errorf("expected %v, got %v", expected, rl.Roles)
}
}
func TestAuthInfoFromCtx(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
ctx := context.Background()
ai, err := as.AuthInfoFromCtx(ctx)
if err != nil && ai != nil {
t.Errorf("expected (nil, nil), got (%v, %v)", ai, err)
}
// as if it came from RPC
ctx = metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"tokens": "dummy"}))
ai, err = as.AuthInfoFromCtx(ctx)
if err != nil && ai != nil {
t.Errorf("expected (nil, nil), got (%v, %v)", ai, err)
}
ctx = context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(1)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
resp, err := as.Authenticate(ctx, "foo", "bar")
if err != nil {
t.Error(err)
}
ctx = metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{rpctypes.TokenFieldNameGRPC: "Invalid Token"}))
_, err = as.AuthInfoFromCtx(ctx)
if err != ErrInvalidAuthToken {
t.Errorf("expected %v, got %v", ErrInvalidAuthToken, err)
}
ctx = metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{rpctypes.TokenFieldNameGRPC: "Invalid.Token"}))
_, err = as.AuthInfoFromCtx(ctx)
if err != ErrInvalidAuthToken {
t.Errorf("expected %v, got %v", ErrInvalidAuthToken, err)
}
ctx = metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{rpctypes.TokenFieldNameGRPC: resp.Token}))
ai, err = as.AuthInfoFromCtx(ctx)
if err != nil {
t.Error(err)
}
if ai.Username != "foo" {
t.Errorf("expected %v, got %v", "foo", ai.Username)
}
}
func TestAuthDisable(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
as.AuthDisable()
ctx := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(2)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
_, err := as.Authenticate(ctx, "foo", "bar")
if err != ErrAuthNotEnabled {
t.Errorf("expected %v, got %v", ErrAuthNotEnabled, err)
}
// Disabling disabled auth to make sure it can return safely if store is already disabled.
as.AuthDisable()
_, err = as.Authenticate(ctx, "foo", "bar")
if err != ErrAuthNotEnabled {
t.Errorf("expected %v, got %v", ErrAuthNotEnabled, err)
}
}
func TestIsAuthEnabled(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
// enable authentication to test the first possible condition
as.AuthEnable()
status := as.IsAuthEnabled()
ctx := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(2)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
_, _ = as.Authenticate(ctx, "foo", "bar")
if status != true {
t.Errorf("expected %v, got %v", true, false)
}
// Disabling disabled auth to test the other condition that can be return
as.AuthDisable()
status = as.IsAuthEnabled()
_, _ = as.Authenticate(ctx, "foo", "bar")
if status != false {
t.Errorf("expected %v, got %v", false, true)
}
}
// TestAuthRevisionRace ensures that access to authStore.revision is thread-safe.
func TestAuthInfoFromCtxRace(t *testing.T) {
b, tPath := backend.NewDefaultTmpBackend()
defer b.Close()
defer os.Remove(tPath)
tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
if err != nil {
t.Fatal(err)
}
as := NewAuthStore(zap.NewExample(), b, nil, tp, bcrypt.MinCost)
defer as.Close()
donec := make(chan struct{})
go func() {
defer close(donec)
ctx := metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{rpctypes.TokenFieldNameGRPC: "test"}))
as.AuthInfoFromCtx(ctx)
}()
as.UserAdd(&pb.AuthUserAddRequest{Name: "test", Options: &authpb.UserAddOptions{NoPassword: false}})
<-donec
}
func TestIsAdminPermitted(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
err := as.IsAdminPermitted(&AuthInfo{Username: "root", Revision: 1})
if err != nil {
t.Errorf("expected nil, got %v", err)
}
// invalid user
err = as.IsAdminPermitted(&AuthInfo{Username: "rooti", Revision: 1})
if err != ErrUserNotFound {
t.Errorf("expected %v, got %v", ErrUserNotFound, err)
}
// empty user
err = as.IsAdminPermitted(&AuthInfo{Username: "", Revision: 1})
if err != ErrUserEmpty {
t.Errorf("expected %v, got %v", ErrUserEmpty, err)
}
// non-admin user
err = as.IsAdminPermitted(&AuthInfo{Username: "foo", Revision: 1})
if err != ErrPermissionDenied {
t.Errorf("expected %v, got %v", ErrPermissionDenied, err)
}
// disabled auth should return nil
as.AuthDisable()
err = as.IsAdminPermitted(&AuthInfo{Username: "root", Revision: 1})
if err != nil {
t.Errorf("expected nil, got %v", err)
}
}
func TestRecoverFromSnapshot(t *testing.T) {
as, teardown := setupAuthStore(t)
defer teardown(t)
ua := &pb.AuthUserAddRequest{Name: "foo", Options: &authpb.UserAddOptions{NoPassword: false}}
_, err := as.UserAdd(ua) // add an existing user
if err == nil {
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
}
if err != ErrUserAlreadyExist {
t.Fatalf("expected %v, got %v", ErrUserAlreadyExist, err)
}
ua = &pb.AuthUserAddRequest{Name: "", Options: &authpb.UserAddOptions{NoPassword: false}}
_, err = as.UserAdd(ua) // add a user with empty name
if err != ErrUserEmpty {
t.Fatal(err)
}
as.Close()
tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
if err != nil {
t.Fatal(err)
}
as2 := NewAuthStore(zap.NewExample(), as.be, nil, tp, bcrypt.MinCost)
defer as2.Close()
if !as2.IsAuthEnabled() {
t.Fatal("recovering authStore from existing backend failed")
}
ul, err := as.UserList(&pb.AuthUserListRequest{})
if err != nil {
t.Fatal(err)
}
if !contains(ul.Users, "root") {
t.Errorf("expected %v in %v", "root", ul.Users)
}
}
func contains(array []string, str string) bool {
for _, s := range array {
if s == str {
return true
}
}
return false
}
func TestHammerSimpleAuthenticate(t *testing.T) {
// set TTL values low to try to trigger races
oldTTL, oldTTLRes := simpleTokenTTLDefault, simpleTokenTTLResolution
defer func() {
simpleTokenTTLDefault = oldTTL
simpleTokenTTLResolution = oldTTLRes
}()
simpleTokenTTLDefault = 10 * time.Millisecond
simpleTokenTTLResolution = simpleTokenTTLDefault
users := make(map[string]struct{})
as, tearDown := setupAuthStore(t)
defer tearDown(t)
// create lots of users
for i := 0; i < 50; i++ {
u := fmt.Sprintf("user-%d", i)
ua := &pb.AuthUserAddRequest{Name: u, HashedPassword: encodePassword("123"), Options: &authpb.UserAddOptions{NoPassword: false}}
if _, err := as.UserAdd(ua); err != nil {
t.Fatal(err)
}
users[u] = struct{}{}
}
// hammer on authenticate with lots of users
for i := 0; i < 10; i++ {
var wg sync.WaitGroup
wg.Add(len(users))
for u := range users {
go func(user string) {
defer wg.Done()
token := fmt.Sprintf("%s(%d)", user, i)
ctx := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(1)), AuthenticateParamSimpleTokenPrefix{}, token)
if _, err := as.Authenticate(ctx, user, "123"); err != nil {
t.Error(err)
}
if _, err := as.AuthInfoFromCtx(ctx); err != nil {
t.Error(err)
}
}(u)
}
time.Sleep(time.Millisecond)
wg.Wait()
}
}
// TestRolesOrder tests authpb.User.Roles is sorted
func TestRolesOrder(t *testing.T) {
b, tPath := backend.NewDefaultTmpBackend()
defer b.Close()
defer os.Remove(tPath)
tp, err := NewTokenProvider(zap.NewExample(), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
defer tp.disable()
if err != nil {
t.Fatal(err)
}
as := NewAuthStore(zap.NewExample(), b, nil, tp, bcrypt.MinCost)
defer as.Close()
err = enableAuthAndCreateRoot(as)
if err != nil {
t.Fatal(err)
}
username := "user"
_, err = as.UserAdd(&pb.AuthUserAddRequest{Name: username, HashedPassword: encodePassword("pass"), Options: &authpb.UserAddOptions{NoPassword: false}})
if err != nil {
t.Fatal(err)
}
roles := []string{"role1", "role2", "abc", "xyz", "role3"}
for _, role := range roles {
_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: role})
if err != nil {
t.Fatal(err)
}
_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: username, Role: role})
if err != nil {
t.Fatal(err)
}
}
user, err := as.UserGet(&pb.AuthUserGetRequest{Name: username})
if err != nil {
t.Fatal(err)
}
for i := 1; i < len(user.Roles); i++ {
if strings.Compare(user.Roles[i-1], user.Roles[i]) != -1 {
t.Errorf("User.Roles isn't sorted (%s vs %s)", user.Roles[i-1], user.Roles[i])
}
}
}
func TestAuthInfoFromCtxWithRootSimple(t *testing.T) {
testAuthInfoFromCtxWithRoot(t, tokenTypeSimple)
}
func TestAuthInfoFromCtxWithRootJWT(t *testing.T) {
opts := testJWTOpts()
testAuthInfoFromCtxWithRoot(t, opts)
}
// testAuthInfoFromCtxWithRoot ensures "WithRoot" properly embeds token in the context.
func testAuthInfoFromCtxWithRoot(t *testing.T, opts string) {
b, tPath := backend.NewDefaultTmpBackend()
defer b.Close()
defer os.Remove(tPath)
tp, err := NewTokenProvider(zap.NewExample(), opts, dummyIndexWaiter, simpleTokenTTLDefault)
if err != nil {
t.Fatal(err)
}
as := NewAuthStore(zap.NewExample(), b, nil, tp, bcrypt.MinCost)
defer as.Close()
if err = enableAuthAndCreateRoot(as); err != nil {
t.Fatal(err)
}
ctx := context.Background()
ctx = as.WithRoot(ctx)
ai, aerr := as.AuthInfoFromCtx(ctx)
if aerr != nil {
t.Error(err)
}
if ai == nil {
t.Error("expected non-nil *AuthInfo")
}
if ai.Username != "root" {
t.Errorf("expected user name 'root', got %+v", ai)
}
}
func TestUserNoPasswordAdd(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
username := "usernopass"
ua := &pb.AuthUserAddRequest{Name: username, Options: &authpb.UserAddOptions{NoPassword: true}}
_, err := as.UserAdd(ua)
if err != nil {
t.Fatal(err)
}
ctx := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(1)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
_, err = as.Authenticate(ctx, username, "")
if err != ErrAuthFailed {
t.Fatalf("expected %v, got %v", ErrAuthFailed, err)
}
}
func TestUserAddWithOldLog(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
ua := &pb.AuthUserAddRequest{Name: "bar", Password: "baz", Options: &authpb.UserAddOptions{NoPassword: false}}
_, err := as.UserAdd(ua)
if err != nil {
t.Fatal(err)
}
}
func TestUserChangePasswordWithOldLog(t *testing.T) {
as, tearDown := setupAuthStore(t)
defer tearDown(t)
ctx1 := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(1)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
_, err := as.Authenticate(ctx1, "foo", "bar")
if err != nil {
t.Fatal(err)
}
_, err = as.UserChangePassword(&pb.AuthUserChangePasswordRequest{Name: "foo", Password: "baz"})
if err != nil {
t.Fatal(err)
}
ctx2 := context.WithValue(context.WithValue(context.TODO(), AuthenticateParamIndex{}, uint64(2)), AuthenticateParamSimpleTokenPrefix{}, "dummy")
_, err = as.Authenticate(ctx2, "foo", "baz")
if err != nil {
t.Fatal(err)
}
// change a non-existing user
_, err = as.UserChangePassword(&pb.AuthUserChangePasswordRequest{Name: "foo-test", HashedPassword: encodePassword("bar")})
if err == nil {
t.Fatalf("expected %v, got %v", ErrUserNotFound, err)
}
if err != ErrUserNotFound {
t.Fatalf("expected %v, got %v", ErrUserNotFound, err)
}
}