mirror of https://github.com/minio/minio.git
Support adding service accounts with expiration (#16430)
Co-authored-by: Harshavardhana <harsha@minio.io>
This commit is contained in:
parent
4d7c8e3bb8
commit
4d708cebe9
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
25
cmd/iam.go
25
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
1
go.mod
1
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
|
||||
|
|
3
go.sum
3
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=
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue