diff --git a/cmd/admin-handlers-idp-ldap.go b/cmd/admin-handlers-idp-ldap.go index 72fd4d99c..46f490ded 100644 --- a/cmd/admin-handlers-idp-ldap.go +++ b/cmd/admin-handlers-idp-ldap.go @@ -19,10 +19,14 @@ package cmd import ( "encoding/json" + "errors" + "fmt" "io" "net/http" + "strings" "github.com/minio/madmin-go/v3" + "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/logger" "github.com/minio/mux" "github.com/minio/pkg/v2/policy" @@ -175,3 +179,267 @@ func (a adminAPIHandlers) AttachDetachPolicyLDAP(w http.ResponseWriter, r *http. writeSuccessResponseJSON(w, encryptedData) } + +// AddServiceAccountLDAP adds a new service account for provided LDAP username or DN +// +// PUT /minio/admin/v3/idp/ldap/add-service-account +func (a adminAPIHandlers) AddServiceAccountLDAP(w http.ResponseWriter, r *http.Request) { + ctx, cred, opts, createReq, targetUser, APIError := commonAddServiceAccount(r) + if APIError.Code != "" { + writeErrorResponseJSON(ctx, w, APIError, r.URL) + return + } + + // fail if ldap is not enabled + if !globalIAMSys.LDAPConfig.Enabled() { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errors.New("LDAP not enabled")), r.URL) + } + + // Find the user for the request sender (as it may be sent via a service + // account or STS account): + requestorUser := cred.AccessKey + requestorParentUser := cred.AccessKey + requestorGroups := cred.Groups + requestorIsDerivedCredential := false + if cred.IsServiceAccount() || cred.IsTemp() { + requestorParentUser = cred.ParentUser + requestorIsDerivedCredential = true + } + + // Check if we are creating svc account for request sender. + isSvcAccForRequestor := false + if targetUser == requestorUser || targetUser == requestorParentUser { + isSvcAccForRequestor = true + } + + var ( + targetGroups []string + err error + ) + + // If we are creating svc account for request sender, ensure + // that targetUser is a real user (i.e. not derived + // credentials). + if isSvcAccForRequestor { + if requestorIsDerivedCredential { + if requestorParentUser == "" { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, + errors.New("service accounts cannot be generated for temporary credentials without parent")), r.URL) + return + } + targetUser = requestorParentUser + } + targetGroups = requestorGroups + + // Deny if the target user is not LDAP + isLDAP, err := globalIAMSys.LDAPConfig.DoesUsernameExist(targetUser) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + if isLDAP == "" { + err := errors.New("Specified user does not exist on LDAP server") + APIErr := errorCodes.ToAPIErrWithErr(ErrAdminNoSuchUser, err) + writeErrorResponseJSON(ctx, w, APIErr, r.URL) + return + } + + // In case of LDAP/OIDC we need to set `opts.claims` to ensure + // it is associated with the LDAP/OIDC user properly. + for k, v := range cred.Claims { + if k == expClaim { + continue + } + opts.claims[k] = v + } + } else { + isDN := globalIAMSys.LDAPConfig.IsLDAPUserDN(targetUser) + + opts.claims[ldapUserN] = targetUser // simple username + targetUser, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(targetUser) + if err != nil { + // if not found, check if DN + if strings.Contains(err.Error(), "not found") && isDN { + // warn user that DNs are not allowed + err = fmt.Errorf("Must use short username to add service account. %w", err) + } + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + opts.claims[ldapUser] = targetUser // DN + } + + newCred, updatedAt, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + createResp := madmin.AddServiceAccountResp{ + Credentials: madmin.Credentials{ + AccessKey: newCred.AccessKey, + SecretKey: newCred.SecretKey, + Expiration: newCred.Expiration, + }, + } + + data, err := json.Marshal(createResp) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + encryptedData, err := madmin.EncryptData(cred.SecretKey, data) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, encryptedData) + + // Call hook for cluster-replication if the service account is not for a + // root user. + if newCred.ParentUser != globalActiveCred.AccessKey { + logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{ + Type: madmin.SRIAMItemSvcAcc, + SvcAccChange: &madmin.SRSvcAccChange{ + Create: &madmin.SRSvcAccCreate{ + Parent: newCred.ParentUser, + AccessKey: newCred.AccessKey, + SecretKey: newCred.SecretKey, + Groups: newCred.Groups, + Name: newCred.Name, + Description: newCred.Description, + Claims: opts.claims, + SessionPolicy: createReq.Policy, + Status: auth.AccountOn, + Expiration: createReq.Expiration, + }, + }, + UpdatedAt: updatedAt, + })) + } +} + +// ListAccessKeysLDAP - GET /minio/admin/v3/idp/ldap/list-access-keys +func (a adminAPIHandlers) ListAccessKeysLDAP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Get current object layer instance. + objectAPI := newObjectLayerFn() + if objectAPI == nil || globalNotificationSys == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + cred, owner, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } + + userDN := r.Form.Get("userDN") + + // If listing is requested for a specific user (who is not the request + // sender), check that the user has permissions. + if userDN != "" && userDN != cred.ParentUser { + if !globalIAMSys.IsAllowed(policy.Args{ + AccountName: cred.AccessKey, + Groups: cred.Groups, + Action: policy.ListServiceAccountsAdminAction, + ConditionValues: getConditionValues(r, "", cred), + IsOwner: owner, + Claims: cred.Claims, + }) { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + } else { + if !globalIAMSys.IsAllowed(policy.Args{ + AccountName: cred.AccessKey, + Groups: cred.Groups, + Action: policy.ListServiceAccountsAdminAction, + ConditionValues: getConditionValues(r, "", cred), + IsOwner: owner, + Claims: cred.Claims, + DenyOnly: true, + }) { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + userDN = cred.AccessKey + if cred.ParentUser != "" { + userDN = cred.ParentUser + } + } + + targetAccount, err := globalIAMSys.LDAPConfig.DoesUsernameExist(userDN) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } else if userDN == "" { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL) + return + } + + listType := r.Form.Get("listType") + if listType != "sts-only" && listType != "svcacc-only" && listType != "" { + // default to both + listType = "" + } + + var serviceAccounts []auth.Credentials + var stsKeys []auth.Credentials + + if listType == "" || listType == "sts-only" { + stsKeys, err = globalIAMSys.ListSTSAccounts(ctx, targetAccount) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + } + if listType == "" || listType == "svcacc-only" { + serviceAccounts, err = globalIAMSys.ListServiceAccounts(ctx, targetAccount) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + } + + var serviceAccountList []madmin.ServiceAccountInfo + var stsKeyList []madmin.ServiceAccountInfo + + for _, svc := range serviceAccounts { + expiryTime := svc.Expiration + serviceAccountList = append(serviceAccountList, madmin.ServiceAccountInfo{ + AccessKey: svc.AccessKey, + Expiration: &expiryTime, + }) + } + for _, sts := range stsKeys { + expiryTime := sts.Expiration + stsKeyList = append(stsKeyList, madmin.ServiceAccountInfo{ + AccessKey: sts.AccessKey, + Expiration: &expiryTime, + }) + } + + listResp := madmin.ListAccessKeysLDAPResp{ + ServiceAccounts: serviceAccountList, + STSKeys: stsKeyList, + } + + data, err := json.Marshal(listResp) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + encryptedData, err := madmin.EncryptData(cred.SecretKey, data) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + writeSuccessResponseJSON(w, encryptedData) +} diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 4fa38de49..1d9f2a5b1 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -614,72 +614,17 @@ func (a adminAPIHandlers) TemporaryAccountInfo(w http.ResponseWriter, r *http.Re // AddServiceAccount - PUT /minio/admin/v3/add-service-account func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - // Get current object layer instance. - objectAPI := newObjectLayerFn() - if objectAPI == nil || globalNotificationSys == nil { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) - return - } - - cred, owner, s3Err := validateAdminSignature(ctx, r, "") - if s3Err != ErrNone { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) - return - } - - password := cred.SecretKey - reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) - if err != nil { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) - return - } - - var createReq madmin.AddServiceAccountReq - if err = json.Unmarshal(reqBytes, &createReq); err != nil { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) - return - } - - // service account access key cannot have space characters beginning and end of the string. - if hasSpaceBE(createReq.AccessKey) { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) - return - } - - if err := createReq.Validate(); err != nil { - // Since this validation would happen client side as well, we only send - // a generic error message here. - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL) + ctx, cred, opts, createReq, targetUser, APIError := commonAddServiceAccount(r) + if APIError.Code != "" { + writeErrorResponseJSON(ctx, w, APIError, r.URL) return } var ( - targetUser string targetGroups []string + err error ) - // If the request did not set a TargetUser, the service account is - // created for the request sender. - targetUser = createReq.TargetUser - if targetUser == "" { - targetUser = cred.AccessKey - } - - description := createReq.Description - if description == "" { - description = createReq.Comment - } - opts := newServiceAccountOpts{ - accessKey: createReq.AccessKey, - secretKey: createReq.SecretKey, - name: createReq.Name, - description: description, - expiration: createReq.Expiration, - claims: make(map[string]interface{}), - } - // Find the user for the request sender (as it may be sent via a service // account or STS account): requestorUser := cred.AccessKey @@ -713,23 +658,6 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque // that targetUser is a real user (i.e. not derived // credentials). if isSvcAccForRequestor { - // Check if adding service account is explicitly denied. - // - // This allows turning off service accounts for request sender, - // if there is no deny statement this call is implicitly enabled. - if !globalIAMSys.IsAllowed(policy.Args{ - AccountName: requestorUser, - Groups: requestorGroups, - Action: policy.CreateServiceAccountAdminAction, - ConditionValues: getConditionValues(r, "", cred), - IsOwner: owner, - Claims: cred.Claims, - DenyOnly: true, - }) { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) - return - } - if requestorIsDerivedCredential { if requestorParentUser == "" { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, @@ -748,32 +676,16 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque } opts.claims[k] = v } - } else { - // Need permission if we are creating a service account for a - // user <> to the request sender - if !globalIAMSys.IsAllowed(policy.Args{ - AccountName: requestorUser, - Groups: requestorGroups, - Action: policy.CreateServiceAccountAdminAction, - ConditionValues: getConditionValues(r, "", cred), - IsOwner: owner, - Claims: cred.Claims, - }) { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) - return - } - + } else if globalIAMSys.LDAPConfig.Enabled() { // In case of LDAP we need to resolve the targetUser to a DN and // query their groups: - if globalIAMSys.LDAPConfig.Enabled() { - opts.claims[ldapUserN] = targetUser // simple username - targetUser, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(targetUser) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - opts.claims[ldapUser] = targetUser // username DN + opts.claims[ldapUserN] = targetUser // simple username + targetUser, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(targetUser) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return } + opts.claims[ldapUser] = targetUser // username DN // NOTE: if not using LDAP, then internal IDP or open ID is // being used - in the former, group info is enforced when @@ -781,19 +693,6 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque // latter, a group notion is not supported. } - var sp *policy.Policy - if len(createReq.Policy) > 0 { - sp, err = policy.ParseConfig(bytes.NewReader(createReq.Policy)) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - if sp.Version == "" && len(sp.Statements) == 0 { - sp = nil - } - } - - opts.sessionPolicy = sp newCred, updatedAt, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) @@ -814,7 +713,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque return } - encryptedData, err := madmin.EncryptData(password, data) + encryptedData, err := madmin.EncryptData(cred.SecretKey, data) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return @@ -2514,3 +2413,85 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) { } } } + +func commonAddServiceAccount(r *http.Request) (context.Context, auth.Credentials, newServiceAccountOpts, madmin.AddServiceAccountReq, string, APIError) { + ctx := r.Context() + + // Get current object layer instance. + objectAPI := newObjectLayerFn() + if objectAPI == nil || globalNotificationSys == nil { + return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrServerNotInitialized) + } + + cred, owner, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(s3Err) + } + + password := cred.SecretKey + reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) + if err != nil { + return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err) + } + + var createReq madmin.AddServiceAccountReq + if err = json.Unmarshal(reqBytes, &createReq); err != nil { + return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err) + } + + // service account access key cannot have space characters beginning and end of the string. + if hasSpaceBE(createReq.AccessKey) { + return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument) + } + + if err := createReq.Validate(); err != nil { + // Since this validation would happen client side as well, we only send + // a generic error message here. + return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument) + } + // If the request did not set a TargetUser, the service account is + // created for the request sender. + targetUser := createReq.TargetUser + if targetUser == "" { + targetUser = cred.AccessKey + } + + description := createReq.Description + if description == "" { + description = createReq.Comment + } + opts := newServiceAccountOpts{ + accessKey: createReq.AccessKey, + secretKey: createReq.SecretKey, + name: createReq.Name, + description: description, + expiration: createReq.Expiration, + claims: make(map[string]interface{}), + } + + // Check if action is allowed if creating access key for another user + // Check if action is explicitly denied if for self + if !globalIAMSys.IsAllowed(policy.Args{ + AccountName: cred.AccessKey, + Groups: cred.Groups, + Action: policy.CreateServiceAccountAdminAction, + ConditionValues: getConditionValues(r, "", cred), + IsOwner: owner, + Claims: cred.Claims, + DenyOnly: (targetUser == cred.AccessKey || targetUser == cred.ParentUser), + }) { + return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", errorCodes.ToAPIErr(ErrAccessDenied) + } + + var sp *policy.Policy + if len(createReq.Policy) > 0 { + sp, err = policy.ParseConfig(bytes.NewReader(createReq.Policy)) + if err != nil { + return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", toAdminAPIErr(ctx, err) + } + } + + opts.sessionPolicy = sp + + return ctx, cred, opts, createReq, targetUser, APIError{} +} diff --git a/cmd/admin-router.go b/cmd/admin-router.go index ad5b3c27d..7af68196f 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -300,6 +300,12 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) { adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(adminMiddleware(adminAPI.GetIdentityProviderCfg)) adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(adminMiddleware(adminAPI.DeleteIdentityProviderCfg)) + // LDAP specific service accounts ops + adminRouter.Methods(http.MethodPut).Path(adminVersion + "/idp/ldap/add-service-account").HandlerFunc(adminMiddleware(adminAPI.AddServiceAccountLDAP)) + adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/ldap/list-access-keys"). + HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAP)). + Queries("userDN", "{userDN:.*}", "listType", "{listType:.*}") + // LDAP IAM operations adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(adminMiddleware(adminAPI.ListLDAPPolicyMappingEntities)) adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/ldap/policy/{operation}").HandlerFunc(adminMiddleware(adminAPI.AttachDetachPolicyLDAP)) diff --git a/cmd/iam-store.go b/cmd/iam-store.go index fe0e16bbd..6f415d345 100644 --- a/cmd/iam-store.go +++ b/cmd/iam-store.go @@ -2319,6 +2319,27 @@ func (store *IAMStoreSys) ListServiceAccounts(ctx context.Context, accessKey str return serviceAccounts, nil } +// ListSTSAccounts - lists only STS accounts from the cache. +func (store *IAMStoreSys) ListSTSAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) { + cache := store.rlock() + defer store.runlock() + + var stsAccounts []auth.Credentials + for _, u := range cache.iamSTSAccountsMap { + v := u.Credentials + if accessKey != "" && v.ParentUser == accessKey { + if v.IsTemp() { + // Hide secret key & session key here + v.SecretKey = "" + v.SessionToken = "" + stsAccounts = append(stsAccounts, v) + } + } + } + + return stsAccounts, nil +} + // AddUser - adds/updates long term user account to storage. func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) (updatedAt time.Time, err error) { cache := store.lock() diff --git a/cmd/iam.go b/cmd/iam.go index 5030f686d..cb212fea5 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -1041,7 +1041,7 @@ func (sys *IAMSys) UpdateServiceAccount(ctx context.Context, accessKey string, o return updatedAt, nil } -// ListServiceAccounts - lists all services accounts associated to a specific user +// ListServiceAccounts - lists all service accounts associated to a specific user func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) { if !sys.Initialized() { return nil, errServerNotInitialized @@ -1055,7 +1055,7 @@ func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([ } } -// ListTempAccounts - lists all services accounts associated to a specific user +// ListTempAccounts - lists all temporary service accounts associated to a specific user func (sys *IAMSys) ListTempAccounts(ctx context.Context, accessKey string) ([]UserIdentity, error) { if !sys.Initialized() { return nil, errServerNotInitialized @@ -1069,6 +1069,20 @@ func (sys *IAMSys) ListTempAccounts(ctx context.Context, accessKey string) ([]Us } } +// ListSTSAccounts - lists all STS accounts associated to a specific user +func (sys *IAMSys) ListSTSAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) { + if !sys.Initialized() { + return nil, errServerNotInitialized + } + + select { + case <-sys.configLoaded: + return sys.store.ListSTSAccounts(ctx, accessKey) + case <-ctx.Done(): + return nil, ctx.Err() + } +} + // GetServiceAccount - wrapper method to get information about a service account func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (auth.Credentials, *policy.Policy, error) { sa, embeddedPolicy, err := sys.getServiceAccount(ctx, accessKey)