github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/cmd/authn/tok/token.go (about) 1 // Package tok provides AuthN token (structure and methods) 2 // for validation by AIS gateways 3 /* 4 * Copyright (c) 2018-2022, NVIDIA CORPORATION. All rights reserved. 5 */ 6 package tok 7 8 import ( 9 "errors" 10 "fmt" 11 "net/http" 12 "strings" 13 "time" 14 15 "github.com/NVIDIA/aistore/api/apc" 16 "github.com/NVIDIA/aistore/api/authn" 17 "github.com/NVIDIA/aistore/cmn" 18 "github.com/NVIDIA/aistore/cmn/cos" 19 "github.com/golang-jwt/jwt/v4" 20 ) 21 22 type Token struct { 23 UserID string `json:"username"` 24 Expires time.Time `json:"expires"` 25 Token string `json:"token"` 26 ClusterACLs []*authn.CluACL `json:"clusters"` 27 BucketACLs []*authn.BckACL `json:"buckets,omitempty"` 28 IsAdmin bool `json:"admin"` 29 } 30 31 var ( 32 ErrNoPermissions = errors.New("insufficient permissions") 33 ErrInvalidToken = errors.New("invalid token") 34 ErrNoToken = errors.New("token required") 35 ErrNoBearerToken = errors.New("invalid token: no bearer") 36 ErrTokenExpired = errors.New("token expired") 37 ErrTokenRevoked = errors.New("token revoked") 38 ) 39 40 func IssueAdminJWT(expires time.Time, userID, secret string) (string, error) { 41 t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 42 "expires": expires, 43 "username": userID, 44 "admin": true, 45 }) 46 return t.SignedString([]byte(secret)) 47 } 48 49 func IssueJWT(expires time.Time, userID string, bucketACLs []*authn.BckACL, clusterACLs []*authn.CluACL, 50 secret string) (string, error) { 51 t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 52 "expires": expires, 53 "username": userID, 54 "buckets": bucketACLs, 55 "clusters": clusterACLs, 56 }) 57 return t.SignedString([]byte(secret)) 58 } 59 60 // Header format: 'Authorization: Bearer <token>' 61 func ExtractToken(hdr http.Header) (string, error) { 62 s := hdr.Get(apc.HdrAuthorization) 63 if s == "" { 64 return "", ErrNoToken 65 } 66 idx := strings.Index(s, " ") 67 if idx == -1 || s[:idx] != apc.AuthenticationTypeBearer { 68 return "", ErrNoBearerToken 69 } 70 return s[idx+1:], nil 71 } 72 73 func DecryptToken(tokenStr, secret string) (*Token, error) { 74 jwtToken, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) { 75 if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { 76 return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) 77 } 78 return []byte(secret), nil 79 }) 80 if err != nil { 81 return nil, err 82 } 83 claims, ok := jwtToken.Claims.(jwt.MapClaims) 84 if !ok || !jwtToken.Valid { 85 return nil, ErrInvalidToken 86 } 87 tk := &Token{} 88 if err := cos.MorphMarshal(claims, tk); err != nil { 89 return nil, ErrInvalidToken 90 } 91 return tk, nil 92 } 93 94 /////////// 95 // Token // 96 /////////// 97 98 func (tk *Token) String() string { 99 return fmt.Sprintf("user %s, %s", tk.UserID, expiresIn(tk.Expires)) 100 } 101 102 // A user has two-level permissions: cluster-wide and on per bucket basis. 103 // To be able to access data, a user must have either permission. This 104 // allows creating users, e.g, with read-only access to the entire cluster, 105 // and read-write access to a single bucket. 106 // Per-bucket ACL overrides cluster-wide one. 107 // Permissions for a cluster with empty ID are used as default ones when 108 // a user do not have permissions for the given `clusterID`. 109 // 110 // ACL rules are checked in the following order (from highest to the lowest priority): 111 // 1. A user's role is an admin. 112 // 2. User's permissions for the given bucket 113 // 3. User's permissions for the given cluster 114 // 4. User's default cluster permissions (ACL for a cluster with empty clusterID) 115 // 116 // If there are no defined ACL found at any step, any access is denied. 117 func (tk *Token) CheckPermissions(clusterID string, bck *cmn.Bck, perms apc.AccessAttrs) error { 118 if tk.IsAdmin { 119 return nil 120 } 121 if perms == 0 { 122 return errors.New("empty permissions requested") 123 } 124 cluPerms := perms & apc.AccessCluster 125 objPerms := perms &^ apc.AccessCluster 126 cluACL, cluOk := tk.aclForCluster(clusterID) 127 if cluPerms != 0 { 128 // Cluster-wide permissions requested 129 if !cluOk { 130 return ErrNoPermissions 131 } 132 if clusterID == "" { 133 return errors.New("requested cluster permissions without cluster ID") 134 } 135 if !cluACL.Has(cluPerms) { 136 return fmt.Errorf("%v: [cluster %s, %s, granted(%s)]", 137 ErrNoPermissions, clusterID, tk, cluACL.Describe(false /*include all*/)) 138 } 139 } 140 if objPerms == 0 { 141 return nil 142 } 143 144 // Check only bucket specific permissions. 145 if bck == nil { 146 return errors.New("requested bucket permissions without a bucket") 147 } 148 bckACL, bckOk := tk.aclForBucket(clusterID, bck) 149 if bckOk { 150 if bckACL.Has(objPerms) { 151 return nil 152 } 153 return fmt.Errorf("%v: [%s, bucket %s, granted(%s)]", 154 ErrNoPermissions, tk, bck.String(), bckACL.Describe(false /*include all*/)) 155 } 156 if !cluOk || !cluACL.Has(objPerms) { 157 return fmt.Errorf("%v: [%s, granted(%s)]", ErrNoPermissions, tk, cluACL.Describe(false /*include all*/)) 158 } 159 return nil 160 } 161 162 // 163 // private 164 // 165 166 func expiresIn(tm time.Time) string { 167 now := time.Now() 168 if now.After(tm) { 169 return ErrTokenExpired.Error() 170 } 171 // round up 172 d := tm.Sub(now) / time.Second 173 d *= time.Second 174 return "token expires in " + d.String() 175 } 176 177 func (tk *Token) aclForCluster(clusterID string) (perms apc.AccessAttrs, ok bool) { 178 var defaultCluster *authn.CluACL 179 for _, pm := range tk.ClusterACLs { 180 if pm.ID == clusterID { 181 return pm.Access, true 182 } 183 if pm.ID == "" { 184 defaultCluster = pm 185 } 186 } 187 if defaultCluster != nil { 188 return defaultCluster.Access, true 189 } 190 return 0, false 191 } 192 193 func (tk *Token) aclForBucket(clusterID string, bck *cmn.Bck) (perms apc.AccessAttrs, ok bool) { 194 for _, b := range tk.BucketACLs { 195 tbBck := b.Bck 196 if tbBck.Ns.UUID != clusterID { 197 continue 198 } 199 // For AuthN all buckets are external: they have UUIDs of the respective AIS clusters. 200 // To correctly compare with the caller's `bck` we construct tokenBck from the token. 201 tokenBck := cmn.Bck{Name: tbBck.Name, Provider: tbBck.Provider} 202 if tokenBck.Equal(bck) { 203 return b.Access, true 204 } 205 } 206 return 0, false 207 }