Support adding service accounts with expiration (#16430)

Co-authored-by: Harshavardhana <harsha@minio.io>
This commit is contained in:
Praveen raj Mani 2023-02-27 23:40:22 +05:30 committed by GitHub
parent 4d7c8e3bb8
commit 4d708cebe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 134 additions and 40 deletions

View File

@ -653,6 +653,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
accessKey: createReq.AccessKey, accessKey: createReq.AccessKey,
secretKey: createReq.SecretKey, secretKey: createReq.SecretKey,
comment: createReq.Comment, comment: createReq.Comment,
expiration: createReq.Expiration,
claims: make(map[string]interface{}), claims: make(map[string]interface{}),
} }
@ -777,6 +778,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
Credentials: madmin.Credentials{ Credentials: madmin.Credentials{
AccessKey: newCred.AccessKey, AccessKey: newCred.AccessKey,
SecretKey: newCred.SecretKey, SecretKey: newCred.SecretKey,
Expiration: newCred.Expiration,
}, },
} }
@ -809,6 +811,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
Claims: opts.claims, Claims: opts.claims,
SessionPolicy: createReq.Policy, SessionPolicy: createReq.Policy,
Status: auth.AccountOn, Status: auth.AccountOn,
Expiration: createReq.Expiration,
}, },
}, },
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
@ -891,6 +894,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
secretKey: updateReq.NewSecretKey, secretKey: updateReq.NewSecretKey,
status: updateReq.NewStatus, status: updateReq.NewStatus,
comment: updateReq.NewComment, comment: updateReq.NewComment,
expiration: updateReq.NewExpiration,
sessionPolicy: sp, sessionPolicy: sp,
} }
updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts) updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts)
@ -910,6 +914,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
Status: opts.status, Status: opts.status,
Comment: opts.comment, Comment: opts.comment,
SessionPolicy: updateReq.NewPolicy, SessionPolicy: updateReq.NewPolicy,
Expiration: updateReq.NewExpiration,
}, },
}, },
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
@ -988,12 +993,18 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ
return return
} }
var expiration *time.Time
if !svcAccount.Expiration.IsZero() && !svcAccount.Expiration.Equal(timeSentinel) {
expiration = &svcAccount.Expiration
}
infoResp := madmin.InfoServiceAccountResp{ infoResp := madmin.InfoServiceAccountResp{
ParentUser: svcAccount.ParentUser, ParentUser: svcAccount.ParentUser,
Comment: svcAccount.Comment, Comment: svcAccount.Comment,
AccountStatus: svcAccount.Status, AccountStatus: svcAccount.Status,
ImpliedPolicy: policy == nil, ImpliedPolicy: policy == nil,
Policy: string(policyJSON), Policy: string(policyJSON),
Expiration: expiration,
} }
data, err := json.Marshal(infoResp) data, err := json.Marshal(infoResp)
@ -2436,6 +2447,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
secretKey: svcAcctReq.SecretKey, secretKey: svcAcctReq.SecretKey,
status: svcAcctReq.Status, status: svcAcctReq.Status,
comment: svcAcctReq.Comment, comment: svcAcctReq.Comment,
expiration: svcAcctReq.Expiration,
sessionPolicy: sp, sessionPolicy: sp,
} }
_, err = globalIAMSys.UpdateServiceAccount(ctx, svcAcctReq.AccessKey, opts) _, err = globalIAMSys.UpdateServiceAccount(ctx, svcAcctReq.AccessKey, opts)
@ -2451,6 +2463,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
sessionPolicy: sp, sessionPolicy: sp,
claims: svcAcctReq.Claims, claims: svcAcctReq.Claims,
comment: svcAcctReq.Comment, comment: svcAcctReq.Comment,
expiration: svcAcctReq.Expiration,
} }
// In case of LDAP we need to resolve the targetUser to a DN and // In case of LDAP we need to resolve the targetUser to a DN and

View File

@ -253,7 +253,7 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in
return nil, ErrNoAccessKey return nil, ErrNoAccessKey
} }
if token == "" && cred.IsTemp() { if token == "" && cred.IsTemp() && !cred.IsServiceAccount() {
// Temporary credentials should always have x-amz-security-token // Temporary credentials should always have x-amz-security-token
return nil, ErrInvalidToken return nil, ErrInvalidToken
} }
@ -263,7 +263,7 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in
return nil, ErrInvalidToken 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. // validate token for temporary credentials only.
return nil, ErrInvalidToken return nil, ErrInvalidToken
} }

View File

@ -245,6 +245,13 @@ func (ies *IAMEtcdStore) addUser(ctx context.Context, user string, userType IAMU
if u.Credentials.AccessKey == "" { if u.Credentials.AccessKey == "" {
u.Credentials.AccessKey = user 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 m[user] = u
return nil return nil
} }

View File

@ -184,6 +184,14 @@ func (iamOS *IAMObjectStore) loadUser(ctx context.Context, user string, userType
u.Credentials.AccessKey = user 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 m[user] = u
return nil return nil
} }

