Add service account type in IAM (#9029)

master
Anis Elleuch 5 years ago committed by GitHub
parent 8b880a246a
commit 496f4a7dc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 114
      cmd/admin-handlers-users.go
  2. 4
      cmd/admin-router.go
  3. 7
      cmd/api-errors.go
  4. 13
      cmd/common-main.go
  5. 39
      cmd/config-encrypted.go
  6. 3
      cmd/globals.go
  7. 111
      cmd/iam-etcd-store.go
  8. 104
      cmd/iam-object-store.go
  9. 347
      cmd/iam.go
  10. 7
      cmd/peer-rest-server.go
  11. 3
      cmd/sts-handlers.go
  12. 17
      pkg/auth/credentials.go
  13. 52
      pkg/madmin/examples/add-service-account-and-policy.go
  14. 49
      pkg/madmin/examples/get-service-account.go
  15. 98
      pkg/madmin/user-commands.go

@ -378,6 +378,120 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
}
}
// AddServiceAccount - PUT /minio/admin/v2/add-service-account?parentUser=<parent_user_accesskey>
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=<access_key>
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")

@ -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:.*}")

@ -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",

@ -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) {

@ -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")
}

@ -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

@ -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 {

@ -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
}

@ -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)

@ -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
}

@ -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"

@ -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))

@ -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)
}

@ -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)
}

@ -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
}

Loading…
Cancel
Save