Skip to content

Commit

Permalink
Support adding service accounts with expiration (minio#16430)
Browse files Browse the repository at this point in the history
Co-authored-by: Harshavardhana <harsha@minio.io>
  • Loading branch information
Praveenrajmani and harshavardhana authored Feb 27, 2023
1 parent 4d7c8e3 commit 4d708ce
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 40 deletions.
25 changes: 19 additions & 6 deletions cmd/admin-handlers-users.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions cmd/auth-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/iam-etcd-store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
8 changes: 8 additions & 0 deletions cmd/iam-object-store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
91 changes: 71 additions & 20 deletions cmd/iam-store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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 "":
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
25 changes: 15 additions & 10 deletions cmd/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,7 @@ type newServiceAccountOpts struct {
accessKey string
secretKey string
comment string
expiration *time.Time

claims map[string]interface{}
}
Expand Down Expand Up @@ -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
Expand All @@ -1019,6 +1028,7 @@ type updateServiceAccountOpts struct {
secretKey string
status string
comment string
expiration *time.Time
}

// UpdateServiceAccount - edit a service account
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down
Loading

0 comments on commit 4d708ce

Please sign in to comment.