View File

@ -33,6 +33,7 @@ import (
"github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config/identity/openid" "github.com/minio/minio/internal/config/identity/openid"
"github.com/minio/minio/internal/jwt"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/iam/policy" iampolicy "github.com/minio/pkg/iam/policy"
) )
@ -76,8 +77,13 @@ const (
iamFormatFile = "format.json" iamFormatFile = "format.json"
iamFormatVersion1 = 1 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 { type iamFormat struct {
Version int `json:"version"` Version int `json:"version"`
} }
@ -394,6 +400,19 @@ func (c *iamCache) policyDBGet(mode UsersSysType, name string, isGroup bool) ([]
return policies, mp.UpdatedAt, nil 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 // IAMStorageAPI defines an interface for the IAM persistence layer
type IAMStorageAPI interface { type IAMStorageAPI interface {
// The role of the read-write lock is to prevent go routines from // The role of the read-write lock is to prevent go routines from
@ -1749,17 +1768,18 @@ func (store *IAMStoreSys) DeleteUser(ctx context.Context, accessKey string, user
if userType == regUser { if userType == regUser {
for _, ui := range cache.iamUsersMap { for _, ui := range cache.iamUsersMap {
u := ui.Credentials u := ui.Credentials
if u.IsServiceAccount() && u.ParentUser == accessKey { if u.ParentUser == accessKey {
switch {
case u.IsServiceAccount():
_ = store.deleteUserIdentity(ctx, u.AccessKey, svcUser) _ = store.deleteUserIdentity(ctx, u.AccessKey, svcUser)
delete(cache.iamUsersMap, u.AccessKey) delete(cache.iamUsersMap, u.AccessKey)
} case u.IsTemp():
// Delete any associated STS users.
if u.IsTemp() && u.ParentUser == accessKey {
_ = store.deleteUserIdentity(ctx, u.AccessKey, stsUser) _ = store.deleteUserIdentity(ctx, u.AccessKey, stsUser)
delete(cache.iamUsersMap, u.AccessKey) delete(cache.iamUsersMap, u.AccessKey)
} }
} }
} }
}
// It is ok to ignore deletion error on the mapped policy // It is ok to ignore deletion error on the mapped policy
store.deleteMappedPolicy(ctx, accessKey, userType, false) store.deleteMappedPolicy(ctx, accessKey, userType, false)
@ -2106,8 +2126,9 @@ func (store *IAMStoreSys) SetUserStatus(ctx context.Context, accessKey string, s
return updatedAt, err return updatedAt, err
} }
cache.iamUsersMap[accessKey] = uinfo if err := cache.updateUserWithClaims(accessKey, uinfo); err != nil {
cache.updatedAt = time.Now() return updatedAt, err
}
return uinfo.UpdatedAt, nil return uinfo.UpdatedAt, nil
} }
@ -2142,8 +2163,7 @@ func (store *IAMStoreSys) AddServiceAccount(ctx context.Context, cred auth.Crede
return updatedAt, err return updatedAt, err
} }
cache.iamUsersMap[u.Credentials.AccessKey] = u cache.updateUserWithClaims(u.Credentials.AccessKey, u)
cache.updatedAt = time.Now()
return u.UpdatedAt, nil return u.UpdatedAt, nil
} }
@ -2170,6 +2190,14 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
cr.Comment = opts.comment 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 { switch opts.status {
// The caller did not ask to update status account, do nothing // The caller did not ask to update status account, do nothing
case "": case "":
@ -2229,8 +2257,9 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
return updatedAt, err return updatedAt, err
} }
cache.iamUsersMap[u.Credentials.AccessKey] = u if err := cache.updateUserWithClaims(u.Credentials.AccessKey, u); err != nil {
cache.updatedAt = time.Now() return updatedAt, err
}
return u.UpdatedAt, nil 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 { if err := store.saveUserIdentity(ctx, accessKey, regUser, u); err != nil {
return updatedAt, err return updatedAt, err
} }
if err := cache.updateUserWithClaims(accessKey, u); err != nil {
cache.iamUsersMap[accessKey] = u return updatedAt, err
}
return u.UpdatedAt, nil return u.UpdatedAt, nil
} }
@ -2355,8 +2385,7 @@ func (store *IAMStoreSys) UpdateUserSecretKey(ctx context.Context, accessKey, se
return err return err
} }
cache.iamUsersMap[accessKey] = u return cache.updateUserWithClaims(accessKey, u)
return nil
} }
// GetSTSAndServiceAccounts - returns all STS and Service account credentials. // 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 { if err := store.saveUserIdentity(ctx, cred.AccessKey, userType, ui); err != nil {
return err 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. // 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
}

