[VOL-5486] Fix deprecated versions
Change-Id: I3e03ea246020547ae75fa92ce8cf5cbba7e8f3bb
Signed-off-by: Abhay Kumar <abhay.kumar@radisys.com>
diff --git a/vendor/go.etcd.io/etcd/server/v3/auth/doc.go b/vendor/go.etcd.io/etcd/server/v3/auth/doc.go
new file mode 100644
index 0000000..72741a1
--- /dev/null
+++ b/vendor/go.etcd.io/etcd/server/v3/auth/doc.go
@@ -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
diff --git a/vendor/go.etcd.io/etcd/server/v3/auth/jwt.go b/vendor/go.etcd.io/etcd/server/v3/auth/jwt.go
new file mode 100644
index 0000000..e6aad18
--- /dev/null
+++ b/vendor/go.etcd.io/etcd/server/v3/auth/jwt.go
@@ -0,0 +1,177 @@
+// 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/ed25519"
+ "crypto/rsa"
+ "errors"
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+ "go.uber.org/zap"
+)
+
+type tokenJWT struct {
+ lg *zap.Logger
+ signMethod jwt.SigningMethod
+ key any
+ 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 float64
+ )
+
+ parsed, err := jwt.Parse(token, func(token *jwt.Token) (any, 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
+ case ed25519.PrivateKey:
+ return k.Public(), nil
+ default:
+ return t.key, nil
+ }
+ })
+ if err != nil {
+ t.lg.Warn(
+ "failed to parse a JWT token",
+ zap.Error(err),
+ )
+ return nil, false
+ }
+
+ claims, ok := parsed.Claims.(jwt.MapClaims)
+ if !parsed.Valid || !ok {
+ t.lg.Warn("failed to obtain claims from a JWT token")
+ return nil, false
+ }
+
+ username, ok = claims["username"].(string)
+ if !ok {
+ t.lg.Warn("failed to obtain user claims from jwt token")
+ return nil, false
+ }
+
+ revision, ok = claims["revision"].(float64)
+ if !ok {
+ t.lg.Warn("failed to obtain revision claims from jwt token")
+ return nil, false
+ }
+
+ return &AuthInfo{Username: username, Revision: uint64(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
+ }
+
+ 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.SigningMethodEd25519:
+ if _, ok := t.key.(ed25519.PublicKey); ok {
+ t.verifyOnly = true
+ }
+ case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
+ if _, ok := t.key.(*rsa.PublicKey); ok {
+ t.verifyOnly = true
+ }
+ }
+
+ return t, nil
+}
diff --git a/vendor/go.etcd.io/etcd/server/v3/auth/metrics.go b/vendor/go.etcd.io/etcd/server/v3/auth/metrics.go
new file mode 100644
index 0000000..8bf9470
--- /dev/null
+++ b/vendor/go.etcd.io/etcd/server/v3/auth/metrics.go
@@ -0,0 +1,44 @@
+// 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 (
+ "sync"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+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)
+}
diff --git a/vendor/go.etcd.io/etcd/server/v3/auth/nop.go b/vendor/go.etcd.io/etcd/server/v3/auth/nop.go
new file mode 100644
index 0000000..8ba3f8c
--- /dev/null
+++ b/vendor/go.etcd.io/etcd/server/v3/auth/nop.go
@@ -0,0 +1,37 @@
+// 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
+}
diff --git a/vendor/go.etcd.io/etcd/server/v3/auth/options.go b/vendor/go.etcd.io/etcd/server/v3/auth/options.go
new file mode 100644
index 0000000..fa9db20
--- /dev/null
+++ b/vendor/go.etcd.io/etcd/server/v3/auth/options.go
@@ -0,0 +1,235 @@
+// 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"
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/rsa"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+)
+
+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,
+}
+
+// DefaultTTL will be used when a 'ttl' is not specified
+var 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 = os.ReadFile(file)
+ if err != nil {
+ return err
+ }
+ }
+
+ if file := optMap[optPrivateKey]; file != "" {
+ opts.PrivateKey, err = os.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() (any, error) {
+ switch opts.SignMethod.(type) {
+ case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
+ return opts.rsaKey()
+ case *jwt.SigningMethodECDSA:
+ return opts.ecKey()
+ case *jwt.SigningMethodEd25519:
+ return opts.edKey()
+ case *jwt.SigningMethodHMAC:
+ return opts.hmacKey()
+ default:
+ return nil, fmt.Errorf("unsupported signing method: %T", opts.SignMethod)
+ }
+}
+
+func (opts *jwtOptions) hmacKey() (any, error) {
+ if len(opts.PrivateKey) == 0 {
+ return nil, ErrMissingKey
+ }
+ return opts.PrivateKey, nil
+}
+
+func (opts *jwtOptions) rsaKey() (any, 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.Equal(priv.Public()) {
+ return nil, ErrKeyMismatch
+ }
+
+ return priv, nil
+}
+
+func (opts *jwtOptions) ecKey() (any, 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.Equal(priv.Public()) {
+ return nil, ErrKeyMismatch
+ }
+
+ return priv, nil
+}
+
+func (opts *jwtOptions) edKey() (any, error) {
+ var (
+ priv ed25519.PrivateKey
+ pub ed25519.PublicKey
+ err error
+ )
+
+ if len(opts.PrivateKey) > 0 {
+ var privKey crypto.PrivateKey
+ privKey, err = jwt.ParseEdPrivateKeyFromPEM(opts.PrivateKey)
+ if err != nil {
+ return nil, err
+ }
+ priv = privKey.(ed25519.PrivateKey)
+ }
+
+ if len(opts.PublicKey) > 0 {
+ var pubKey crypto.PublicKey
+ pubKey, err = jwt.ParseEdPublicKeyFromPEM(opts.PublicKey)
+ if err != nil {
+ return nil, err
+ }
+ pub = pubKey.(ed25519.PublicKey)
+ }
+
+ 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.Equal(priv.Public()) {
+ return nil, ErrKeyMismatch
+ }
+
+ return priv, nil
+}
diff --git a/vendor/go.etcd.io/etcd/server/v3/auth/range_perm_cache.go b/vendor/go.etcd.io/etcd/server/v3/auth/range_perm_cache.go
new file mode 100644
index 0000000..539ed29
--- /dev/null
+++ b/vendor/go.etcd.io/etcd/server/v3/auth/range_perm_cache.go
@@ -0,0 +1,204 @@
+// 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.uber.org/zap"
+
+ "go.etcd.io/etcd/api/v3/authpb"
+ "go.etcd.io/etcd/pkg/v3/adt"
+)
+
+func getMergedPerms(tx UnsafeAuthReader, userName string) *unifiedRangePermissions {
+ user := tx.UnsafeGetUser(userName)
+ if user == nil {
+ return nil
+ }
+
+ readPerms := adt.NewIntervalTree()
+ writePerms := adt.NewIntervalTree()
+
+ for _, roleName := range user.Roles {
+ role := tx.UnsafeGetRole(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 isOpenEnded(rangeEnd) {
+ rangeEnd = nil
+ // nil rangeEnd will be converetd to []byte{}, the largest element of BytesAffineComparable,
+ // in NewBytesAffineInterval().
+ }
+
+ 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(userName string, key, rangeEnd []byte, permtyp authpb.Permission_Type) bool {
+ // assumption: tx is Lock()ed
+ as.rangePermCacheMu.RLock()
+ defer as.rangePermCacheMu.RUnlock()
+
+ rangePerm, ok := as.rangePermCache[userName]
+ if !ok {
+ as.lg.Error(
+ "user doesn't exist",
+ zap.String("user-name", userName),
+ )
+ return false
+ }
+
+ if len(rangeEnd) == 0 {
+ return checkKeyPoint(as.lg, rangePerm, key, permtyp)
+ }
+
+ return checkKeyInterval(as.lg, rangePerm, key, rangeEnd, permtyp)
+}
+
+func (as *authStore) refreshRangePermCache(tx UnsafeAuthReader) {
+ // Note that every authentication configuration update calls this method and it invalidates the entire
+ // rangePermCache and reconstruct it based on information of users and roles stored in the backend.
+ // This can be a costly operation.
+ as.rangePermCacheMu.Lock()
+ defer as.rangePermCacheMu.Unlock()
+
+ as.lg.Debug("Refreshing rangePermCache")
+
+ as.rangePermCache = make(map[string]*unifiedRangePermissions)
+
+ users := tx.UnsafeGetAllUsers()
+ for _, user := range users {
+ userName := string(user.Name)
+ perms := getMergedPerms(tx, userName)
+ if perms == nil {
+ as.lg.Error(
+ "failed to create a merged permission",
+ zap.String("user-name", userName),
+ )
+ continue
+ }
+ as.rangePermCache[userName] = perms
+ }
+}
+
+type unifiedRangePermissions struct {
+ readPerms adt.IntervalTree
+ writePerms adt.IntervalTree
+}
+
+// Constraints related to key range
+// Assumptions:
+// a1. key must be non-nil
+// a2. []byte{} (in the case of string, "") is not a valid key of etcd
+// For representing an open-ended range, BytesAffineComparable uses []byte{} as the largest element.
+// a3. []byte{0x00} is the minimum valid etcd key
+//
+// Based on the above assumptions, key and rangeEnd must follow below rules:
+// b1. for representing a single key point, rangeEnd should be nil or zero length byte array (in the case of string, "")
+// Rule a2 guarantees that (X, []byte{}) for any X is not a valid range. So such ranges can be used for representing
+// a single key permission.
+//
+// b2. key range with upper limit, like (X, Y), larger or equal to X and smaller than Y
+//
+// b3. key range with open-ended, like (X, <open ended>), is represented like (X, []byte{0x00})
+// Because of rule a3, if we have (X, []byte{0x00}), such a range represents an empty range and makes no sense to have
+// such a permission. So we use []byte{0x00} for representing an open-ended permission.
+// Note that rangeEnd with []byte{0x00} will be converted into []byte{} before inserted into the interval tree
+// (rule a2 ensures that this is the largest element).
+// Special range like key = []byte{0x00} and rangeEnd = []byte{0x00} is treated as a range which matches with all keys.
+//
+// Treating a range whose rangeEnd with []byte{0x00} as an open-ended comes from the rules of Range() and Watch() API.
+
+func isOpenEnded(rangeEnd []byte) bool { // check rule b3
+ return len(rangeEnd) == 1 && rangeEnd[0] == 0
+}
+
+func isValidPermissionRange(key, rangeEnd []byte) bool {
+ if len(key) == 0 {
+ return false
+ }
+ if len(rangeEnd) == 0 { // ensure rule b1
+ return true
+ }
+
+ begin := adt.BytesAffineComparable(key)
+ end := adt.BytesAffineComparable(rangeEnd)
+ if begin.Compare(end) == -1 { // rule b2
+ return true
+ }
+
+ return isOpenEnded(rangeEnd)
+}
diff --git a/vendor/go.etcd.io/etcd/server/v3/auth/simple_token.go b/vendor/go.etcd.io/etcd/server/v3/auth/simple_token.go
new file mode 100644
index 0000000..f8272b1
--- /dev/null
+++ b/vendor/go.etcd.io/etcd/server/v3/auth/simple_token.go
@@ -0,0 +1,257 @@
+// 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"
+ "errors"
+ "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() {
+ t.simpleTokensMu.Lock()
+ defer t.simpleTokensMu.Unlock()
+ if t.simpleTokenKeeper != nil { // already enabled
+ return
+ }
+ if t.simpleTokenTTL <= 0 {
+ t.simpleTokenTTL = simpleTokenTTLDefault
+ }
+
+ delf := func(tk string) {
+ if username, ok := t.simpleTokens[tk]; ok {
+ t.lg.Debug(
+ "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
+ var index uint64
+ var ok bool
+ if index, ok = ctx.Value(AuthenticateParamIndex{}).(uint64); !ok {
+ return "", errors.New("failed to assign")
+ }
+ 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(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,
+ }
+}
diff --git a/vendor/go.etcd.io/etcd/server/v3/auth/store.go b/vendor/go.etcd.io/etcd/server/v3/auth/store.go
new file mode 100644
index 0000000..cfacfb0
--- /dev/null
+++ b/vendor/go.etcd.io/etcd/server/v3/auth/store.go
@@ -0,0 +1,1230 @@
+// 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 (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "errors"
+ "sort"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "go.uber.org/zap"
+ "golang.org/x/crypto/bcrypt"
+ "google.golang.org/grpc/credentials"
+ "google.golang.org/grpc/metadata"
+ "google.golang.org/grpc/peer"
+
+ "go.etcd.io/etcd/api/v3/authpb"
+ pb "go.etcd.io/etcd/api/v3/etcdserverpb"
+ "go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
+)
+
+var _ AuthStore = (*authStore)(nil)
+
+var (
+ rootPerm = authpb.Permission{PermType: authpb.READWRITE, Key: []byte{}, RangeEnd: []byte{0}}
+
+ ErrRootUserNotExist = errors.New("auth: root user does not exist")
+ ErrRootRoleNotExist = errors.New("auth: root user does not have root role")
+ ErrUserAlreadyExist = errors.New("auth: user already exists")
+ ErrUserEmpty = errors.New("auth: user name is empty")
+ ErrUserNotFound = errors.New("auth: user not found")
+ ErrRoleAlreadyExist = errors.New("auth: role already exists")
+ ErrRoleNotFound = errors.New("auth: role not found")
+ ErrRoleEmpty = errors.New("auth: role name is empty")
+ ErrPermissionNotGiven = errors.New("auth: permission not given")
+ ErrAuthFailed = errors.New("auth: authentication failed, invalid user ID or password")
+ ErrNoPasswordUser = errors.New("auth: authentication failed, password was given for no password user")
+ ErrPermissionDenied = errors.New("auth: permission denied")
+ ErrRoleNotGranted = errors.New("auth: role is not granted to the user")
+ ErrPermissionNotGranted = errors.New("auth: permission is not granted to the role")
+ ErrAuthNotEnabled = errors.New("auth: authentication is not enabled")
+ ErrAuthOldRevision = errors.New("auth: revision in header is old")
+ ErrInvalidAuthToken = errors.New("auth: invalid auth token")
+ ErrInvalidAuthOpts = errors.New("auth: invalid auth options")
+ ErrInvalidAuthMgmt = errors.New("auth: invalid auth management")
+ ErrInvalidAuthMethod = errors.New("auth: invalid auth signature method")
+ ErrMissingKey = errors.New("auth: missing key data")
+ ErrKeyMismatch = errors.New("auth: public and private keys don't match")
+ ErrVerifyOnly = errors.New("auth: token signing attempted with verify-only key")
+)
+
+const (
+ rootUser = "root"
+ rootRole = "root"
+
+ tokenTypeSimple = "simple"
+ tokenTypeJWT = "jwt"
+)
+
+type AuthInfo struct {
+ Username string
+ Revision uint64
+}
+
+// AuthenticateParamIndex is used for a key of context in the parameters of Authenticate()
+type AuthenticateParamIndex struct{}
+
+// AuthenticateParamSimpleTokenPrefix is used for a key of context in the parameters of Authenticate()
+type AuthenticateParamSimpleTokenPrefix struct{}
+
+// AuthStore defines auth storage interface.
+type AuthStore interface {
+ // AuthEnable turns on the authentication feature
+ AuthEnable() error
+
+ // AuthDisable turns off the authentication feature
+ AuthDisable()
+
+ // IsAuthEnabled returns true if the authentication feature is enabled.
+ IsAuthEnabled() bool
+
+ // Authenticate does authentication based on given user name and password
+ Authenticate(ctx context.Context, username, password string) (*pb.AuthenticateResponse, error)
+
+ // Recover recovers the state of auth store from the given backend
+ Recover(be AuthBackend)
+
+ // UserAdd adds a new user
+ UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error)
+
+ // UserDelete deletes a user
+ UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error)
+
+ // UserChangePassword changes a password of a user
+ UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error)
+
+ // UserGrantRole grants a role to the user
+ UserGrantRole(r *pb.AuthUserGrantRoleRequest) (*pb.AuthUserGrantRoleResponse, error)
+
+ // UserGet gets the detailed information of a users
+ UserGet(r *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse, error)
+
+ // UserRevokeRole revokes a role of a user
+ UserRevokeRole(r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error)
+
+ // RoleAdd adds a new role
+ RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error)
+
+ // RoleGrantPermission grants a permission to a role
+ RoleGrantPermission(r *pb.AuthRoleGrantPermissionRequest) (*pb.AuthRoleGrantPermissionResponse, error)
+
+ // RoleGet gets the detailed information of a role
+ RoleGet(r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse, error)
+
+ // RoleRevokePermission gets the detailed information of a role
+ RoleRevokePermission(r *pb.AuthRoleRevokePermissionRequest) (*pb.AuthRoleRevokePermissionResponse, error)
+
+ // RoleDelete gets the detailed information of a role
+ RoleDelete(r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error)
+
+ // UserList gets a list of all users
+ UserList(r *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error)
+
+ // RoleList gets a list of all roles
+ RoleList(r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error)
+
+ // IsPutPermitted checks put permission of the user
+ IsPutPermitted(authInfo *AuthInfo, key []byte) error
+
+ // IsRangePermitted checks range permission of the user
+ IsRangePermitted(authInfo *AuthInfo, key, rangeEnd []byte) error
+
+ // IsDeleteRangePermitted checks delete-range permission of the user
+ IsDeleteRangePermitted(authInfo *AuthInfo, key, rangeEnd []byte) error
+
+ // IsAdminPermitted checks admin permission of the user
+ IsAdminPermitted(authInfo *AuthInfo) error
+
+ // GenTokenPrefix produces a random string in a case of simple token
+ // in a case of JWT, it produces an empty string
+ GenTokenPrefix() (string, error)
+
+ // Revision gets current revision of authStore
+ Revision() uint64
+
+ // CheckPassword checks a given pair of username and password is correct
+ CheckPassword(username, password string) (uint64, error)
+
+ // Close does cleanup of AuthStore
+ Close() error
+
+ // AuthInfoFromCtx gets AuthInfo from gRPC's context
+ AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error)
+
+ // AuthInfoFromTLS gets AuthInfo from TLS info of gRPC's context
+ AuthInfoFromTLS(ctx context.Context) *AuthInfo
+
+ // WithRoot generates and installs a token that can be used as a root credential
+ WithRoot(ctx context.Context) context.Context
+
+ // HasRole checks that user has role
+ HasRole(user, role string) bool
+
+ // BcryptCost gets strength of hashing bcrypted auth password
+ BcryptCost() int
+}
+
+type TokenProvider interface {
+ info(ctx context.Context, token string, revision uint64) (*AuthInfo, bool)
+ assign(ctx context.Context, username string, revision uint64) (string, error)
+ enable()
+ disable()
+
+ invalidateUser(string)
+ genTokenPrefix() (string, error)
+}
+
+type AuthBackend interface {
+ CreateAuthBuckets()
+ ForceCommit()
+ ReadTx() AuthReadTx
+ BatchTx() AuthBatchTx
+
+ GetUser(string) *authpb.User
+ GetAllUsers() []*authpb.User
+ GetRole(string) *authpb.Role
+ GetAllRoles() []*authpb.Role
+}
+
+type AuthReadTx interface {
+ RLock()
+ RUnlock()
+ UnsafeAuthReader
+}
+
+type UnsafeAuthReader interface {
+ UnsafeReadAuthEnabled() bool
+ UnsafeReadAuthRevision() uint64
+ UnsafeGetUser(string) *authpb.User
+ UnsafeGetRole(string) *authpb.Role
+ UnsafeGetAllUsers() []*authpb.User
+ UnsafeGetAllRoles() []*authpb.Role
+}
+
+type AuthBatchTx interface {
+ Lock()
+ Unlock()
+ UnsafeAuthReadWriter
+}
+
+type UnsafeAuthReadWriter interface {
+ UnsafeAuthReader
+ UnsafeAuthWriter
+}
+
+type UnsafeAuthWriter interface {
+ UnsafeSaveAuthEnabled(enabled bool)
+ UnsafeSaveAuthRevision(rev uint64)
+ UnsafePutUser(*authpb.User)
+ UnsafeDeleteUser(string)
+ UnsafePutRole(*authpb.Role)
+ UnsafeDeleteRole(string)
+}
+
+type authStore struct {
+ // atomic operations; need 64-bit align, or 32-bit tests will crash
+ revision uint64
+
+ lg *zap.Logger
+ be AuthBackend
+ enabled bool
+ enabledMu sync.RWMutex
+
+ // rangePermCache needs to be protected by rangePermCacheMu
+ // rangePermCacheMu needs to be write locked only in initialization phase or configuration changes
+ // Hot paths like Range(), needs to acquire read lock for improving performance
+ //
+ // Note that BatchTx and ReadTx cannot be a mutex for rangePermCache because they are independent resources
+ // see also: https://github.com/etcd-io/etcd/pull/13920#discussion_r849114855
+ rangePermCache map[string]*unifiedRangePermissions // username -> unifiedRangePermissions
+ rangePermCacheMu sync.RWMutex
+
+ tokenProvider TokenProvider
+ bcryptCost int // the algorithm cost / strength for hashing auth passwords
+}
+
+func (as *authStore) AuthEnable() error {
+ as.enabledMu.Lock()
+ defer as.enabledMu.Unlock()
+ if as.enabled {
+ as.lg.Info("authentication is already enabled; ignored auth enable request")
+ return nil
+ }
+ tx := as.be.BatchTx()
+ tx.Lock()
+ defer func() {
+ tx.Unlock()
+ as.be.ForceCommit()
+ }()
+
+ u := tx.UnsafeGetUser(rootUser)
+ if u == nil {
+ return ErrRootUserNotExist
+ }
+
+ if !hasRootRole(u) {
+ return ErrRootRoleNotExist
+ }
+
+ tx.UnsafeSaveAuthEnabled(true)
+ as.enabled = true
+ as.tokenProvider.enable()
+
+ as.refreshRangePermCache(tx)
+
+ as.setRevision(tx.UnsafeReadAuthRevision())
+
+ as.lg.Info("enabled authentication")
+ return nil
+}
+
+func (as *authStore) AuthDisable() {
+ as.enabledMu.Lock()
+ defer as.enabledMu.Unlock()
+ if !as.enabled {
+ return
+ }
+ b := as.be
+
+ tx := b.BatchTx()
+ tx.Lock()
+ tx.UnsafeSaveAuthEnabled(false)
+ as.commitRevision(tx)
+ tx.Unlock()
+
+ b.ForceCommit()
+
+ as.enabled = false
+ as.tokenProvider.disable()
+
+ as.lg.Info("disabled authentication")
+}
+
+func (as *authStore) Close() error {
+ as.enabledMu.Lock()
+ defer as.enabledMu.Unlock()
+ if !as.enabled {
+ return nil
+ }
+ as.tokenProvider.disable()
+ return nil
+}
+
+func (as *authStore) Authenticate(ctx context.Context, username, password string) (*pb.AuthenticateResponse, error) {
+ if !as.IsAuthEnabled() {
+ return nil, ErrAuthNotEnabled
+ }
+ user := as.be.GetUser(username)
+ if user == nil {
+ return nil, ErrAuthFailed
+ }
+
+ if user.Options != nil && user.Options.NoPassword {
+ return nil, ErrAuthFailed
+ }
+
+ // Password checking is already performed in the API layer, so we don't need to check for now.
+ // Staleness of password can be detected with OCC in the API layer, too.
+
+ token, err := as.tokenProvider.assign(ctx, username, as.Revision())
+ if err != nil {
+ return nil, err
+ }
+
+ as.lg.Debug(
+ "authenticated a user",
+ zap.String("user-name", username),
+ zap.String("token", token),
+ )
+ return &pb.AuthenticateResponse{Token: token}, nil
+}
+
+func (as *authStore) CheckPassword(username, password string) (uint64, error) {
+ if !as.IsAuthEnabled() {
+ return 0, ErrAuthNotEnabled
+ }
+
+ var user *authpb.User
+ // CompareHashAndPassword is very expensive, so we use closures
+ // to avoid putting it in the critical section of the tx lock.
+ revision, err := func() (uint64, error) {
+ tx := as.be.ReadTx()
+ tx.RLock()
+ defer tx.RUnlock()
+
+ user = tx.UnsafeGetUser(username)
+ if user == nil {
+ return 0, ErrAuthFailed
+ }
+
+ if user.Options != nil && user.Options.NoPassword {
+ return 0, ErrNoPasswordUser
+ }
+
+ return tx.UnsafeReadAuthRevision(), nil
+ }()
+ if err != nil {
+ return 0, err
+ }
+
+ if bcrypt.CompareHashAndPassword(user.Password, []byte(password)) != nil {
+ as.lg.Info("invalid password", zap.String("user-name", username))
+ return 0, ErrAuthFailed
+ }
+ return revision, nil
+}
+
+func (as *authStore) Recover(be AuthBackend) {
+ as.be = be
+ tx := be.ReadTx()
+ tx.RLock()
+
+ enabled := tx.UnsafeReadAuthEnabled()
+ as.setRevision(tx.UnsafeReadAuthRevision())
+ as.refreshRangePermCache(tx)
+
+ tx.RUnlock()
+
+ as.enabledMu.Lock()
+ as.enabled = enabled
+ if enabled {
+ as.tokenProvider.enable()
+ }
+ as.enabledMu.Unlock()
+}
+
+func (as *authStore) selectPassword(password string, hashedPassword string) ([]byte, error) {
+ if password != "" && hashedPassword == "" {
+ // This path is for processing log entries created by etcd whose version is older than 3.5
+ return bcrypt.GenerateFromPassword([]byte(password), as.bcryptCost)
+ }
+ return base64.StdEncoding.DecodeString(hashedPassword)
+}
+
+func (as *authStore) UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) {
+ if len(r.Name) == 0 {
+ return nil, ErrUserEmpty
+ }
+
+ tx := as.be.BatchTx()
+ tx.Lock()
+ defer tx.Unlock()
+
+ user := tx.UnsafeGetUser(r.Name)
+ if user != nil {
+ return nil, ErrUserAlreadyExist
+ }
+
+ options := r.Options
+ if options == nil {
+ options = &authpb.UserAddOptions{
+ NoPassword: false,
+ }
+ }
+
+ var password []byte
+ var err error
+
+ if !options.NoPassword {
+ password, err = as.selectPassword(r.Password, r.HashedPassword)
+ if err != nil {
+ return nil, ErrNoPasswordUser
+ }
+ }
+
+ newUser := &authpb.User{
+ Name: []byte(r.Name),
+ Password: password,
+ Options: options,
+ }
+ tx.UnsafePutUser(newUser)
+
+ as.commitRevision(tx)
+ as.refreshRangePermCache(tx)
+
+ as.lg.Info("added a user", zap.String("user-name", r.Name))
+ return &pb.AuthUserAddResponse{}, nil
+}
+
+func (as *authStore) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) {
+ if as.enabled && r.Name == rootUser {
+ as.lg.Error("cannot delete 'root' user", zap.String("user-name", r.Name))
+ return nil, ErrInvalidAuthMgmt
+ }
+
+ tx := as.be.BatchTx()
+ tx.Lock()
+ defer tx.Unlock()
+
+ user := tx.UnsafeGetUser(r.Name)
+ if user == nil {
+ return nil, ErrUserNotFound
+ }
+ tx.UnsafeDeleteUser(r.Name)
+
+ as.commitRevision(tx)
+ as.refreshRangePermCache(tx)
+
+ as.tokenProvider.invalidateUser(r.Name)
+
+ as.lg.Info(
+ "deleted a user",
+ zap.String("user-name", r.Name),
+ zap.Strings("user-roles", user.Roles),
+ )
+ return &pb.AuthUserDeleteResponse{}, nil
+}
+
+func (as *authStore) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) {
+ tx := as.be.BatchTx()
+ tx.Lock()
+ defer tx.Unlock()
+
+ user := tx.UnsafeGetUser(r.Name)
+ if user == nil {
+ return nil, ErrUserNotFound
+ }
+
+ var password []byte
+ var err error
+
+ // Backward compatible with old versions of etcd, user options is nil
+ if user.Options == nil || !user.Options.NoPassword {
+ password, err = as.selectPassword(r.Password, r.HashedPassword)
+ if err != nil {
+ return nil, ErrNoPasswordUser
+ }
+ }
+
+ updatedUser := &authpb.User{
+ Name: []byte(r.Name),
+ Roles: user.Roles,
+ Password: password,
+ Options: user.Options,
+ }
+ tx.UnsafePutUser(updatedUser)
+
+ as.commitRevision(tx)
+ as.refreshRangePermCache(tx)
+
+ as.tokenProvider.invalidateUser(r.Name)
+
+ as.lg.Info(
+ "changed a password of a user",
+ zap.String("user-name", r.Name),
+ zap.Strings("user-roles", user.Roles),
+ )
+ return &pb.AuthUserChangePasswordResponse{}, nil
+}
+
+func (as *authStore) UserGrantRole(r *pb.AuthUserGrantRoleRequest) (*pb.AuthUserGrantRoleResponse, error) {
+ tx := as.be.BatchTx()
+ tx.Lock()
+ defer tx.Unlock()
+
+ user := tx.UnsafeGetUser(r.User)
+ if user == nil {
+ return nil, ErrUserNotFound
+ }
+
+ if r.Role != rootRole {
+ role := tx.UnsafeGetRole(r.Role)
+ if role == nil {
+ return nil, ErrRoleNotFound
+ }
+ }
+
+ idx := sort.SearchStrings(user.Roles, r.Role)
+ if idx < len(user.Roles) && user.Roles[idx] == r.Role {
+ as.lg.Warn(
+ "ignored grant role request to a user",
+ zap.String("user-name", r.User),
+ zap.Strings("user-roles", user.Roles),
+ zap.String("duplicate-role-name", r.Role),
+ )
+ return &pb.AuthUserGrantRoleResponse{}, nil
+ }
+
+ user.Roles = append(user.Roles, r.Role)
+ sort.Strings(user.Roles)
+
+ tx.UnsafePutUser(user)
+
+ as.commitRevision(tx)
+ as.refreshRangePermCache(tx)
+
+ as.lg.Info(
+ "granted a role to a user",
+ zap.String("user-name", r.User),
+ zap.Strings("user-roles", user.Roles),
+ zap.String("added-role-name", r.Role),
+ )
+ return &pb.AuthUserGrantRoleResponse{}, nil
+}
+
+func (as *authStore) UserGet(r *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse, error) {
+ user := as.be.GetUser(r.Name)
+
+ if user == nil {
+ return nil, ErrUserNotFound
+ }
+
+ var resp pb.AuthUserGetResponse
+ resp.Roles = append(resp.Roles, user.Roles...)
+ return &resp, nil
+}
+
+func (as *authStore) UserList(r *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error) {
+ users := as.be.GetAllUsers()
+
+ resp := &pb.AuthUserListResponse{Users: make([]string, len(users))}
+ for i := range users {
+ resp.Users[i] = string(users[i].Name)
+ }
+ return resp, nil
+}
+
+func (as *authStore) UserRevokeRole(r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error) {
+ if as.enabled && r.Name == rootUser && r.Role == rootRole {
+ as.lg.Error(
+ "'root' user cannot revoke 'root' role",
+ zap.String("user-name", r.Name),
+ zap.String("role-name", r.Role),
+ )
+ return nil, ErrInvalidAuthMgmt
+ }
+
+ tx := as.be.BatchTx()
+ tx.Lock()
+ defer tx.Unlock()
+
+ user := tx.UnsafeGetUser(r.Name)
+ if user == nil {
+ return nil, ErrUserNotFound
+ }
+
+ updatedUser := &authpb.User{
+ Name: user.Name,
+ Password: user.Password,
+ Options: user.Options,
+ }
+
+ for _, role := range user.Roles {
+ if role != r.Role {
+ updatedUser.Roles = append(updatedUser.Roles, role)
+ }
+ }
+
+ if len(updatedUser.Roles) == len(user.Roles) {
+ return nil, ErrRoleNotGranted
+ }
+
+ tx.UnsafePutUser(updatedUser)
+
+ as.commitRevision(tx)
+ as.refreshRangePermCache(tx)
+
+ as.lg.Info(
+ "revoked a role from a user",
+ zap.String("user-name", r.Name),
+ zap.Strings("old-user-roles", user.Roles),
+ zap.Strings("new-user-roles", updatedUser.Roles),
+ zap.String("revoked-role-name", r.Role),
+ )
+ return &pb.AuthUserRevokeRoleResponse{}, nil
+}
+
+func (as *authStore) RoleGet(r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse, error) {
+ var resp pb.AuthRoleGetResponse
+
+ role := as.be.GetRole(r.Role)
+ if role == nil {
+ return nil, ErrRoleNotFound
+ }
+ if rootRole == string(role.Name) {
+ resp.Perm = append(resp.Perm, &rootPerm)
+ } else {
+ resp.Perm = append(resp.Perm, role.KeyPermission...)
+ }
+ return &resp, nil
+}
+
+func (as *authStore) RoleList(r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error) {
+ roles := as.be.GetAllRoles()
+
+ resp := &pb.AuthRoleListResponse{Roles: make([]string, len(roles))}
+ for i := range roles {
+ resp.Roles[i] = string(roles[i].Name)
+ }
+ return resp, nil
+}
+
+func (as *authStore) RoleRevokePermission(r *pb.AuthRoleRevokePermissionRequest) (*pb.AuthRoleRevokePermissionResponse, error) {
+ tx := as.be.BatchTx()
+ tx.Lock()
+ defer tx.Unlock()
+
+ role := tx.UnsafeGetRole(r.Role)
+ if role == nil {
+ return nil, ErrRoleNotFound
+ }
+
+ updatedRole := &authpb.Role{
+ Name: role.Name,
+ }
+
+ for _, perm := range role.KeyPermission {
+ if !bytes.Equal(perm.Key, r.Key) || !bytes.Equal(perm.RangeEnd, r.RangeEnd) {
+ updatedRole.KeyPermission = append(updatedRole.KeyPermission, perm)
+ }
+ }
+
+ if len(role.KeyPermission) == len(updatedRole.KeyPermission) {
+ return nil, ErrPermissionNotGranted
+ }
+
+ tx.UnsafePutRole(updatedRole)
+
+ as.commitRevision(tx)
+ as.refreshRangePermCache(tx)
+
+ as.lg.Info(
+ "revoked a permission on range",
+ zap.String("role-name", r.Role),
+ zap.String("key", string(r.Key)),
+ zap.String("range-end", string(r.RangeEnd)),
+ )
+ return &pb.AuthRoleRevokePermissionResponse{}, nil
+}
+
+func (as *authStore) RoleDelete(r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error) {
+ if as.enabled && r.Role == rootRole {
+ as.lg.Error("cannot delete 'root' role", zap.String("role-name", r.Role))
+ return nil, ErrInvalidAuthMgmt
+ }
+
+ tx := as.be.BatchTx()
+ tx.Lock()
+ defer tx.Unlock()
+
+ role := tx.UnsafeGetRole(r.Role)
+ if role == nil {
+ return nil, ErrRoleNotFound
+ }
+
+ tx.UnsafeDeleteRole(r.Role)
+
+ users := tx.UnsafeGetAllUsers()
+ for _, user := range users {
+ updatedUser := &authpb.User{
+ Name: user.Name,
+ Password: user.Password,
+ Options: user.Options,
+ }
+
+ for _, role := range user.Roles {
+ if role != r.Role {
+ updatedUser.Roles = append(updatedUser.Roles, role)
+ }
+ }
+
+ if len(updatedUser.Roles) == len(user.Roles) {
+ continue
+ }
+
+ tx.UnsafePutUser(updatedUser)
+ }
+
+ as.commitRevision(tx)
+ as.refreshRangePermCache(tx)
+
+ as.lg.Info("deleted a role", zap.String("role-name", r.Role))
+ return &pb.AuthRoleDeleteResponse{}, nil
+}
+
+func (as *authStore) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error) {
+ if len(r.Name) == 0 {
+ return nil, ErrRoleEmpty
+ }
+
+ tx := as.be.BatchTx()
+ tx.Lock()
+ defer tx.Unlock()
+
+ role := tx.UnsafeGetRole(r.Name)
+ if role != nil {
+ return nil, ErrRoleAlreadyExist
+ }
+
+ newRole := &authpb.Role{
+ Name: []byte(r.Name),
+ }
+
+ tx.UnsafePutRole(newRole)
+
+ as.commitRevision(tx)
+
+ as.lg.Info("created a role", zap.String("role-name", r.Name))
+ return &pb.AuthRoleAddResponse{}, nil
+}
+
+func (as *authStore) authInfoFromToken(ctx context.Context, token string) (*AuthInfo, bool) {
+ return as.tokenProvider.info(ctx, token, as.Revision())
+}
+
+type permSlice []*authpb.Permission
+
+func (perms permSlice) Len() int {
+ return len(perms)
+}
+
+func (perms permSlice) Less(i, j int) bool {
+ return bytes.Compare(perms[i].Key, perms[j].Key) < 0
+}
+
+func (perms permSlice) Swap(i, j int) {
+ perms[i], perms[j] = perms[j], perms[i]
+}
+
+func (as *authStore) RoleGrantPermission(r *pb.AuthRoleGrantPermissionRequest) (*pb.AuthRoleGrantPermissionResponse, error) {
+ if r.Perm == nil {
+ return nil, ErrPermissionNotGiven
+ }
+ if !isValidPermissionRange(r.Perm.Key, r.Perm.RangeEnd) {
+ return nil, ErrInvalidAuthMgmt
+ }
+
+ tx := as.be.BatchTx()
+ tx.Lock()
+ defer tx.Unlock()
+
+ role := tx.UnsafeGetRole(r.Name)
+ if role == nil {
+ return nil, ErrRoleNotFound
+ }
+
+ idx := sort.Search(len(role.KeyPermission), func(i int) bool {
+ return bytes.Compare(role.KeyPermission[i].Key, r.Perm.Key) >= 0
+ })
+
+ if idx < len(role.KeyPermission) && bytes.Equal(role.KeyPermission[idx].Key, r.Perm.Key) && bytes.Equal(role.KeyPermission[idx].RangeEnd, r.Perm.RangeEnd) {
+ // update existing permission
+ role.KeyPermission[idx].PermType = r.Perm.PermType
+ } else {
+ // append new permission to the role
+ newPerm := &authpb.Permission{
+ Key: r.Perm.Key,
+ RangeEnd: r.Perm.RangeEnd,
+ PermType: r.Perm.PermType,
+ }
+
+ role.KeyPermission = append(role.KeyPermission, newPerm)
+ sort.Sort(permSlice(role.KeyPermission))
+ }
+
+ tx.UnsafePutRole(role)
+
+ as.commitRevision(tx)
+ as.refreshRangePermCache(tx)
+
+ as.lg.Info(
+ "granted/updated a permission to a user",
+ zap.String("user-name", r.Name),
+ zap.String("permission-name", authpb.Permission_Type_name[int32(r.Perm.PermType)]),
+ zap.ByteString("key", r.Perm.Key),
+ zap.ByteString("range-end", r.Perm.RangeEnd),
+ )
+ return &pb.AuthRoleGrantPermissionResponse{}, nil
+}
+
+func (as *authStore) isOpPermitted(userName string, revision uint64, key, rangeEnd []byte, permTyp authpb.Permission_Type) error {
+ // TODO(mitake): this function would be costly so we need a caching mechanism
+ if !as.IsAuthEnabled() {
+ return nil
+ }
+
+ // only gets rev == 0 when passed AuthInfo{}; no user given
+ if revision == 0 {
+ return ErrUserEmpty
+ }
+ rev := as.Revision()
+ if revision < rev {
+ as.lg.Warn("request auth revision is less than current node auth revision",
+ zap.Uint64("current node auth revision", rev),
+ zap.Uint64("request auth revision", revision),
+ zap.ByteString("request key", key),
+ zap.Error(ErrAuthOldRevision))
+ return ErrAuthOldRevision
+ }
+
+ tx := as.be.ReadTx()
+ tx.RLock()
+ defer tx.RUnlock()
+
+ user := tx.UnsafeGetUser(userName)
+ if user == nil {
+ as.lg.Error("cannot find a user for permission check", zap.String("user-name", userName))
+ return ErrPermissionDenied
+ }
+
+ // root role should have permission on all ranges
+ if hasRootRole(user) {
+ return nil
+ }
+
+ if as.isRangeOpPermitted(userName, key, rangeEnd, permTyp) {
+ return nil
+ }
+
+ return ErrPermissionDenied
+}
+
+func (as *authStore) IsPutPermitted(authInfo *AuthInfo, key []byte) error {
+ return as.isOpPermitted(authInfo.Username, authInfo.Revision, key, nil, authpb.WRITE)
+}
+
+func (as *authStore) IsRangePermitted(authInfo *AuthInfo, key, rangeEnd []byte) error {
+ return as.isOpPermitted(authInfo.Username, authInfo.Revision, key, rangeEnd, authpb.READ)
+}
+
+func (as *authStore) IsDeleteRangePermitted(authInfo *AuthInfo, key, rangeEnd []byte) error {
+ return as.isOpPermitted(authInfo.Username, authInfo.Revision, key, rangeEnd, authpb.WRITE)
+}
+
+func (as *authStore) IsAdminPermitted(authInfo *AuthInfo) error {
+ if !as.IsAuthEnabled() {
+ return nil
+ }
+ if authInfo == nil || authInfo.Username == "" {
+ return ErrUserEmpty
+ }
+
+ tx := as.be.ReadTx()
+ tx.RLock()
+ defer tx.RUnlock()
+ u := tx.UnsafeGetUser(authInfo.Username)
+
+ if u == nil {
+ return ErrUserNotFound
+ }
+
+ if !hasRootRole(u) {
+ return ErrPermissionDenied
+ }
+
+ return nil
+}
+
+func (as *authStore) IsAuthEnabled() bool {
+ as.enabledMu.RLock()
+ defer as.enabledMu.RUnlock()
+ return as.enabled
+}
+
+// NewAuthStore creates a new AuthStore.
+func NewAuthStore(lg *zap.Logger, be AuthBackend, tp TokenProvider, bcryptCost int) AuthStore {
+ if lg == nil {
+ lg = zap.NewNop()
+ }
+
+ if bcryptCost < bcrypt.MinCost || bcryptCost > bcrypt.MaxCost {
+ lg.Warn(
+ "use default bcrypt cost instead of the invalid given cost",
+ zap.Int("min-cost", bcrypt.MinCost),
+ zap.Int("max-cost", bcrypt.MaxCost),
+ zap.Int("default-cost", bcrypt.DefaultCost),
+ zap.Int("given-cost", bcryptCost),
+ )
+ bcryptCost = bcrypt.DefaultCost
+ }
+
+ be.CreateAuthBuckets()
+ tx := be.BatchTx()
+ // We should call LockOutsideApply here, but the txPostLockHoos isn't set
+ // to EtcdServer yet, so it's OK.
+ tx.Lock()
+ enabled := tx.UnsafeReadAuthEnabled()
+ as := &authStore{
+ revision: tx.UnsafeReadAuthRevision(),
+ lg: lg,
+ be: be,
+ enabled: enabled,
+ rangePermCache: make(map[string]*unifiedRangePermissions),
+ tokenProvider: tp,
+ bcryptCost: bcryptCost,
+ }
+
+ if enabled {
+ as.tokenProvider.enable()
+ }
+
+ if as.Revision() == 0 {
+ as.commitRevision(tx)
+ }
+
+ as.setupMetricsReporter()
+
+ as.refreshRangePermCache(tx)
+
+ tx.Unlock()
+ be.ForceCommit()
+
+ return as
+}
+
+func hasRootRole(u *authpb.User) bool {
+ // u.Roles is sorted in UserGrantRole(), so we can use binary search.
+ idx := sort.SearchStrings(u.Roles, rootRole)
+ return idx != len(u.Roles) && u.Roles[idx] == rootRole
+}
+
+func (as *authStore) commitRevision(tx UnsafeAuthWriter) {
+ atomic.AddUint64(&as.revision, 1)
+ tx.UnsafeSaveAuthRevision(as.Revision())
+}
+
+func (as *authStore) setRevision(rev uint64) {
+ atomic.StoreUint64(&as.revision, rev)
+}
+
+func (as *authStore) Revision() uint64 {
+ return atomic.LoadUint64(&as.revision)
+}
+
+func (as *authStore) AuthInfoFromTLS(ctx context.Context) (ai *AuthInfo) {
+ peer, ok := peer.FromContext(ctx)
+ if !ok || peer == nil || peer.AuthInfo == nil {
+ return nil
+ }
+
+ tlsInfo := peer.AuthInfo.(credentials.TLSInfo)
+ for _, chains := range tlsInfo.State.VerifiedChains {
+ if len(chains) < 1 {
+ continue
+ }
+ ai = &AuthInfo{
+ Username: chains[0].Subject.CommonName,
+ Revision: as.Revision(),
+ }
+ md, ok := metadata.FromIncomingContext(ctx)
+ if !ok {
+ return nil
+ }
+
+ // gRPC-gateway proxy request to etcd server includes Grpcgateway-Accept
+ // header. The proxy uses etcd client server certificate. If the certificate
+ // has a CommonName we should never use this for authentication.
+ if gw := md["grpcgateway-accept"]; len(gw) > 0 {
+ as.lg.Warn(
+ "ignoring common name in gRPC-gateway proxy request",
+ zap.String("common-name", ai.Username),
+ zap.String("user-name", ai.Username),
+ zap.Uint64("revision", ai.Revision),
+ )
+ return nil
+ }
+ as.lg.Debug(
+ "found command name",
+ zap.String("common-name", ai.Username),
+ zap.String("user-name", ai.Username),
+ zap.Uint64("revision", ai.Revision),
+ )
+ break
+ }
+ return ai
+}
+
+func (as *authStore) AuthInfoFromCtx(ctx context.Context) (*AuthInfo, error) {
+ if !as.IsAuthEnabled() {
+ return nil, nil
+ }
+
+ md, ok := metadata.FromIncomingContext(ctx)
+ if !ok {
+ return nil, nil
+ }
+
+ // TODO(mitake|hexfusion) review unifying key names
+ ts, ok := md[rpctypes.TokenFieldNameGRPC]
+ if !ok {
+ ts, ok = md[rpctypes.TokenFieldNameSwagger]
+ }
+ if !ok {
+ return nil, nil
+ }
+
+ token := ts[0]
+ authInfo, uok := as.authInfoFromToken(ctx, token)
+ if !uok {
+ as.lg.Warn("invalid auth token", zap.String("token", token))
+ return nil, ErrInvalidAuthToken
+ }
+
+ return authInfo, nil
+}
+
+func (as *authStore) GenTokenPrefix() (string, error) {
+ return as.tokenProvider.genTokenPrefix()
+}
+
+func decomposeOpts(lg *zap.Logger, optstr string) (string, map[string]string, error) {
+ opts := strings.Split(optstr, ",")
+ tokenType := opts[0]
+
+ typeSpecificOpts := make(map[string]string)
+ for i := 1; i < len(opts); i++ {
+ pair := strings.Split(opts[i], "=")
+
+ if len(pair) != 2 {
+ if lg != nil {
+ lg.Error("invalid token option", zap.String("option", optstr))
+ }
+ return "", nil, ErrInvalidAuthOpts
+ }
+
+ if _, ok := typeSpecificOpts[pair[0]]; ok {
+ if lg != nil {
+ lg.Error(
+ "invalid token option",
+ zap.String("option", optstr),
+ zap.String("duplicate-parameter", pair[0]),
+ )
+ }
+ return "", nil, ErrInvalidAuthOpts
+ }
+
+ typeSpecificOpts[pair[0]] = pair[1]
+ }
+
+ return tokenType, typeSpecificOpts, nil
+}
+
+// NewTokenProvider creates a new token provider.
+func NewTokenProvider(
+ lg *zap.Logger,
+ tokenOpts string,
+ indexWaiter func(uint64) <-chan struct{},
+ TokenTTL time.Duration,
+) (TokenProvider, error) {
+ tokenType, typeSpecificOpts, err := decomposeOpts(lg, tokenOpts)
+ if err != nil {
+ return nil, ErrInvalidAuthOpts
+ }
+
+ switch tokenType {
+ case tokenTypeSimple:
+ if lg != nil {
+ lg.Warn("simple token is not cryptographically signed")
+ }
+ return newTokenProviderSimple(lg, indexWaiter, TokenTTL), nil
+
+ case tokenTypeJWT:
+ return newTokenProviderJWT(lg, typeSpecificOpts)
+
+ case "":
+ return newTokenProviderNop()
+
+ default:
+ if lg != nil {
+ lg.Warn(
+ "unknown token type",
+ zap.String("type", tokenType),
+ zap.Error(ErrInvalidAuthOpts),
+ )
+ }
+ return nil, ErrInvalidAuthOpts
+ }
+}
+
+func (as *authStore) WithRoot(ctx context.Context) context.Context {
+ if !as.IsAuthEnabled() {
+ return ctx
+ }
+
+ var ctxForAssign context.Context
+ if ts, ok := as.tokenProvider.(*tokenSimple); ok && ts != nil {
+ ctx1 := context.WithValue(ctx, AuthenticateParamIndex{}, uint64(0))
+ prefix, err := ts.genTokenPrefix()
+ if err != nil {
+ as.lg.Error(
+ "failed to generate prefix of internally used token",
+ zap.Error(err),
+ )
+ return ctx
+ }
+ ctxForAssign = context.WithValue(ctx1, AuthenticateParamSimpleTokenPrefix{}, prefix)
+ } else {
+ ctxForAssign = ctx
+ }
+
+ token, err := as.tokenProvider.assign(ctxForAssign, "root", as.Revision())
+ if err != nil {
+ // this must not happen
+ as.lg.Error(
+ "failed to assign token for lease revoking",
+ zap.Error(err),
+ )
+ return ctx
+ }
+
+ mdMap := map[string]string{
+ rpctypes.TokenFieldNameGRPC: token,
+ }
+ tokenMD := metadata.New(mdMap)
+
+ // use "mdIncomingKey{}" since it's called from local etcdserver
+ return metadata.NewIncomingContext(ctx, tokenMD)
+}
+
+func (as *authStore) HasRole(user, role string) bool {
+ tx := as.be.BatchTx()
+ tx.Lock()
+ u := tx.UnsafeGetUser(user)
+ tx.Unlock()
+
+ if u == nil {
+ as.lg.Warn(
+ "'has-role' requested for non-existing user",
+ zap.String("user-name", user),
+ zap.String("role-name", role),
+ )
+ return false
+ }
+
+ for _, r := range u.Roles {
+ if role == r {
+ return true
+ }
+ }
+ return false
+}
+
+func (as *authStore) BcryptCost() int {
+ return as.bcryptCost
+}
+
+func (as *authStore) setupMetricsReporter() {
+ reportCurrentAuthRevMu.Lock()
+ reportCurrentAuthRev = func() float64 {
+ return float64(as.Revision())
+ }
+ reportCurrentAuthRevMu.Unlock()
+}