From 496f4a7dc7fbbf1aa537d3b80def6353666947f7 Mon Sep 17 00:00:00 2001 From: Anis Elleuch Date: Tue, 17 Mar 2020 18:36:13 +0100 Subject: [PATCH] Add service account type in IAM (#9029) --- cmd/admin-handlers-users.go | 114 ++++++ cmd/admin-router.go | 4 + cmd/api-errors.go | 7 + cmd/common-main.go | 13 + cmd/config-encrypted.go | 39 +- cmd/globals.go | 3 + cmd/iam-etcd-store.go | 111 ++++-- cmd/iam-object-store.go | 104 ++++-- cmd/iam.go | 347 +++++++++++++++--- cmd/peer-rest-server.go | 7 +- cmd/sts-handlers.go | 3 + pkg/auth/credentials.go | 17 +- .../add-service-account-and-policy.go | 52 +++ pkg/madmin/examples/get-service-account.go | 49 +++ pkg/madmin/user-commands.go | 98 +++++ 15 files changed, 815 insertions(+), 153 deletions(-) create mode 100644 pkg/madmin/examples/add-service-account-and-policy.go create mode 100644 pkg/madmin/examples/get-service-account.go diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 9986d6878..04cd76d06 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -378,6 +378,120 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) { } } +// AddServiceAccount - PUT /minio/admin/v2/add-service-account?parentUser= +func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "AddServiceAccount") + + objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.CreateUserAdminAction) + if objectAPI == nil { + return + } + + // Deny if WORM is enabled + if globalWORMEnabled { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL) + return + } + + if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 { + // More than maxConfigSize bytes were available + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL) + return + } + + password := cred.SecretKey + configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) + if err != nil { + logger.LogIf(ctx, err) + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL) + return + } + + var createReq madmin.AddServiceAccountReq + if err = json.Unmarshal(configBytes, &createReq); err != nil { + logger.LogIf(ctx, err) + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL) + return + } + + if createReq.Parent == "" { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL) + return + } + + // Disallow creating service accounts for the root user + if createReq.Parent == globalActiveCred.AccessKey { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddServiceAccountInvalidArgument), r.URL) + return + } + + creds, err := globalIAMSys.NewServiceAccount(ctx, createReq.Parent, createReq.Policy) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Notify all other Minio peers to reload user + for _, nerr := range globalNotificationSys.LoadUser(creds.AccessKey, false) { + if nerr.Err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) + logger.LogIf(ctx, nerr.Err) + } + } + + var createResp = madmin.AddServiceAccountResp{ + Credentials: creds, + } + + data, err := json.Marshal(createResp) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + econfigData, err := madmin.EncryptData(password, data) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, econfigData) +} + +// GetServiceAccount - GET /minio/admin/v2/get-service-account?accessKey= +func (a adminAPIHandlers) GetServiceAccount(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetServiceAccount") + + objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetUserAdminAction) + if objectAPI == nil { + return + } + + vars := mux.Vars(r) + accessKey := vars["accessKey"] + + creds, err := globalIAMSys.GetServiceAccount(ctx, accessKey) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + data, err := json.Marshal(creds) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + password := globalActiveCred.SecretKey + econfigData, err := madmin.EncryptData(password, data) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, econfigData) +} + // InfoCannedPolicy - GET /minio/admin/v2/info-canned-policy?name={policyName} func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "InfoCannedPolicy") diff --git a/cmd/admin-router.go b/cmd/admin-router.go index b2cb97533..59ba25c49 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -110,6 +110,10 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)). Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}") + // Service accounts ops + adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix + "/add-service-account").HandlerFunc(httpTraceHdrs(adminAPI.AddServiceAccount)) + adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/get-service-account").HandlerFunc(httpTraceHdrs(adminAPI.GetServiceAccount)).Queries("accessKey", "{accessKey:.*}") + // Info policy IAM adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}") diff --git a/cmd/api-errors.go b/cmd/api-errors.go index 998b38488..5982efe44 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -332,6 +332,7 @@ const ( ErrAdminProfilerNotEnabled ErrInvalidDecompressedSize ErrAddUserInvalidArgument + ErrAddServiceAccountInvalidArgument ErrPostPolicyConditionInvalidFormat ) @@ -1573,6 +1574,12 @@ var errorCodes = errorCodeMap{ Description: "User is not allowed to be same as admin access key", HTTPStatusCode: http.StatusConflict, }, + ErrAddServiceAccountInvalidArgument: { + Code: "XMinioInvalidArgument", + Description: "New service accounts for admin access key is not allowed", + HTTPStatusCode: http.StatusConflict, + }, + ErrPostPolicyConditionInvalidFormat: { Code: "PostPolicyInvalidKeyName", Description: "Invalid according to Policy: Policy Condition failed", diff --git a/cmd/common-main.go b/cmd/common-main.go index 15545471b..ed69db9ba 100644 --- a/cmd/common-main.go +++ b/cmd/common-main.go @@ -22,6 +22,7 @@ import ( "encoding/gob" "errors" "net" + "os" "path/filepath" "strings" "time" @@ -226,10 +227,22 @@ func handleCommonEnvVars() { globalConfigEncrypted = true } + if env.IsSet(config.EnvAccessKeyOld) && env.IsSet(config.EnvSecretKeyOld) { + oldCred, err := auth.CreateCredentials(env.Get(config.EnvAccessKeyOld, ""), env.Get(config.EnvSecretKeyOld, "")) + if err != nil { + logger.Fatal(config.ErrInvalidCredentials(err), + "Unable to validate the old credentials inherited from the shell environment") + } + globalOldCred = oldCred + os.Unsetenv(config.EnvAccessKeyOld) + os.Unsetenv(config.EnvSecretKeyOld) + } + globalWORMEnabled, err = config.LookupWorm() if err != nil { logger.Fatal(config.ErrInvalidWormValue(err), "Invalid worm configuration") } + } func logStartupMessage(msg string) { diff --git a/cmd/config-encrypted.go b/cmd/config-encrypted.go index 5de7f5d3f..0a6fc77cc 100644 --- a/cmd/config-encrypted.go +++ b/cmd/config-encrypted.go @@ -21,7 +21,6 @@ import ( "context" "errors" "fmt" - "os" "strings" "unicode/utf8" @@ -29,7 +28,6 @@ import ( "github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" - "github.com/minio/minio/pkg/env" "github.com/minio/minio/pkg/madmin" ) @@ -88,11 +86,6 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error { } } - activeCredOld, err := getOldCreds() - if err != nil { - return err - } - doneCh = make(chan struct{}) defer close(doneCh) @@ -106,7 +99,7 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error { select { case <-retryTimerCh: // Migrate IAM configuration - if err = migrateConfigPrefixToEncrypted(objAPI, activeCredOld, encrypted); err != nil { + if err = migrateConfigPrefixToEncrypted(objAPI, globalOldCred, encrypted); err != nil { if err == errDiskNotFound || strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) || strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) { @@ -164,21 +157,6 @@ func decryptData(edata []byte, creds ...auth.Credentials) ([]byte, error) { return data, err } -func getOldCreds() (activeCredOld auth.Credentials, err error) { - accessKeyOld := env.Get(config.EnvAccessKeyOld, "") - secretKeyOld := env.Get(config.EnvSecretKeyOld, "") - if accessKeyOld != "" && secretKeyOld != "" { - activeCredOld, err = auth.CreateCredentials(accessKeyOld, secretKeyOld) - if err != nil { - return activeCredOld, err - } - // Once we have obtained the rotating creds - os.Unsetenv(config.EnvAccessKeyOld) - os.Unsetenv(config.EnvSecretKeyOld) - } - return activeCredOld, nil -} - func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error { ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) defer cancel() @@ -206,20 +184,15 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error { } } - activeCredOld, err := getOldCreds() - if err != nil { - return err - } - if encrypted { // No key rotation requested, and backend is // already encrypted. We proceed without migration. - if !activeCredOld.IsValid() { + if !globalOldCred.IsValid() { return nil } // No real reason to rotate if old and new creds are same. - if activeCredOld.Equal(globalActiveCred) { + if globalOldCred.Equal(globalActiveCred) { return nil } @@ -254,8 +227,8 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error { var data []byte // Is rotating of creds requested? - if activeCredOld.IsValid() { - data, err = decryptData(cdata, activeCredOld, globalActiveCred) + if globalOldCred.IsValid() { + data, err = decryptData(cdata, globalOldCred, globalActiveCred) if err != nil { if err == madmin.ErrMaliciousData { return config.ErrInvalidRotatingCredentialsBackendEncrypted(nil) @@ -285,7 +258,7 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error { } } - if encrypted && globalActiveCred.IsValid() && activeCredOld.IsValid() { + if encrypted && globalActiveCred.IsValid() && globalOldCred.IsValid() { logger.Info("Rotation complete, please make sure to unset MINIO_ACCESS_KEY_OLD and MINIO_SECRET_KEY_OLD envs") } diff --git a/cmd/globals.go b/cmd/globals.go index baa06ae19..0e8015c34 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -193,6 +193,9 @@ var ( globalActiveCred auth.Credentials + // Hold the old server credentials passed by the environment + globalOldCred auth.Credentials + // Indicates if config is to be encrypted globalConfigEncrypted bool diff --git a/cmd/iam-etcd-store.go b/cmd/iam-etcd-store.go index d133b6270..d703e7c54 100644 --- a/cmd/iam-etcd-store.go +++ b/cmd/iam-etcd-store.go @@ -28,6 +28,7 @@ import ( etcd "github.com/coreos/etcd/clientv3" "github.com/coreos/etcd/mvcc/mvccpb" + jwtgo "github.com/dgrijalva/jwt-go" "github.com/minio/minio-go/v6/pkg/set" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" @@ -174,7 +175,11 @@ func (ies *IAMEtcdStore) migrateUsersConfigToV1(isSTS bool) error { // 2. copy policy to new loc. mp := newMappedPolicy(policyName) - path := getMappedPolicyPath(user, isSTS, false) + userType := regularUser + if isSTS { + userType = stsUser + } + path := getMappedPolicyPath(user, userType, false) if err := ies.saveIAMConfig(mp, path); err != nil { return err } @@ -300,9 +305,9 @@ func (ies *IAMEtcdStore) loadPolicyDocs(m map[string]iampolicy.Policy) error { return nil } -func (ies *IAMEtcdStore) loadUser(user string, isSTS bool, m map[string]auth.Credentials) error { +func (ies *IAMEtcdStore) loadUser(user string, userType IAMUserType, m map[string]auth.Credentials) error { var u UserIdentity - err := ies.loadIAMConfig(&u, getUserIdentityPath(user, isSTS)) + err := ies.loadIAMConfig(&u, getUserIdentityPath(user, userType)) if err != nil { if err == errConfigNotFound { return errNoSuchUser @@ -313,11 +318,31 @@ func (ies *IAMEtcdStore) loadUser(user string, isSTS bool, m map[string]auth.Cre if u.Credentials.IsExpired() { // Delete expired identity. ctx := ies.getContext() - deleteKeyEtcd(ctx, ies.client, getUserIdentityPath(user, isSTS)) - deleteKeyEtcd(ctx, ies.client, getMappedPolicyPath(user, isSTS, false)) + deleteKeyEtcd(ctx, ies.client, getUserIdentityPath(user, userType)) + deleteKeyEtcd(ctx, ies.client, getMappedPolicyPath(user, userType, false)) return nil } + // If this is a service account, rotate the session key if we are changing the server creds + if globalOldCred.IsValid() && u.Credentials.IsServiceAccount() { + if !globalOldCred.Equal(globalActiveCred) { + m := jwtgo.MapClaims{} + stsTokenCallback := func(t *jwtgo.Token) (interface{}, error) { + return []byte(globalOldCred.SecretKey), nil + } + if _, err := jwtgo.ParseWithClaims(u.Credentials.SessionToken, m, stsTokenCallback); err == nil { + jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims(m)) + if token, err := jwt.SignedString([]byte(globalActiveCred.SecretKey)); err == nil { + u.Credentials.SessionToken = token + err := ies.saveIAMConfig(&u, getUserIdentityPath(user, userType)) + if err != nil { + return err + } + } + } + } + } + if u.Credentials.AccessKey == "" { u.Credentials.AccessKey = user } @@ -326,10 +351,15 @@ func (ies *IAMEtcdStore) loadUser(user string, isSTS bool, m map[string]auth.Cre } -func (ies *IAMEtcdStore) loadUsers(isSTS bool, m map[string]auth.Credentials) error { - basePrefix := iamConfigUsersPrefix - if isSTS { +func (ies *IAMEtcdStore) loadUsers(userType IAMUserType, m map[string]auth.Credentials) error { + var basePrefix string + switch userType { + case srvAccUser: + basePrefix = iamConfigServiceAccountsPrefix + case stsUser: basePrefix = iamConfigSTSPrefix + default: + basePrefix = iamConfigUsersPrefix } ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) @@ -345,7 +375,7 @@ func (ies *IAMEtcdStore) loadUsers(isSTS bool, m map[string]auth.Credentials) er // Reload config for all users. for _, user := range users.ToSlice() { - if err = ies.loadUser(user, isSTS, m); err != nil { + if err = ies.loadUser(user, userType, m); err != nil { return err } } @@ -388,9 +418,9 @@ func (ies *IAMEtcdStore) loadGroups(m map[string]GroupInfo) error { } -func (ies *IAMEtcdStore) loadMappedPolicy(name string, isSTS, isGroup bool, m map[string]MappedPolicy) error { +func (ies *IAMEtcdStore) loadMappedPolicy(name string, userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error { var p MappedPolicy - err := ies.loadIAMConfig(&p, getMappedPolicyPath(name, isSTS, isGroup)) + err := ies.loadIAMConfig(&p, getMappedPolicyPath(name, userType, isGroup)) if err != nil { if err == errConfigNotFound { return errNoSuchPolicy @@ -402,19 +432,23 @@ func (ies *IAMEtcdStore) loadMappedPolicy(name string, isSTS, isGroup bool, m ma } -func (ies *IAMEtcdStore) loadMappedPolicies(isSTS, isGroup bool, m map[string]MappedPolicy) error { +func (ies *IAMEtcdStore) loadMappedPolicies(userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error { ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) defer cancel() ies.setContext(ctx) defer ies.clearContext() var basePrefix string - switch { - case isSTS: - basePrefix = iamConfigPolicyDBSTSUsersPrefix - case isGroup: + if isGroup { basePrefix = iamConfigPolicyDBGroupsPrefix - default: - basePrefix = iamConfigPolicyDBUsersPrefix + } else { + switch userType { + case srvAccUser: + basePrefix = iamConfigPolicyDBServiceAccountsPrefix + case stsUser: + basePrefix = iamConfigPolicyDBSTSUsersPrefix + default: + basePrefix = iamConfigPolicyDBUsersPrefix + } } r, err := ies.client.Get(ctx, basePrefix, etcd.WithPrefix(), etcd.WithKeysOnly()) if err != nil { @@ -425,7 +459,7 @@ func (ies *IAMEtcdStore) loadMappedPolicies(isSTS, isGroup bool, m map[string]Ma // Reload config and policies for all users. for _, user := range users.ToSlice() { - if err = ies.loadMappedPolicy(user, isSTS, isGroup, m); err != nil { + if err = ies.loadMappedPolicy(user, userType, isGroup, m); err != nil { return err } } @@ -452,29 +486,32 @@ func (ies *IAMEtcdStore) loadAll(sys *IAMSys, objectAPI ObjectLayer) error { } // load STS temp users - if err := ies.loadUsers(true, iamUsersMap); err != nil { + if err := ies.loadUsers(stsUser, iamUsersMap); err != nil { return err } if isMinIOUsersSys { // load long term users - if err := ies.loadUsers(false, iamUsersMap); err != nil { + if err := ies.loadUsers(regularUser, iamUsersMap); err != nil { + return err + } + if err := ies.loadUsers(srvAccUser, iamUsersMap); err != nil { return err } if err := ies.loadGroups(iamGroupsMap); err != nil { return err } - if err := ies.loadMappedPolicies(false, false, iamUserPolicyMap); err != nil { + if err := ies.loadMappedPolicies(regularUser, false, iamUserPolicyMap); err != nil { return err } } // load STS policy mappings into the same map - if err := ies.loadMappedPolicies(true, false, iamUserPolicyMap); err != nil { + if err := ies.loadMappedPolicies(stsUser, false, iamUserPolicyMap); err != nil { return err } // load policies mapped to groups - if err := ies.loadMappedPolicies(false, true, iamGroupPolicyMap); err != nil { + if err := ies.loadMappedPolicies(regularUser, true, iamGroupPolicyMap); err != nil { return err } @@ -498,12 +535,12 @@ func (ies *IAMEtcdStore) savePolicyDoc(policyName string, p iampolicy.Policy) er return ies.saveIAMConfig(&p, getPolicyDocPath(policyName)) } -func (ies *IAMEtcdStore) saveMappedPolicy(name string, isSTS, isGroup bool, mp MappedPolicy) error { - return ies.saveIAMConfig(mp, getMappedPolicyPath(name, isSTS, isGroup)) +func (ies *IAMEtcdStore) saveMappedPolicy(name string, userType IAMUserType, isGroup bool, mp MappedPolicy) error { + return ies.saveIAMConfig(mp, getMappedPolicyPath(name, userType, isGroup)) } -func (ies *IAMEtcdStore) saveUserIdentity(name string, isSTS bool, u UserIdentity) error { - return ies.saveIAMConfig(u, getUserIdentityPath(name, isSTS)) +func (ies *IAMEtcdStore) saveUserIdentity(name string, userType IAMUserType, u UserIdentity) error { + return ies.saveIAMConfig(u, getUserIdentityPath(name, userType)) } func (ies *IAMEtcdStore) saveGroupInfo(name string, gi GroupInfo) error { @@ -518,16 +555,16 @@ func (ies *IAMEtcdStore) deletePolicyDoc(name string) error { return err } -func (ies *IAMEtcdStore) deleteMappedPolicy(name string, isSTS, isGroup bool) error { - err := ies.deleteIAMConfig(getMappedPolicyPath(name, isSTS, isGroup)) +func (ies *IAMEtcdStore) deleteMappedPolicy(name string, userType IAMUserType, isGroup bool) error { + err := ies.deleteIAMConfig(getMappedPolicyPath(name, userType, isGroup)) if err == errConfigNotFound { err = errNoSuchPolicy } return err } -func (ies *IAMEtcdStore) deleteUserIdentity(name string, isSTS bool) error { - err := ies.deleteIAMConfig(getUserIdentityPath(name, isSTS)) +func (ies *IAMEtcdStore) deleteUserIdentity(name string, userType IAMUserType) error { + err := ies.deleteIAMConfig(getUserIdentityPath(name, userType)) if err == errConfigNotFound { err = errNoSuchUser } @@ -599,11 +636,11 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) { case usersPrefix: accessKey := path.Dir(strings.TrimPrefix(string(event.Kv.Key), iamConfigUsersPrefix)) - ies.loadUser(accessKey, false, sys.iamUsersMap) + ies.loadUser(accessKey, regularUser, sys.iamUsersMap) case stsPrefix: accessKey := path.Dir(strings.TrimPrefix(string(event.Kv.Key), iamConfigSTSPrefix)) - ies.loadUser(accessKey, true, sys.iamUsersMap) + ies.loadUser(accessKey, stsUser, sys.iamUsersMap) case groupsPrefix: group := path.Dir(strings.TrimPrefix(string(event.Kv.Key), iamConfigGroupsPrefix)) @@ -619,17 +656,17 @@ func (ies *IAMEtcdStore) reloadFromEvent(sys *IAMSys, event *etcd.Event) { policyMapFile := strings.TrimPrefix(string(event.Kv.Key), iamConfigPolicyDBUsersPrefix) user := strings.TrimSuffix(policyMapFile, ".json") - ies.loadMappedPolicy(user, false, false, sys.iamUserPolicyMap) + ies.loadMappedPolicy(user, regularUser, false, sys.iamUserPolicyMap) case policyDBSTSUsersPrefix: policyMapFile := strings.TrimPrefix(string(event.Kv.Key), iamConfigPolicyDBSTSUsersPrefix) user := strings.TrimSuffix(policyMapFile, ".json") - ies.loadMappedPolicy(user, true, false, sys.iamUserPolicyMap) + ies.loadMappedPolicy(user, stsUser, false, sys.iamUserPolicyMap) case policyDBGroupsPrefix: policyMapFile := strings.TrimPrefix(string(event.Kv.Key), iamConfigPolicyDBGroupsPrefix) user := strings.TrimSuffix(policyMapFile, ".json") - ies.loadMappedPolicy(user, false, true, sys.iamGroupPolicyMap) + ies.loadMappedPolicy(user, regularUser, true, sys.iamGroupPolicyMap) } case eventDelete: switch { diff --git a/cmd/iam-object-store.go b/cmd/iam-object-store.go index 1d3953f35..3411589c9 100644 --- a/cmd/iam-object-store.go +++ b/cmd/iam-object-store.go @@ -25,6 +25,8 @@ import ( "sync" "time" + jwtgo "github.com/dgrijalva/jwt-go" + "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" iampolicy "github.com/minio/minio/pkg/iam/policy" @@ -116,7 +118,11 @@ func (iamOS *IAMObjectStore) migrateUsersConfigToV1(isSTS bool) error { // 2. copy policy file to new location. mp := newMappedPolicy(policyName) - if err := iamOS.saveMappedPolicy(user, isSTS, false, mp); err != nil { + userType := regularUser + if isSTS { + userType = stsUser + } + if err := iamOS.saveMappedPolicy(user, userType, false, mp); err != nil { return err } @@ -289,14 +295,14 @@ func (iamOS *IAMObjectStore) loadPolicyDocs(m map[string]iampolicy.Policy) error return nil } -func (iamOS *IAMObjectStore) loadUser(user string, isSTS bool, m map[string]auth.Credentials) error { +func (iamOS *IAMObjectStore) loadUser(user string, userType IAMUserType, m map[string]auth.Credentials) error { objectAPI := iamOS.getObjectAPI() if objectAPI == nil { return errServerNotInitialized } var u UserIdentity - err := iamOS.loadIAMConfig(&u, getUserIdentityPath(user, isSTS)) + err := iamOS.loadIAMConfig(&u, getUserIdentityPath(user, userType)) if err != nil { if err == errConfigNotFound { return errNoSuchUser @@ -306,11 +312,31 @@ func (iamOS *IAMObjectStore) loadUser(user string, isSTS bool, m map[string]auth if u.Credentials.IsExpired() { // Delete expired identity - ignoring errors here. - iamOS.deleteIAMConfig(getUserIdentityPath(user, isSTS)) - iamOS.deleteIAMConfig(getMappedPolicyPath(user, isSTS, false)) + iamOS.deleteIAMConfig(getUserIdentityPath(user, userType)) + iamOS.deleteIAMConfig(getMappedPolicyPath(user, userType, false)) return nil } + // If this is a service account, rotate the session key if needed + if globalOldCred.IsValid() && u.Credentials.IsServiceAccount() { + if !globalOldCred.Equal(globalActiveCred) { + m := jwtgo.MapClaims{} + stsTokenCallback := func(t *jwtgo.Token) (interface{}, error) { + return []byte(globalOldCred.SecretKey), nil + } + if _, err := jwtgo.ParseWithClaims(u.Credentials.SessionToken, m, stsTokenCallback); err == nil { + jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims(m)) + if token, err := jwt.SignedString([]byte(globalActiveCred.SecretKey)); err == nil { + u.Credentials.SessionToken = token + err := iamOS.saveIAMConfig(&u, getUserIdentityPath(user, userType)) + if err != nil { + return err + } + } + } + } + } + if u.Credentials.AccessKey == "" { u.Credentials.AccessKey = user } @@ -318,7 +344,7 @@ func (iamOS *IAMObjectStore) loadUser(user string, isSTS bool, m map[string]auth return nil } -func (iamOS *IAMObjectStore) loadUsers(isSTS bool, m map[string]auth.Credentials) error { +func (iamOS *IAMObjectStore) loadUsers(userType IAMUserType, m map[string]auth.Credentials) error { objectAPI := iamOS.getObjectAPI() if objectAPI == nil { return errServerNotInitialized @@ -326,17 +352,24 @@ func (iamOS *IAMObjectStore) loadUsers(isSTS bool, m map[string]auth.Credentials doneCh := make(chan struct{}) defer close(doneCh) - basePrefix := iamConfigUsersPrefix - if isSTS { + + var basePrefix string + switch userType { + case srvAccUser: + basePrefix = iamConfigServiceAccountsPrefix + case stsUser: basePrefix = iamConfigSTSPrefix + default: + basePrefix = iamConfigUsersPrefix } + for item := range listIAMConfigItems(objectAPI, basePrefix, true, doneCh) { if item.Err != nil { return item.Err } userName := item.Item - err := iamOS.loadUser(userName, isSTS, m) + err := iamOS.loadUser(userName, userType, m) if err != nil { return err } @@ -384,7 +417,7 @@ func (iamOS *IAMObjectStore) loadGroups(m map[string]GroupInfo) error { return nil } -func (iamOS *IAMObjectStore) loadMappedPolicy(name string, isSTS, isGroup bool, +func (iamOS *IAMObjectStore) loadMappedPolicy(name string, userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error { objectAPI := iamOS.getObjectAPI() @@ -393,7 +426,7 @@ func (iamOS *IAMObjectStore) loadMappedPolicy(name string, isSTS, isGroup bool, } var p MappedPolicy - err := iamOS.loadIAMConfig(&p, getMappedPolicyPath(name, isSTS, isGroup)) + err := iamOS.loadIAMConfig(&p, getMappedPolicyPath(name, userType, isGroup)) if err != nil { if err == errConfigNotFound { return errNoSuchPolicy @@ -404,7 +437,7 @@ func (iamOS *IAMObjectStore) loadMappedPolicy(name string, isSTS, isGroup bool, return nil } -func (iamOS *IAMObjectStore) loadMappedPolicies(isSTS, isGroup bool, m map[string]MappedPolicy) error { +func (iamOS *IAMObjectStore) loadMappedPolicies(userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error { objectAPI := iamOS.getObjectAPI() if objectAPI == nil { return errServerNotInitialized @@ -413,13 +446,17 @@ func (iamOS *IAMObjectStore) loadMappedPolicies(isSTS, isGroup bool, m map[strin doneCh := make(chan struct{}) defer close(doneCh) var basePath string - switch { - case isSTS: - basePath = iamConfigPolicyDBSTSUsersPrefix - case isGroup: + if isGroup { basePath = iamConfigPolicyDBGroupsPrefix - default: - basePath = iamConfigPolicyDBUsersPrefix + } else { + switch userType { + case srvAccUser: + basePath = iamConfigPolicyDBServiceAccountsPrefix + case stsUser: + basePath = iamConfigPolicyDBSTSUsersPrefix + default: + basePath = iamConfigPolicyDBUsersPrefix + } } for item := range listIAMConfigItems(objectAPI, basePath, false, doneCh) { if item.Err != nil { @@ -428,7 +465,7 @@ func (iamOS *IAMObjectStore) loadMappedPolicies(isSTS, isGroup bool, m map[strin policyFile := item.Item userOrGroupName := strings.TrimSuffix(policyFile, ".json") - err := iamOS.loadMappedPolicy(userOrGroupName, isSTS, isGroup, m) + err := iamOS.loadMappedPolicy(userOrGroupName, userType, isGroup, m) if err != nil { return err } @@ -466,27 +503,30 @@ func (iamOS *IAMObjectStore) loadAll(sys *IAMSys, objectAPI ObjectLayer) error { return err } // load STS temp users - if err := iamOS.loadUsers(true, iamUsersMap); err != nil { + if err := iamOS.loadUsers(stsUser, iamUsersMap); err != nil { return err } if isMinIOUsersSys { - if err := iamOS.loadUsers(false, iamUsersMap); err != nil { + if err := iamOS.loadUsers(regularUser, iamUsersMap); err != nil { + return err + } + if err := iamOS.loadUsers(srvAccUser, iamUsersMap); err != nil { return err } if err := iamOS.loadGroups(iamGroupsMap); err != nil { return err } - if err := iamOS.loadMappedPolicies(false, false, iamUserPolicyMap); err != nil { + if err := iamOS.loadMappedPolicies(regularUser, false, iamUserPolicyMap); err != nil { return err } } // load STS policy mappings - if err := iamOS.loadMappedPolicies(true, false, iamUserPolicyMap); err != nil { + if err := iamOS.loadMappedPolicies(stsUser, false, iamUserPolicyMap); err != nil { return err } // load policies mapped to groups - if err := iamOS.loadMappedPolicies(false, true, iamGroupPolicyMap); err != nil { + if err := iamOS.loadMappedPolicies(regularUser, true, iamGroupPolicyMap); err != nil { return err } @@ -510,12 +550,12 @@ func (iamOS *IAMObjectStore) savePolicyDoc(policyName string, p iampolicy.Policy return iamOS.saveIAMConfig(&p, getPolicyDocPath(policyName)) } -func (iamOS *IAMObjectStore) saveMappedPolicy(name string, isSTS, isGroup bool, mp MappedPolicy) error { - return iamOS.saveIAMConfig(mp, getMappedPolicyPath(name, isSTS, isGroup)) +func (iamOS *IAMObjectStore) saveMappedPolicy(name string, userType IAMUserType, isGroup bool, mp MappedPolicy) error { + return iamOS.saveIAMConfig(mp, getMappedPolicyPath(name, userType, isGroup)) } -func (iamOS *IAMObjectStore) saveUserIdentity(name string, isSTS bool, u UserIdentity) error { - return iamOS.saveIAMConfig(u, getUserIdentityPath(name, isSTS)) +func (iamOS *IAMObjectStore) saveUserIdentity(name string, userType IAMUserType, u UserIdentity) error { + return iamOS.saveIAMConfig(u, getUserIdentityPath(name, userType)) } func (iamOS *IAMObjectStore) saveGroupInfo(name string, gi GroupInfo) error { @@ -530,16 +570,16 @@ func (iamOS *IAMObjectStore) deletePolicyDoc(name string) error { return err } -func (iamOS *IAMObjectStore) deleteMappedPolicy(name string, isSTS, isGroup bool) error { - err := iamOS.deleteIAMConfig(getMappedPolicyPath(name, isSTS, isGroup)) +func (iamOS *IAMObjectStore) deleteMappedPolicy(name string, userType IAMUserType, isGroup bool) error { + err := iamOS.deleteIAMConfig(getMappedPolicyPath(name, userType, isGroup)) if err == errConfigNotFound { err = errNoSuchPolicy } return err } -func (iamOS *IAMObjectStore) deleteUserIdentity(name string, isSTS bool) error { - err := iamOS.deleteIAMConfig(getUserIdentityPath(name, isSTS)) +func (iamOS *IAMObjectStore) deleteUserIdentity(name string, userType IAMUserType) error { + err := iamOS.deleteIAMConfig(getUserIdentityPath(name, userType)) if err == errConfigNotFound { err = errNoSuchUser } diff --git a/cmd/iam.go b/cmd/iam.go index 47b50eb41..ff2a2a24a 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -19,7 +19,9 @@ package cmd import ( "bytes" "context" + "encoding/base64" "encoding/json" + "fmt" "sync" "github.com/minio/minio-go/v6/pkg/set" @@ -52,6 +54,9 @@ const ( // IAM users directory. iamConfigUsersPrefix = iamConfigPrefix + "/users/" + // IAM service accounts directory. + iamConfigServiceAccountsPrefix = iamConfigPrefix + "/service-accounts/" + // IAM groups directory. iamConfigGroupsPrefix = iamConfigPrefix + "/groups/" @@ -62,10 +67,11 @@ const ( iamConfigSTSPrefix = iamConfigPrefix + "/sts/" // IAM Policy DB prefixes. - iamConfigPolicyDBPrefix = iamConfigPrefix + "/policydb/" - iamConfigPolicyDBUsersPrefix = iamConfigPolicyDBPrefix + "users/" - iamConfigPolicyDBSTSUsersPrefix = iamConfigPolicyDBPrefix + "sts-users/" - iamConfigPolicyDBGroupsPrefix = iamConfigPolicyDBPrefix + "groups/" + iamConfigPolicyDBPrefix = iamConfigPrefix + "/policydb/" + iamConfigPolicyDBUsersPrefix = iamConfigPolicyDBPrefix + "users/" + iamConfigPolicyDBSTSUsersPrefix = iamConfigPolicyDBPrefix + "sts-users/" + iamConfigPolicyDBServiceAccountsPrefix = iamConfigPolicyDBPrefix + "service-accounts/" + iamConfigPolicyDBGroupsPrefix = iamConfigPolicyDBPrefix + "groups/" // IAM identity file which captures identity credentials. iamIdentityFile = "identity.json" @@ -99,10 +105,15 @@ func getIAMFormatFilePath() string { return iamConfigPrefix + SlashSeparator + iamFormatFile } -func getUserIdentityPath(user string, isSTS bool) string { - basePath := iamConfigUsersPrefix - if isSTS { +func getUserIdentityPath(user string, userType IAMUserType) string { + var basePath string + switch userType { + case srvAccUser: + basePath = iamConfigServiceAccountsPrefix + case stsUser: basePath = iamConfigSTSPrefix + default: + basePath = iamConfigUsersPrefix } return pathJoin(basePath, user, iamIdentityFile) } @@ -115,12 +126,15 @@ func getPolicyDocPath(name string) string { return pathJoin(iamConfigPoliciesPrefix, name, iamPolicyFile) } -func getMappedPolicyPath(name string, isSTS, isGroup bool) string { - switch { - case isSTS: - return pathJoin(iamConfigPolicyDBSTSUsersPrefix, name+".json") - case isGroup: +func getMappedPolicyPath(name string, userType IAMUserType, isGroup bool) string { + if isGroup { return pathJoin(iamConfigPolicyDBGroupsPrefix, name+".json") + } + switch userType { + case srvAccUser: + return pathJoin(iamConfigPolicyDBServiceAccountsPrefix, name+".json") + case stsUser: + return pathJoin(iamConfigPolicyDBSTSUsersPrefix, name+".json") default: return pathJoin(iamConfigPolicyDBUsersPrefix, name+".json") } @@ -180,6 +194,15 @@ type IAMSys struct { store IAMStorageAPI } +// IAMUserType represents a user type inside MinIO server +type IAMUserType int + +const ( + regularUser IAMUserType = iota + stsUser + srvAccUser +) + // IAMStorageAPI defines an interface for the IAM persistence layer type IAMStorageAPI interface { migrateBackendFormat(ObjectLayer) error @@ -187,14 +210,14 @@ type IAMStorageAPI interface { loadPolicyDoc(policy string, m map[string]iampolicy.Policy) error loadPolicyDocs(m map[string]iampolicy.Policy) error - loadUser(user string, isSTS bool, m map[string]auth.Credentials) error - loadUsers(isSTS bool, m map[string]auth.Credentials) error + loadUser(user string, userType IAMUserType, m map[string]auth.Credentials) error + loadUsers(userType IAMUserType, m map[string]auth.Credentials) error loadGroup(group string, m map[string]GroupInfo) error loadGroups(m map[string]GroupInfo) error - loadMappedPolicy(name string, isSTS, isGroup bool, m map[string]MappedPolicy) error - loadMappedPolicies(isSTS, isGroup bool, m map[string]MappedPolicy) error + loadMappedPolicy(name string, userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error + loadMappedPolicies(userType IAMUserType, isGroup bool, m map[string]MappedPolicy) error loadAll(*IAMSys, ObjectLayer) error @@ -203,13 +226,13 @@ type IAMStorageAPI interface { deleteIAMConfig(path string) error savePolicyDoc(policyName string, p iampolicy.Policy) error - saveMappedPolicy(name string, isSTS, isGroup bool, mp MappedPolicy) error - saveUserIdentity(name string, isSTS bool, u UserIdentity) error + saveMappedPolicy(name string, userType IAMUserType, isGroup bool, mp MappedPolicy) error + saveUserIdentity(name string, userType IAMUserType, u UserIdentity) error saveGroupInfo(group string, gi GroupInfo) error deletePolicyDoc(policyName string) error - deleteMappedPolicy(name string, isSTS, isGroup bool) error - deleteUserIdentity(name string, isSTS bool) error + deleteMappedPolicy(name string, userType IAMUserType, isGroup bool) error + deleteUserIdentity(name string, userType IAMUserType) error deleteGroupInfo(name string) error watch(*IAMSys) @@ -290,9 +313,9 @@ func (sys *IAMSys) LoadPolicyMapping(objAPI ObjectLayer, userOrGroup string, isG if globalEtcdClient == nil { var err error if isGroup { - err = sys.store.loadMappedPolicy(userOrGroup, false, isGroup, sys.iamGroupPolicyMap) + err = sys.store.loadMappedPolicy(userOrGroup, regularUser, isGroup, sys.iamGroupPolicyMap) } else { - err = sys.store.loadMappedPolicy(userOrGroup, false, isGroup, sys.iamUserPolicyMap) + err = sys.store.loadMappedPolicy(userOrGroup, regularUser, isGroup, sys.iamUserPolicyMap) } // Ignore policy not mapped error @@ -305,7 +328,7 @@ func (sys *IAMSys) LoadPolicyMapping(objAPI ObjectLayer, userOrGroup string, isG } // LoadUser - reloads a specific user from backend disks or etcd. -func (sys *IAMSys) LoadUser(objAPI ObjectLayer, accessKey string, isSTS bool) error { +func (sys *IAMSys) LoadUser(objAPI ObjectLayer, accessKey string, userType IAMUserType) error { sys.Lock() defer sys.Unlock() @@ -314,11 +337,11 @@ func (sys *IAMSys) LoadUser(objAPI ObjectLayer, accessKey string, isSTS bool) er } if globalEtcdClient == nil { - err := sys.store.loadUser(accessKey, isSTS, sys.iamUsersMap) + err := sys.store.loadUser(accessKey, userType, sys.iamUsersMap) if err != nil { return err } - err = sys.store.loadMappedPolicy(accessKey, isSTS, false, sys.iamUserPolicyMap) + err = sys.store.loadMappedPolicy(accessKey, userType, false, sys.iamUserPolicyMap) // Ignore policy not mapped error if err != nil && err != errConfigNotFound { return err @@ -360,8 +383,12 @@ func (sys *IAMSys) Init(objAPI ObjectLayer) error { } sys.store.watch(sys) + err := sys.store.loadAll(sys, objAPI) - return sys.store.loadAll(sys, objAPI) + // Invalidate the old cred after finishing IAM initialization + globalOldCred = auth.Credentials{} + + return err } // DeletePolicy - deletes a canned policy from backend or etcd. @@ -393,7 +420,7 @@ func (sys *IAMSys) DeletePolicy(policyName string) error { // Delete user-policy mappings that will no longer apply var usersToDel []string - var isUserSTS []bool + var usersType []IAMUserType for u, mp := range sys.iamUserPolicyMap { if mp.Policy == policyName { usersToDel = append(usersToDel, u) @@ -403,12 +430,15 @@ func (sys *IAMSys) DeletePolicy(policyName string) error { return errNoSuchUser } // User is from STS if the creds are temporary - isSTS := cr.IsTemp() - isUserSTS = append(isUserSTS, isSTS) + if cr.IsTemp() { + usersType = append(usersType, stsUser) + } else { + usersType = append(usersType, regularUser) + } } } for i, u := range usersToDel { - sys.policyDBSet(u, "", isUserSTS[i], false) + sys.policyDBSet(u, "", usersType[i], false) } // Delete group-policy mappings that will no longer apply @@ -419,7 +449,7 @@ func (sys *IAMSys) DeletePolicy(policyName string) error { } } for _, g := range groupsToDel { - sys.policyDBSet(g, "", false, true) + sys.policyDBSet(g, "", regularUser, true) } return err @@ -523,8 +553,8 @@ func (sys *IAMSys) DeleteUser(accessKey string) error { } // It is ok to ignore deletion error on the mapped policy - sys.store.deleteMappedPolicy(accessKey, false, false) - err := sys.store.deleteUserIdentity(accessKey, false) + sys.store.deleteMappedPolicy(accessKey, regularUser, false) + err := sys.store.deleteUserIdentity(accessKey, regularUser) switch err.(type) { case ObjectNotFound: // ignore if user is already deleted. @@ -534,6 +564,15 @@ func (sys *IAMSys) DeleteUser(accessKey string) error { delete(sys.iamUsersMap, accessKey) delete(sys.iamUserPolicyMap, accessKey) + for _, u := range sys.iamUsersMap { + if u.IsServiceAccount() { + if u.ParentUser == accessKey { + _ = sys.store.deleteUserIdentity(u.AccessKey, srvAccUser) + delete(sys.iamUsersMap, u.AccessKey) + } + } + } + return err } @@ -565,7 +604,7 @@ func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyNa } mp := newMappedPolicy(policyName) - if err := sys.store.saveMappedPolicy(accessKey, true, false, mp); err != nil { + if err := sys.store.saveMappedPolicy(accessKey, stsUser, false, mp); err != nil { return err } @@ -577,7 +616,7 @@ func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyNa } u := newUserIdentity(cred) - if err := sys.store.saveUserIdentity(accessKey, true, u); err != nil { + if err := sys.store.saveUserIdentity(accessKey, stsUser, u); err != nil { return err } @@ -602,7 +641,7 @@ func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) { } for k, v := range sys.iamUsersMap { - if !v.IsTemp() { + if !v.IsTemp() && !v.IsServiceAccount() { users[k] = madmin.UserInfo{ PolicyName: sys.iamUserPolicyMap[k].Policy, Status: func() madmin.AccountStatus { @@ -636,6 +675,28 @@ func (sys *IAMSys) IsTempUser(name string) (bool, error) { return creds.IsTemp(), nil } +// IsServiceAccount - returns if given key is a service account +func (sys *IAMSys) IsServiceAccount(name string) (bool, string, error) { + objectAPI := newObjectLayerWithoutSafeModeFn() + if objectAPI == nil { + return false, "", errServerNotInitialized + } + + sys.RLock() + defer sys.RUnlock() + + creds, found := sys.iamUsersMap[name] + if !found { + return false, "", errNoSuchUser + } + + if creds.IsServiceAccount() { + return true, creds.ParentUser, nil + } + + return false, "", nil +} + // GetUserInfo - get info on a user. func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) { objectAPI := newObjectLayerWithoutSafeModeFn() @@ -717,7 +778,7 @@ func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus) return errServerNotInitialized } - if err := sys.store.saveUserIdentity(accessKey, false, uinfo); err != nil { + if err := sys.store.saveUserIdentity(accessKey, regularUser, uinfo); err != nil { return err } @@ -725,6 +786,104 @@ func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus) return nil } +// NewServiceAccount - create a new service account +func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser, sessionPolicy string) (auth.Credentials, error) { + objectAPI := newObjectLayerWithoutSafeModeFn() + if objectAPI == nil { + return auth.Credentials{}, errServerNotInitialized + } + + if len(sessionPolicy) > 16*1024 { + return auth.Credentials{}, fmt.Errorf("Session policy should not exceed 16*1024 characters") + } + + policy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(sessionPolicy))) + if err != nil { + return auth.Credentials{}, err + } + + // Version in policy must not be empty + if policy.Version == "" { + return auth.Credentials{}, fmt.Errorf("Invalid session policy version") + } + + sys.Lock() + defer sys.Unlock() + + if sys.usersSysType != MinIOUsersSysType { + return auth.Credentials{}, errIAMActionNotAllowed + } + + if sys.store == nil { + return auth.Credentials{}, errServerNotInitialized + } + + if parentUser == globalActiveCred.AccessKey { + return auth.Credentials{}, errIAMActionNotAllowed + } + + cr, ok := sys.iamUsersMap[parentUser] + if !ok { + return auth.Credentials{}, errNoSuchUser + } + + if cr.IsTemp() { + return auth.Credentials{}, errIAMActionNotAllowed + } + + m := make(map[string]interface{}) + m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString([]byte(sessionPolicy)) + m[parentClaim] = parentUser + m[iamPolicyClaimName()] = "embedded-policy" + + secret := globalActiveCred.SecretKey + cred, err := auth.GetNewCredentialsWithMetadata(m, secret) + if err != nil { + return auth.Credentials{}, err + } + + cred.ParentUser = parentUser + u := newUserIdentity(cred) + + if err := sys.store.saveUserIdentity(u.Credentials.AccessKey, srvAccUser, u); err != nil { + return auth.Credentials{}, err + } + + sys.iamUsersMap[u.Credentials.AccessKey] = u.Credentials + + return cred, nil +} + +// GetServiceAccount - returns the credentials of the given service account +func (sys *IAMSys) GetServiceAccount(ctx context.Context, serviceAccountAccessKey string) (auth.Credentials, error) { + objectAPI := newObjectLayerWithoutSafeModeFn() + if objectAPI == nil { + return auth.Credentials{}, errServerNotInitialized + } + + sys.Lock() + defer sys.Unlock() + + if sys.usersSysType != MinIOUsersSysType { + return auth.Credentials{}, errIAMActionNotAllowed + } + + if sys.store == nil { + return auth.Credentials{}, errServerNotInitialized + } + + cr, ok := sys.iamUsersMap[serviceAccountAccessKey] + if !ok { + return auth.Credentials{}, errNoSuchUser + } + + if !cr.IsServiceAccount() { + return auth.Credentials{}, errIAMActionNotAllowed + } + + return cr, nil +} + // SetUser - set user credentials and policy. func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error { objectAPI := newObjectLayerWithoutSafeModeFn() @@ -754,7 +913,7 @@ func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error { return errIAMActionNotAllowed } - if err := sys.store.saveUserIdentity(accessKey, false, u); err != nil { + if err := sys.store.saveUserIdentity(accessKey, regularUser, u); err != nil { return err } @@ -762,7 +921,7 @@ func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error { // Set policy if specified. if uinfo.PolicyName != "" { - return sys.policyDBSet(accessKey, uinfo.PolicyName, false, false) + return sys.policyDBSet(accessKey, uinfo.PolicyName, regularUser, false) } return nil } @@ -792,7 +951,7 @@ func (sys *IAMSys) SetUserSecretKey(accessKey string, secretKey string) error { cred.SecretKey = secretKey u := newUserIdentity(cred) - if err := sys.store.saveUserIdentity(accessKey, false, u); err != nil { + if err := sys.store.saveUserIdentity(accessKey, regularUser, u); err != nil { return err } @@ -923,7 +1082,7 @@ func (sys *IAMSys) RemoveUsersFromGroup(group string, members []string) error { // Remove the group from storage. First delete the // mapped policy. - err := sys.store.deleteMappedPolicy(group, false, true) + err := sys.store.deleteMappedPolicy(group, regularUser, true) // No-mapped-policy case is ignored. if err != nil && err != errConfigNotFound { return err @@ -1068,12 +1227,12 @@ func (sys *IAMSys) PolicyDBSet(name, policy string, isGroup bool) error { // isSTS is always false when called via PolicyDBSet as policy // is never set by an external API call for STS users. - return sys.policyDBSet(name, policy, false, isGroup) + return sys.policyDBSet(name, policy, regularUser, isGroup) } // policyDBSet - sets a policy for user in the policy db. Assumes that caller // has sys.Lock(). If policy == "", then policy mapping is removed. -func (sys *IAMSys) policyDBSet(name, policy string, isSTS, isGroup bool) error { +func (sys *IAMSys) policyDBSet(name, policy string, userType IAMUserType, isGroup bool) error { if sys.store == nil { return errServerNotInitialized } @@ -1099,7 +1258,7 @@ func (sys *IAMSys) policyDBSet(name, policy string, isSTS, isGroup bool) error { // Handle policy mapping removal if policy == "" { - if err := sys.store.deleteMappedPolicy(name, isSTS, isGroup); err != nil { + if err := sys.store.deleteMappedPolicy(name, userType, isGroup); err != nil { return err } if !isGroup { @@ -1112,7 +1271,7 @@ func (sys *IAMSys) policyDBSet(name, policy string, isSTS, isGroup bool) error { // Handle policy mapping set/update mp := newMappedPolicy(policy) - if err := sys.store.saveMappedPolicy(name, isSTS, isGroup, mp); err != nil { + if err := sys.store.saveMappedPolicy(name, userType, isGroup, mp); err != nil { return err } if !isGroup { @@ -1294,6 +1453,93 @@ func (sys *IAMSys) policyDBGet(name string, isGroup bool) ([]string, error) { return result, nil } +// IsAllowedServiceAccount - checks if the given service account is allowed to perform +// actions. The permission of the parent user is checked first +func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parent string) bool { + // Now check if we have a subject claim + p, ok := args.Claims[parentClaim] + if ok { + parentInClaim, ok := p.(string) + if !ok { + // Reject malformed/malicious requests. + return false + } + // The parent claim in the session token should be equal + // to the parent detected in the backend + if parentInClaim != parent { + return false + } + } + + // Check if the parent is allowed to perform this action, reject if not + parentUserPolicies, err := sys.PolicyDBGet(parent, false) + if err != nil { + logger.LogIf(context.Background(), err) + return false + } + + if len(parentUserPolicies) == 0 { + return false + } + + var availablePolicies []iampolicy.Policy + + // Policies were found, evaluate all of them. + sys.RLock() + for _, pname := range parentUserPolicies { + p, found := sys.iamPolicyDocsMap[pname] + if found { + availablePolicies = append(availablePolicies, p) + } + } + sys.RUnlock() + + if len(availablePolicies) == 0 { + return false + } + + combinedPolicy := availablePolicies[0] + for i := 1; i < len(availablePolicies); i++ { + combinedPolicy.Statements = append(combinedPolicy.Statements, + availablePolicies[i].Statements...) + } + + serviceAcc := args.AccountName + args.AccountName = parent + if !combinedPolicy.IsAllowed(args) { + return false + } + args.AccountName = serviceAcc + + // Now check if we have a sessionPolicy. + spolicy, ok := args.Claims[iampolicy.SessionPolicyName] + if ok { + spolicyStr, ok := spolicy.(string) + if !ok { + // Sub policy if set, should be a string reject + // malformed/malicious requests. + return false + } + + // Check if policy is parseable. + subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr))) + if err != nil { + // Log any error in input session policy config. + logger.LogIf(context.Background(), err) + return false + } + + // Policy without Version string value reject it. + if subPolicy.Version == "" { + return false + } + + return subPolicy.IsAllowed(args) + } + + return false +} + // IsAllowedSTS is meant for STS based temporary credentials, // which implements claims validation and verification other than // applying policies. @@ -1444,6 +1690,17 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool { return sys.IsAllowedSTS(args) } + // If the credential is for a service account, perform related check + ok, parentUser, err := sys.IsServiceAccount(args.AccountName) + if err != nil { + logger.LogIf(context.Background(), err) + return false + } + if ok { + return sys.IsAllowedServiceAccount(args, parentUser) + } + + // Continue with the assumption of a regular user policies, err := sys.PolicyDBGet(args.AccountName, false) if err != nil { logger.LogIf(context.Background(), err) diff --git a/cmd/peer-rest-server.go b/cmd/peer-rest-server.go index 94a64a673..e174084c0 100644 --- a/cmd/peer-rest-server.go +++ b/cmd/peer-rest-server.go @@ -325,7 +325,12 @@ func (s *peerRESTServer) LoadUserHandler(w http.ResponseWriter, r *http.Request) return } - if err = globalIAMSys.LoadUser(objAPI, accessKey, temp); err != nil { + var userType = regularUser + if temp { + userType = stsUser + } + + if err = globalIAMSys.LoadUser(objAPI, accessKey, userType); err != nil { s.writeErrorResponse(w, err) return } diff --git a/cmd/sts-handlers.go b/cmd/sts-handlers.go index 2d0e73fdf..6049c2512 100644 --- a/cmd/sts-handlers.go +++ b/cmd/sts-handlers.go @@ -58,6 +58,9 @@ const ( expClaim = "exp" subClaim = "sub" + // JWT claim to check the parent user + parentClaim = "parent" + // LDAP claim keys ldapUser = "ldapUser" ldapGroups = "ldapGroups" diff --git a/pkg/auth/credentials.go b/pkg/auth/credentials.go index be9413dd9..604218d15 100644 --- a/pkg/auth/credentials.go +++ b/pkg/auth/credentials.go @@ -90,6 +90,7 @@ type Credentials struct { Expiration time.Time `xml:"Expiration" json:"expiration,omitempty"` SessionToken string `xml:"SessionToken" json:"sessionToken,omitempty"` Status string `xml:"-" json:"status,omitempty"` + ParentUser string `xml:"-" json:"parentUser,omitempty"` } func (cred Credentials) String() string { @@ -119,7 +120,12 @@ func (cred Credentials) IsExpired() bool { // IsTemp - returns whether credential is temporary or not. func (cred Credentials) IsTemp() bool { - return cred.SessionToken != "" + return cred.SessionToken != "" && cred.ParentUser == "" +} + +// IsServiceAccount - returns whether credential is a service account or not +func (cred Credentials) IsServiceAccount() bool { + return cred.ParentUser != "" } // IsValid - returns whether credential is valid or not. @@ -207,14 +213,15 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string) "/", "+", -1) cred.Status = "on" + if tokenSecret == "" { + cred.Expiration = timeSentinel + return cred, nil + } + expiry, err := ExpToInt64(m["exp"]) if err != nil { return cred, err } - if expiry == 0 { - cred.Expiration = timeSentinel - return cred, nil - } m["accessKey"] = cred.AccessKey jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims(m)) diff --git a/pkg/madmin/examples/add-service-account-and-policy.go b/pkg/madmin/examples/add-service-account-and-policy.go new file mode 100644 index 000000000..214811815 --- /dev/null +++ b/pkg/madmin/examples/add-service-account-and-policy.go @@ -0,0 +1,52 @@ +// +build ignore + +/* + * MinIO Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package main + +import ( + "fmt" + "log" + + "github.com/minio/minio/pkg/madmin" +) + +func main() { + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are + // dummy values, please replace them with original values. + + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are + // dummy values, please replace them with original values. + + // API requests are secure (HTTPS) if secure=true and insecure (HTTP) otherwise. + // New returns an MinIO Admin client object. + madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) + if err != nil { + log.Fatalln(err) + } + + // Create policy + policy := `{"Version": "2012-10-17","Statement": [{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": ["arn:aws:s3:::testbucket/*"],"Sid": ""}]}` + + creds, err := madmClnt.AddServiceAccount("parentuser", policy) + if err != nil { + log.Fatalln(err) + } + + fmt.Println(creds) +} diff --git a/pkg/madmin/examples/get-service-account.go b/pkg/madmin/examples/get-service-account.go new file mode 100644 index 000000000..93303bd4a --- /dev/null +++ b/pkg/madmin/examples/get-service-account.go @@ -0,0 +1,49 @@ +// +build ignore + +/* + * MinIO Cloud Storage, (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package main + +import ( + "fmt" + "log" + + "github.com/minio/minio/pkg/madmin" +) + +func main() { + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are + // dummy values, please replace them with original values. + + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are + // dummy values, please replace them with original values. + + // API requests are secure (HTTPS) if secure=true and insecure (HTTP) otherwise. + // New returns an MinIO Admin client object. + madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) + if err != nil { + log.Fatalln(err) + } + + creds, err := madmClnt.GetServiceAccount("service-account-access-key") + if err != nil { + log.Fatalln(err) + } + + fmt.Println(creds) +} diff --git a/pkg/madmin/user-commands.go b/pkg/madmin/user-commands.go index 3cd73b47d..1ec006204 100644 --- a/pkg/madmin/user-commands.go +++ b/pkg/madmin/user-commands.go @@ -210,3 +210,101 @@ func (adm *AdminClient) SetUserStatus(accessKey string, status AccountStatus) er return nil } + +// AddServiceAccountReq is the request body of the add service account admin call +type AddServiceAccountReq struct { + Parent string `json:"parent"` + Policy string `json:"policy"` +} + +// AddServiceAccountResp is the response body of the add service account admin call +type AddServiceAccountResp struct { + Credentials auth.Credentials `json:"credentials"` +} + +// AddServiceAccount - creates a new service account belonging to the given parent user +// while restricting the service account permission by the given policy document. +func (adm *AdminClient) AddServiceAccount(parentUser string, policy string) (auth.Credentials, error) { + + if !auth.IsAccessKeyValid(parentUser) { + return auth.Credentials{}, auth.ErrInvalidAccessKeyLength + } + + data, err := json.Marshal(AddServiceAccountReq{ + Parent: parentUser, + Policy: policy, + }) + if err != nil { + return auth.Credentials{}, err + } + + econfigBytes, err := EncryptData(adm.secretAccessKey, data) + if err != nil { + return auth.Credentials{}, err + } + + reqData := requestData{ + relPath: adminAPIPrefix + "/add-service-account", + content: econfigBytes, + } + + // Execute PUT on /minio/admin/v2/add-service-account to set a user. + resp, err := adm.executeMethod("PUT", reqData) + defer closeResponse(resp) + if err != nil { + return auth.Credentials{}, err + } + + if resp.StatusCode != http.StatusOK { + return auth.Credentials{}, httpRespToErrorResponse(resp) + } + + data, err = DecryptData(adm.secretAccessKey, resp.Body) + if err != nil { + return auth.Credentials{}, err + } + + var serviceAccountResp AddServiceAccountResp + if err = json.Unmarshal(data, &serviceAccountResp); err != nil { + return auth.Credentials{}, err + } + return serviceAccountResp.Credentials, nil +} + +// GetServiceAccount returns the credential of the service account +func (adm *AdminClient) GetServiceAccount(serviceAccountAccessKey string) (auth.Credentials, error) { + + if !auth.IsAccessKeyValid(serviceAccountAccessKey) { + return auth.Credentials{}, auth.ErrInvalidAccessKeyLength + } + + queryValues := url.Values{} + queryValues.Set("accessKey", serviceAccountAccessKey) + + reqData := requestData{ + relPath: adminAPIPrefix + "/get-service-account", + queryValues: queryValues, + } + + // Execute GET on /minio/admin/v2/get-service-account to set a user. + resp, err := adm.executeMethod("GET", reqData) + defer closeResponse(resp) + if err != nil { + return auth.Credentials{}, err + } + + if resp.StatusCode != http.StatusOK { + return auth.Credentials{}, httpRespToErrorResponse(resp) + } + + data, err := DecryptData(adm.secretAccessKey, resp.Body) + if err != nil { + return auth.Credentials{}, err + } + + var creds auth.Credentials + if err = json.Unmarshal(data, &creds); err != nil { + return auth.Credentials{}, err + } + return creds, nil +}