View File

@ -933,6 +933,7 @@ type newServiceAccountOpts struct {
accessKey string accessKey string
secretKey string secretKey string
comment string comment string
expiration *time.Time
claims map[string]interface{} claims map[string]interface{}
} }
@ -1005,6 +1006,14 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
cred.Status = string(auth.AccountOn) cred.Status = string(auth.AccountOn)
cred.Comment = opts.comment 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) updatedAt, err := sys.store.AddServiceAccount(ctx, cred)
if err != nil { if err != nil {
return auth.Credentials{}, time.Time{}, err return auth.Credentials{}, time.Time{}, err
@ -1019,6 +1028,7 @@ type updateServiceAccountOpts struct {
secretKey string secretKey string
status string status string
comment string comment string
expiration *time.Time
} }
// UpdateServiceAccount - edit a service account // UpdateServiceAccount - edit a service account
@ -1158,13 +1168,10 @@ func (sys *IAMSys) getAccountWithClaims(ctx context.Context, accessKey string) (
return UserIdentity{}, nil, errNoSuchAccount 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 { if err != nil {
return UserIdentity{}, nil, err return UserIdentity{}, nil, err
} }
}
return acc, jwtClaims, nil return acc, jwtClaims, nil
} }
@ -1184,13 +1191,11 @@ func (sys *IAMSys) GetClaimsForSvcAcc(ctx context.Context, accessKey string) (ma
return nil, errNoSuchServiceAccount 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 { if err != nil {
return nil, err return nil, err
} }
}
return jwtClaims.Map(), nil return jwtClaims.Map(), nil
} }

View File

@ -1219,6 +1219,7 @@ func (c *SiteReplicationSys) PeerSvcAccChangeHandler(ctx context.Context, change
sessionPolicy: sp, sessionPolicy: sp,
claims: change.Create.Claims, claims: change.Create.Claims,
comment: change.Create.Comment, comment: change.Create.Comment,
expiration: change.Create.Expiration,
} }
_, _, err = globalIAMSys.NewServiceAccount(ctx, change.Create.Parent, change.Create.Groups, opts) _, _, err = globalIAMSys.NewServiceAccount(ctx, change.Create.Parent, change.Create.Groups, opts)
if err != nil { if err != nil {
@ -1245,6 +1246,7 @@ func (c *SiteReplicationSys) PeerSvcAccChangeHandler(ctx context.Context, change
status: change.Update.Status, status: change.Update.Status,
comment: change.Update.Comment, comment: change.Update.Comment,
sessionPolicy: sp, sessionPolicy: sp,
expiration: change.Update.Expiration,
} }
_, err = globalIAMSys.UpdateServiceAccount(ctx, change.Update.AccessKey, opts) _, err = globalIAMSys.UpdateServiceAccount(ctx, change.Update.AccessKey, opts)
@ -1848,6 +1850,7 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
SessionPolicy: json.RawMessage(policyJSON), SessionPolicy: json.RawMessage(policyJSON),
Status: acc.Credentials.Status, Status: acc.Credentials.Status,
Comment: acc.Credentials.Comment, Comment: acc.Credentials.Comment,
Expiration: &acc.Credentials.Expiration,
}, },
}, },
UpdatedAt: acc.UpdatedAt, UpdatedAt: acc.UpdatedAt,
@ -4716,6 +4719,7 @@ func (c *SiteReplicationSys) healUsers(ctx context.Context, objAPI ObjectLayer,
SessionPolicy: json.RawMessage(policyJSON), SessionPolicy: json.RawMessage(policyJSON),
Status: creds.Status, Status: creds.Status,
Comment: creds.Comment, Comment: creds.Comment,
Expiration: &creds.Expiration,
}, },
}, },
UpdatedAt: lastUpdate, UpdatedAt: lastUpdate,

1
go.mod
View File

@ -144,6 +144,7 @@ require (
github.com/googleapis/gax-go/v2 v2.7.0 // indirect github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/errwrap v1.1.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-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect

3
go.sum
View File

@ -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 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 h1:QsGcniKx5/LuX2eYoeL+Np3UKYPNaN7YKpTh29h8rbw=
github.com/hashicorp/go-hclog v1.1.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= 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.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.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 v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs=

View File

@ -88,6 +88,9 @@ var (
} }
) )
// claim key found in credentials which are service accounts
const iamPolicyClaimNameSA = "sa-policy"
const ( const (
// AccountOn indicates that credentials are enabled // AccountOn indicates that credentials are enabled
AccountOn = "on" AccountOn = "on"
@ -140,7 +143,8 @@ func (cred Credentials) IsTemp() bool {
// IsServiceAccount - returns whether credential is a service account or not // IsServiceAccount - returns whether credential is a service account or not
func (cred Credentials) IsServiceAccount() bool { 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. // IsValid - returns whether credential is valid or not.