mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
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:
16
server/auth/doc.go
Normal file
16
server/auth/doc.go
Normal 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
163
server/auth/jwt.go
Normal 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
204
server/auth/jwt_test.go
Normal 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
15
server/auth/main_test.go
Normal 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
42
server/auth/metrics.go
Normal 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
35
server/auth/nop.go
Normal 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
192
server/auth/options.go
Normal 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
|
||||
}
|
||||
141
server/auth/range_perm_cache.go
Normal file
141
server/auth/range_perm_cache.go
Normal 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
|
||||
}
|
||||
102
server/auth/range_perm_cache_test.go
Normal file
102
server/auth/range_perm_cache_test.go
Normal 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
247
server/auth/simple_token.go
Normal 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,
|
||||
}
|
||||
}
|
||||
70
server/auth/simple_token_test.go
Normal file
70
server/auth/simple_token_test.go
Normal 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
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
956
server/auth/store_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user