diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 3fdcc433fb66e..1d6b85b74134b 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -650,10 +650,11 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque } opts := newServiceAccountOpts{ - accessKey: createReq.AccessKey, - secretKey: createReq.SecretKey, - comment: createReq.Comment, - claims: make(map[string]interface{}), + accessKey: createReq.AccessKey, + secretKey: createReq.SecretKey, + comment: createReq.Comment, + expiration: createReq.Expiration, + claims: make(map[string]interface{}), } // Find the user for the request sender (as it may be sent via a service @@ -775,8 +776,9 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque createResp := madmin.AddServiceAccountResp{ Credentials: madmin.Credentials{ - AccessKey: newCred.AccessKey, - SecretKey: newCred.SecretKey, + AccessKey: newCred.AccessKey, + SecretKey: newCred.SecretKey, + Expiration: newCred.Expiration, }, } @@ -809,6 +811,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque Claims: opts.claims, SessionPolicy: createReq.Policy, Status: auth.AccountOn, + Expiration: createReq.Expiration, }, }, UpdatedAt: updatedAt, @@ -891,6 +894,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re secretKey: updateReq.NewSecretKey, status: updateReq.NewStatus, comment: updateReq.NewComment, + expiration: updateReq.NewExpiration, sessionPolicy: sp, } updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts) @@ -910,6 +914,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re Status: opts.status, Comment: opts.comment, SessionPolicy: updateReq.NewPolicy, + Expiration: updateReq.NewExpiration, }, }, UpdatedAt: updatedAt, @@ -988,12 +993,18 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ return } + var expiration *time.Time + if !svcAccount.Expiration.IsZero() && !svcAccount.Expiration.Equal(timeSentinel) { + expiration = &svcAccount.Expiration + } + infoResp := madmin.InfoServiceAccountResp{ ParentUser: svcAccount.ParentUser, Comment: svcAccount.Comment, AccountStatus: svcAccount.Status, ImpliedPolicy: policy == nil, Policy: string(policyJSON), + Expiration: expiration, } data, err := json.Marshal(infoResp) @@ -2436,6 +2447,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) { secretKey: svcAcctReq.SecretKey, status: svcAcctReq.Status, comment: svcAcctReq.Comment, + expiration: svcAcctReq.Expiration, sessionPolicy: sp, } _, err = globalIAMSys.UpdateServiceAccount(ctx, svcAcctReq.AccessKey, opts) @@ -2451,6 +2463,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) { sessionPolicy: sp, claims: svcAcctReq.Claims, comment: svcAcctReq.Comment, + expiration: svcAcctReq.Expiration, } // In case of LDAP we need to resolve the targetUser to a DN and diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go index 07e5ebc860c27..6d8499fa761e9 100644 --- a/cmd/auth-handler.go +++ b/cmd/auth-handler.go @@ -253,7 +253,7 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in return nil, ErrNoAccessKey } - if token == "" && cred.IsTemp() { + if token == "" && cred.IsTemp() && !cred.IsServiceAccount() { // Temporary credentials should always have x-amz-security-token return nil, ErrInvalidToken } @@ -263,7 +263,7 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in return nil, ErrInvalidToken } - if cred.IsTemp() && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 { + if !cred.IsServiceAccount() && cred.IsTemp() && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 { // validate token for temporary credentials only. return nil, ErrInvalidToken } diff --git a/cmd/iam-etcd-store.go b/cmd/iam-etcd-store.go index 36a8a64e0fd5a..52d4bc56efdf5 100644 --- a/cmd/iam-etcd-store.go +++ b/cmd/iam-etcd-store.go @@ -245,6 +245,13 @@ func (ies *IAMEtcdStore) addUser(ctx context.Context, user string, userType IAMU if u.Credentials.AccessKey == "" { u.Credentials.AccessKey = user } + if u.Credentials.SessionToken != "" { + jwtClaims, err := extractJWTClaims(u) + if err != nil { + return err + } + u.Credentials.Claims = jwtClaims.Map() + } m[user] = u return nil } diff --git a/cmd/iam-object-store.go b/cmd/iam-object-store.go index dcdcdc8c6bcc3..cbcdfefe4c250 100644 --- a/cmd/iam-object-store.go +++ b/cmd/iam-object-store.go @@ -184,6 +184,14 @@ func (iamOS *IAMObjectStore) loadUser(ctx context.Context, user string, userType u.Credentials.AccessKey = user } + if u.Credentials.SessionToken != "" { + jwtClaims, err := extractJWTClaims(u) + if err != nil { + return err + } + u.Credentials.Claims = jwtClaims.Map() + } + m[user] = u return nil } diff --git a/cmd/iam-store.go b/cmd/iam-store.go index a706ea0c076c2..6a816361866d3 100644 --- a/cmd/iam-store.go +++ b/cmd/iam-store.go @@ -33,6 +33,7 @@ import ( "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/config/identity/openid" + "github.com/minio/minio/internal/jwt" "github.com/minio/minio/internal/logger" iampolicy "github.com/minio/pkg/iam/policy" ) @@ -76,8 +77,13 @@ const ( iamFormatFile = "format.json" iamFormatVersion1 = 1 + + minServiceAccountExpiry time.Duration = 15 * time.Minute + maxServiceAccountExpiry time.Duration = 365 * 24 * time.Hour ) +var errInvalidSvcAcctExpiration = errors.New("invalid service account expiration") + type iamFormat struct { Version int `json:"version"` } @@ -394,6 +400,19 @@ func (c *iamCache) policyDBGet(mode UsersSysType, name string, isGroup bool) ([] return policies, mp.UpdatedAt, nil } +func (c *iamCache) updateUserWithClaims(key string, u UserIdentity) error { + if u.Credentials.SessionToken != "" { + jwtClaims, err := extractJWTClaims(u) + if err != nil { + return err + } + u.Credentials.Claims = jwtClaims.Map() + } + c.iamUsersMap[key] = u + c.updatedAt = time.Now() + return nil +} + // IAMStorageAPI defines an interface for the IAM persistence layer type IAMStorageAPI interface { // The role of the read-write lock is to prevent go routines from @@ -1749,14 +1768,15 @@ func (store *IAMStoreSys) DeleteUser(ctx context.Context, accessKey string, user if userType == regUser { for _, ui := range cache.iamUsersMap { u := ui.Credentials - if u.IsServiceAccount() && u.ParentUser == accessKey { - _ = store.deleteUserIdentity(ctx, u.AccessKey, svcUser) - delete(cache.iamUsersMap, u.AccessKey) - } - // Delete any associated STS users. - if u.IsTemp() && u.ParentUser == accessKey { - _ = store.deleteUserIdentity(ctx, u.AccessKey, stsUser) - delete(cache.iamUsersMap, u.AccessKey) + if u.ParentUser == accessKey { + switch { + case u.IsServiceAccount(): + _ = store.deleteUserIdentity(ctx, u.AccessKey, svcUser) + delete(cache.iamUsersMap, u.AccessKey) + case u.IsTemp(): + _ = store.deleteUserIdentity(ctx, u.AccessKey, stsUser) + delete(cache.iamUsersMap, u.AccessKey) + } } } } @@ -2106,8 +2126,9 @@ func (store *IAMStoreSys) SetUserStatus(ctx context.Context, accessKey string, s return updatedAt, err } - cache.iamUsersMap[accessKey] = uinfo - cache.updatedAt = time.Now() + if err := cache.updateUserWithClaims(accessKey, uinfo); err != nil { + return updatedAt, err + } return uinfo.UpdatedAt, nil } @@ -2142,8 +2163,7 @@ func (store *IAMStoreSys) AddServiceAccount(ctx context.Context, cred auth.Crede return updatedAt, err } - cache.iamUsersMap[u.Credentials.AccessKey] = u - cache.updatedAt = time.Now() + cache.updateUserWithClaims(u.Credentials.AccessKey, u) return u.UpdatedAt, nil } @@ -2170,6 +2190,14 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st cr.Comment = opts.comment } + if opts.expiration != nil { + expirationInUTC := opts.expiration.UTC() + if err := validateSvcExpirationInUTC(expirationInUTC); err != nil { + return updatedAt, err + } + cr.Expiration = expirationInUTC + } + switch opts.status { // The caller did not ask to update status account, do nothing case "": @@ -2229,8 +2257,9 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st return updatedAt, err } - cache.iamUsersMap[u.Credentials.AccessKey] = u - cache.updatedAt = time.Now() + if err := cache.updateUserWithClaims(u.Credentials.AccessKey, u); err != nil { + return updatedAt, err + } return u.UpdatedAt, nil } @@ -2331,8 +2360,9 @@ func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, ureq ma if err := store.saveUserIdentity(ctx, accessKey, regUser, u); err != nil { return updatedAt, err } - - cache.iamUsersMap[accessKey] = u + if err := cache.updateUserWithClaims(accessKey, u); err != nil { + return updatedAt, err + } return u.UpdatedAt, nil } @@ -2355,8 +2385,7 @@ func (store *IAMStoreSys) UpdateUserSecretKey(ctx context.Context, accessKey, se return err } - cache.iamUsersMap[accessKey] = u - return nil + return cache.updateUserWithClaims(accessKey, u) } // GetSTSAndServiceAccounts - returns all STS and Service account credentials. @@ -2393,8 +2422,8 @@ func (store *IAMStoreSys) UpdateUserIdentity(ctx context.Context, cred auth.Cred if err := store.saveUserIdentity(ctx, cred.AccessKey, userType, ui); err != nil { return err } - cache.iamUsersMap[cred.AccessKey] = ui - return nil + + return cache.updateUserWithClaims(cred.AccessKey, ui) } // LoadUser - attempts to load user info from storage and updates cache. @@ -2437,3 +2466,25 @@ func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) { } } } + +func extractJWTClaims(u UserIdentity) (*jwt.MapClaims, error) { + jwtClaims, err := auth.ExtractClaims(u.Credentials.SessionToken, u.Credentials.SecretKey) + if err != nil { + // Session tokens for STS creds will be generated with root secret + jwtClaims, err = auth.ExtractClaims(u.Credentials.SessionToken, globalActiveCred.SecretKey) + if err != nil { + return nil, err + } + } + return jwtClaims, nil +} + +func validateSvcExpirationInUTC(expirationInUTC time.Time) error { + currentTime := time.Now().UTC() + minExpiration := currentTime.Add(minServiceAccountExpiry) + maxExpiration := currentTime.Add(maxServiceAccountExpiry) + if expirationInUTC.Before(minExpiration) || expirationInUTC.After(maxExpiration) { + return errInvalidSvcAcctExpiration + } + return nil +} diff --git a/cmd/iam.go b/cmd/iam.go index 1129324659194..3bbba046fbd27 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -933,6 +933,7 @@ type newServiceAccountOpts struct { accessKey string secretKey string comment string + expiration *time.Time claims map[string]interface{} } @@ -1005,6 +1006,14 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro cred.Status = string(auth.AccountOn) cred.Comment = opts.comment + if opts.expiration != nil { + expirationInUTC := opts.expiration.UTC() + if err := validateSvcExpirationInUTC(expirationInUTC); err != nil { + return auth.Credentials{}, time.Time{}, err + } + cred.Expiration = expirationInUTC + } + updatedAt, err := sys.store.AddServiceAccount(ctx, cred) if err != nil { return auth.Credentials{}, time.Time{}, err @@ -1019,6 +1028,7 @@ type updateServiceAccountOpts struct { secretKey string status string comment string + expiration *time.Time } // UpdateServiceAccount - edit a service account @@ -1158,12 +1168,9 @@ func (sys *IAMSys) getAccountWithClaims(ctx context.Context, accessKey string) ( return UserIdentity{}, nil, errNoSuchAccount } - jwtClaims, err := auth.ExtractClaims(acc.Credentials.SessionToken, acc.Credentials.SecretKey) + jwtClaims, err := extractJWTClaims(acc) if err != nil { - jwtClaims, err = auth.ExtractClaims(acc.Credentials.SessionToken, globalActiveCred.SecretKey) - if err != nil { - return UserIdentity{}, nil, err - } + return UserIdentity{}, nil, err } return acc, jwtClaims, nil @@ -1184,13 +1191,11 @@ func (sys *IAMSys) GetClaimsForSvcAcc(ctx context.Context, accessKey string) (ma return nil, errNoSuchServiceAccount } - jwtClaims, err := auth.ExtractClaims(sa.Credentials.SessionToken, sa.Credentials.SecretKey) + jwtClaims, err := extractJWTClaims(sa) if err != nil { - jwtClaims, err = auth.ExtractClaims(sa.Credentials.SessionToken, globalActiveCred.SecretKey) - if err != nil { - return nil, err - } + return nil, err } + return jwtClaims.Map(), nil } diff --git a/cmd/site-replication.go b/cmd/site-replication.go index d3cfbc1b5509c..82d7c677ee407 100644 --- a/cmd/site-replication.go +++ b/cmd/site-replication.go @@ -1219,6 +1219,7 @@ func (c *SiteReplicationSys) PeerSvcAccChangeHandler(ctx context.Context, change sessionPolicy: sp, claims: change.Create.Claims, comment: change.Create.Comment, + expiration: change.Create.Expiration, } _, _, err = globalIAMSys.NewServiceAccount(ctx, change.Create.Parent, change.Create.Groups, opts) if err != nil { @@ -1245,6 +1246,7 @@ func (c *SiteReplicationSys) PeerSvcAccChangeHandler(ctx context.Context, change status: change.Update.Status, comment: change.Update.Comment, sessionPolicy: sp, + expiration: change.Update.Expiration, } _, err = globalIAMSys.UpdateServiceAccount(ctx, change.Update.AccessKey, opts) @@ -1848,6 +1850,7 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error { SessionPolicy: json.RawMessage(policyJSON), Status: acc.Credentials.Status, Comment: acc.Credentials.Comment, + Expiration: &acc.Credentials.Expiration, }, }, UpdatedAt: acc.UpdatedAt, @@ -4716,6 +4719,7 @@ func (c *SiteReplicationSys) healUsers(ctx context.Context, objAPI ObjectLayer, SessionPolicy: json.RawMessage(policyJSON), Status: creds.Status, Comment: creds.Comment, + Expiration: &creds.Expiration, }, }, UpdatedAt: lastUpdate, diff --git a/go.mod b/go.mod index 02955e3a2b15c..a23d9c3ee3125 100644 --- a/go.mod +++ b/go.mod @@ -144,6 +144,7 @@ require ( github.com/googleapis/gax-go/v2 v2.7.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index 4c57ade200961..118e9bfb96ad1 100644 --- a/go.sum +++ b/go.sum @@ -580,8 +580,9 @@ github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v1.1.0 h1:QsGcniKx5/LuX2eYoeL+Np3UKYPNaN7YKpTh29h8rbw= github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= diff --git a/internal/auth/credentials.go b/internal/auth/credentials.go index aeabec58ffe3c..9d4e4653ac2aa 100644 --- a/internal/auth/credentials.go +++ b/internal/auth/credentials.go @@ -88,6 +88,9 @@ var ( } ) +// claim key found in credentials which are service accounts +const iamPolicyClaimNameSA = "sa-policy" + const ( // AccountOn indicates that credentials are enabled AccountOn = "on" @@ -140,7 +143,8 @@ func (cred Credentials) IsTemp() bool { // IsServiceAccount - returns whether credential is a service account or not func (cred Credentials) IsServiceAccount() bool { - return cred.ParentUser != "" && (cred.Expiration.IsZero() || cred.Expiration.Equal(timeSentinel)) + _, ok := cred.Claims[iamPolicyClaimNameSA] + return cred.ParentUser != "" && ok } // IsValid - returns whether credential is valid or not.