diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index ad2314cf7..e7537a70f 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -471,18 +471,12 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque return } - cred, _, owner, s3Err := validateAdminSignature(ctx, r, "") + cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "") if s3Err != ErrNone { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) return } - // Disallow creating service accounts by root user. - if owner { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL) - return - } - password := cred.SecretKey reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) if err != nil { @@ -496,12 +490,55 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque return } - parentUser := cred.AccessKey - if cred.ParentUser != "" { - parentUser = cred.ParentUser + // Disallow creating service accounts by root user. + if createReq.TargetUser == globalActiveCred.AccessKey { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL) + return } - newCred, err := globalIAMSys.NewServiceAccount(ctx, parentUser, cred.Groups, createReq.Policy) + var ( + targetUser string + targetGroups []string + ) + + targetUser = createReq.TargetUser + + // Need permission if we are creating a service acccount + // for a user <> to the request sender + if targetUser != "" && targetUser != cred.AccessKey { + if !globalIAMSys.IsAllowed(iampolicy.Args{ + AccountName: cred.AccessKey, + Action: iampolicy.CreateServiceAccountAdminAction, + ConditionValues: getConditionValues(r, "", cred.AccessKey, claims), + IsOwner: owner, + Claims: claims, + }) { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + } + + if globalLDAPConfig.Enabled && targetUser != "" { + // If LDAP enabled, service accounts need + // to be created only for LDAP users. + var err error + _, targetGroups, err = globalLDAPConfig.LookupUserDN(targetUser) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + } else { + if targetUser == "" { + targetUser = cred.AccessKey + } + if cred.ParentUser != "" { + targetUser = cred.ParentUser + } + targetGroups = cred.Groups + } + + opts := newServiceAccountOpts{sessionPolicy: createReq.Policy, accessKey: createReq.AccessKey, secretKey: createReq.SecretKey} + newCred, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return @@ -537,6 +574,191 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque writeSuccessResponseJSON(w, encryptedData) } +// UpdateServiceAccount - POST /minio/admin/v3/update-service-account +func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "UpdateServiceAccount") + + defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) + + // Get current object layer instance. + objectAPI := newObjectLayerFn() + if objectAPI == nil || globalNotificationSys == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } + + accessKey := mux.Vars(r)["accessKey"] + if accessKey == "" { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) + return + } + + // Disallow editing service accounts by root user. + if owner { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL) + return + } + + svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, accessKey) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + if !globalIAMSys.IsAllowed(iampolicy.Args{ + AccountName: cred.AccessKey, + Action: iampolicy.UpdateServiceAccountAdminAction, + ConditionValues: getConditionValues(r, "", cred.AccessKey, claims), + IsOwner: owner, + Claims: claims, + }) { + requestUser := cred.AccessKey + if cred.ParentUser != "" { + requestUser = cred.ParentUser + } + + if requestUser != svcAccount.ParentUser { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), 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 updateReq madmin.UpdateServiceAccountReq + if err = json.Unmarshal(reqBytes, &updateReq); err != nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) + return + } + + opts := updateServiceAccountOpts{sessionPolicy: updateReq.NewPolicy, secretKey: updateReq.NewSecretKey, status: updateReq.NewStatus} + err = globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Notify all other Minio peers to reload user the service account + for _, nerr := range globalNotificationSys.LoadServiceAccount(accessKey) { + if nerr.Err != nil { + logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) + logger.LogIf(ctx, nerr.Err) + } + } + + writeSuccessNoContent(w) +} + +// InfoServiceAccount - GET /minio/admin/v3/info-service-account +func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "InfoServiceAccount") + + defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) + + // Get current object layer instance. + objectAPI := newObjectLayerFn() + if objectAPI == nil || globalNotificationSys == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } + + // Disallow creating service accounts by root user. + if owner { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL) + return + } + + accessKey := mux.Vars(r)["accessKey"] + if accessKey == "" { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) + return + } + + svcAccount, policy, err := globalIAMSys.GetServiceAccount(ctx, accessKey) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + if !globalIAMSys.IsAllowed(iampolicy.Args{ + AccountName: cred.AccessKey, + Action: iampolicy.ListServiceAccountsAdminAction, + ConditionValues: getConditionValues(r, "", cred.AccessKey, claims), + IsOwner: owner, + Claims: claims, + }) { + requestUser := cred.AccessKey + if cred.ParentUser != "" { + requestUser = cred.ParentUser + } + + if requestUser != svcAccount.ParentUser { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + } + + var svcAccountPolicy iampolicy.Policy + + impliedPolicy := policy == nil + + // If policy is empty, check for policy of the parent user + if !impliedPolicy { + svcAccountPolicy.Merge(*policy) + } else { + policiesNames, err := globalIAMSys.PolicyDBGet(svcAccount.AccessKey, false) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + svcAccountPolicy.Merge(globalIAMSys.GetCombinedPolicy(policiesNames...)) + } + + policyJSON, err := json.Marshal(svcAccountPolicy) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + var infoResp = madmin.InfoServiceAccountResp{ + ParentUser: svcAccount.ParentUser, + AccountStatus: svcAccount.Status, + ImpliedPolicy: impliedPolicy, + Policy: string(policyJSON), + } + + data, err := json.Marshal(infoResp) + 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) +} + // ListServiceAccounts - GET /minio/admin/v3/list-service-accounts func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "ListServiceAccounts") @@ -550,7 +772,7 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req return } - cred, _, owner, s3Err := validateAdminSignature(ctx, r, "") + cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "") if s3Err != ErrNone { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) return @@ -562,19 +784,42 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req return } - parentUser := cred.AccessKey - if cred.ParentUser != "" { - parentUser = cred.ParentUser + var targetAccount string + + user := r.URL.Query().Get("user") + if user != "" { + if !globalIAMSys.IsAllowed(iampolicy.Args{ + AccountName: cred.AccessKey, + Action: iampolicy.ListServiceAccountsAdminAction, + ConditionValues: getConditionValues(r, "", cred.AccessKey, claims), + IsOwner: owner, + Claims: claims, + }) { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL) + return + } + targetAccount = user + } else { + targetAccount = cred.AccessKey + if cred.ParentUser != "" { + targetAccount = cred.ParentUser + } } - serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, parentUser) + serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, targetAccount) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } + var serviceAccountsNames []string + + for _, svc := range serviceAccounts { + serviceAccountsNames = append(serviceAccountsNames, svc.AccessKey) + } + var listResp = madmin.ListServiceAccountsResp{ - Accounts: serviceAccounts, + Accounts: serviceAccountsNames, } data, err := json.Marshal(listResp) @@ -605,7 +850,7 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re return } - cred, _, owner, s3Err := validateAdminSignature(ctx, r, "") + cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "") if s3Err != ErrNone { writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) return @@ -623,23 +868,32 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re return } - user, err := globalIAMSys.GetServiceAccountParent(ctx, serviceAccount) + svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, serviceAccount) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } - parentUser := cred.AccessKey - if cred.ParentUser != "" { - parentUser = cred.ParentUser - } + adminPrivilege := globalIAMSys.IsAllowed(iampolicy.Args{ + AccountName: cred.AccessKey, + Action: iampolicy.RemoveServiceAccountAdminAction, + ConditionValues: getConditionValues(r, "", cred.AccessKey, claims), + IsOwner: owner, + Claims: claims, + }) - if parentUser != user || user == "" { - // The service account belongs to another user but return not - // found error to mitigate brute force attacks. or the - // serviceAccount doesn't exist. - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServiceAccountNotFound), r.URL) - return + if !adminPrivilege { + parentUser := cred.AccessKey + if cred.ParentUser != "" { + parentUser = cred.ParentUser + } + if parentUser != svcAccount.ParentUser { + // The service account belongs to another user but return not + // found error to mitigate brute force attacks. or the + // serviceAccount doesn't exist. + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminServiceAccountNotFound), r.URL) + return + } } err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount) diff --git a/cmd/admin-router.go b/cmd/admin-router.go index d262a7775..f472e74a5 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -121,6 +121,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) // Service accounts ops adminRouter.Methods(http.MethodPut).Path(adminVersion + "/add-service-account").HandlerFunc(httpTraceHdrs(adminAPI.AddServiceAccount)) + adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update-service-account").HandlerFunc(httpTraceHdrs(adminAPI.UpdateServiceAccount)).Queries("accessKey", "{accessKey:.*}") + adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-service-account").HandlerFunc(httpTraceHdrs(adminAPI.InfoServiceAccount)).Queries("accessKey", "{accessKey:.*}") adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(httpTraceHdrs(adminAPI.ListServiceAccounts)) adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(httpTraceHdrs(adminAPI.DeleteServiceAccount)).Queries("accessKey", "{accessKey:.*}") diff --git a/cmd/api-errors.go b/cmd/api-errors.go index 06c3c22ea..4cc8466ab 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -367,7 +367,7 @@ const ( ErrAddUserInvalidArgument ErrAdminAccountNotEligible ErrAccountNotEligible - ErrServiceAccountNotFound + ErrAdminServiceAccountNotFound ErrPostPolicyConditionInvalidFormat ) @@ -1754,7 +1754,7 @@ var errorCodes = errorCodeMap{ Description: "The account key is not eligible for this operation", HTTPStatusCode: http.StatusForbidden, }, - ErrServiceAccountNotFound: { + ErrAdminServiceAccountNotFound: { Code: "XMinioInvalidIAMCredentials", Description: "The specified service account is not found", HTTPStatusCode: http.StatusNotFound, @@ -1790,6 +1790,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) { apiErr = ErrAdminInvalidArgument case errNoSuchUser: apiErr = ErrAdminNoSuchUser + case errNoSuchServiceAccount: + apiErr = ErrAdminServiceAccountNotFound case errNoSuchGroup: apiErr = ErrAdminNoSuchGroup case errGroupNotEmpty: diff --git a/cmd/apierrorcode_string.go b/cmd/apierrorcode_string.go index 712835a72..4004c7148 100644 --- a/cmd/apierrorcode_string.go +++ b/cmd/apierrorcode_string.go @@ -281,13 +281,13 @@ func _() { _ = x[ErrAddUserInvalidArgument-270] _ = x[ErrAdminAccountNotEligible-271] _ = x[ErrAccountNotEligible-272] - _ = x[ErrServiceAccountNotFound-273] + _ = x[ErrAdminServiceAccountNotFound-273] _ = x[ErrPostPolicyConditionInvalidFormat-274] } -const _APIErrorCode_name = "NoneAccessDeniedBadDigestEntityTooSmallEntityTooLargePolicyTooLargeIncompleteBodyInternalErrorInvalidAccessKeyIDInvalidBucketNameInvalidDigestInvalidRangeInvalidRangePartNumberInvalidCopyPartRangeInvalidCopyPartRangeSourceInvalidMaxKeysInvalidEncodingMethodInvalidMaxUploadsInvalidMaxPartsInvalidPartNumberMarkerInvalidPartNumberInvalidRequestBodyInvalidCopySourceInvalidMetadataDirectiveInvalidCopyDestInvalidPolicyDocumentInvalidObjectStateMalformedXMLMissingContentLengthMissingContentMD5MissingRequestBodyErrorMissingSecurityHeaderNoSuchBucketNoSuchBucketPolicyNoSuchBucketLifecycleNoSuchLifecycleConfigurationNoSuchBucketSSEConfigNoSuchCORSConfigurationNoSuchWebsiteConfigurationReplicationConfigurationNotFoundErrorRemoteDestinationNotFoundErrorReplicationDestinationMissingLockRemoteTargetNotFoundErrorReplicationRemoteConnectionErrorBucketRemoteIdenticalToSourceBucketRemoteAlreadyExistsBucketRemoteLabelInUseBucketRemoteArnTypeInvalidBucketRemoteArnInvalidBucketRemoteRemoveDisallowedRemoteTargetNotVersionedErrorReplicationSourceNotVersionedErrorReplicationNeedsVersioningErrorReplicationBucketNeedsVersioningErrorObjectRestoreAlreadyInProgressNoSuchKeyNoSuchUploadInvalidVersionIDNoSuchVersionNotImplementedPreconditionFailedRequestTimeTooSkewedSignatureDoesNotMatchMethodNotAllowedInvalidPartInvalidPartOrderAuthorizationHeaderMalformedMalformedPOSTRequestPOSTFileRequiredSignatureVersionNotSupportedBucketNotEmptyAllAccessDisabledMalformedPolicyMissingFieldsMissingCredTagCredMalformedInvalidRegionInvalidServiceS3InvalidServiceSTSInvalidRequestVersionMissingSignTagMissingSignHeadersTagMalformedDateMalformedPresignedDateMalformedCredentialDateMalformedCredentialRegionMalformedExpiresNegativeExpiresAuthHeaderEmptyExpiredPresignRequestRequestNotReadyYetUnsignedHeadersMissingDateHeaderInvalidQuerySignatureAlgoInvalidQueryParamsBucketAlreadyOwnedByYouInvalidDurationBucketAlreadyExistsMetadataTooLargeUnsupportedMetadataMaximumExpiresSlowDownInvalidPrefixMarkerBadRequestKeyTooLongErrorInvalidBucketObjectLockConfigurationObjectLockConfigurationNotFoundObjectLockConfigurationNotAllowedNoSuchObjectLockConfigurationObjectLockedInvalidRetentionDatePastObjectLockRetainDateUnknownWORMModeDirectiveBucketTaggingNotFoundObjectLockInvalidHeadersInvalidTagDirectiveInvalidEncryptionMethodInsecureSSECustomerRequestSSEMultipartEncryptedSSEEncryptedObjectInvalidEncryptionParametersInvalidSSECustomerAlgorithmInvalidSSECustomerKeyMissingSSECustomerKeyMissingSSECustomerKeyMD5SSECustomerKeyMD5MismatchInvalidSSECustomerParametersIncompatibleEncryptionMethodKMSNotConfiguredKMSAuthFailureNoAccessKeyInvalidTokenEventNotificationARNNotificationRegionNotificationOverlappingFilterNotificationFilterNameInvalidFilterNamePrefixFilterNameSuffixFilterValueInvalidOverlappingConfigsUnsupportedNotificationContentSHA256MismatchReadQuorumWriteQuorumParentIsObjectStorageFullRequestBodyParseObjectExistsAsDirectoryInvalidObjectNameInvalidObjectNamePrefixSlashInvalidResourceNameServerNotInitializedOperationTimedOutClientDisconnectedOperationMaxedOutInvalidRequestInvalidStorageClassBackendDownMalformedJSONAdminNoSuchUserAdminNoSuchGroupAdminGroupNotEmptyAdminNoSuchPolicyAdminInvalidArgumentAdminInvalidAccessKeyAdminInvalidSecretKeyAdminConfigNoQuorumAdminConfigTooLargeAdminConfigBadJSONAdminConfigDuplicateKeysAdminCredentialsMismatchInsecureClientRequestObjectTamperedAdminBucketQuotaExceededAdminNoSuchQuotaConfigurationHealNotImplementedHealNoSuchProcessHealInvalidClientTokenHealMissingBucketHealAlreadyRunningHealOverlappingPathsIncorrectContinuationTokenEmptyRequestBodyUnsupportedFunctionInvalidExpressionTypeBusyUnauthorizedAccessExpressionTooLongIllegalSQLFunctionArgumentInvalidKeyPathInvalidCompressionFormatInvalidFileHeaderInfoInvalidJSONTypeInvalidQuoteFieldsInvalidRequestParameterInvalidDataTypeInvalidTextEncodingInvalidDataSourceInvalidTableAliasMissingRequiredParameterObjectSerializationConflictUnsupportedSQLOperationUnsupportedSQLStructureUnsupportedSyntaxUnsupportedRangeHeaderLexerInvalidCharLexerInvalidOperatorLexerInvalidLiteralLexerInvalidIONLiteralParseExpectedDatePartParseExpectedKeywordParseExpectedTokenTypeParseExpected2TokenTypesParseExpectedNumberParseExpectedRightParenBuiltinFunctionCallParseExpectedTypeNameParseExpectedWhenClauseParseUnsupportedTokenParseUnsupportedLiteralsGroupByParseExpectedMemberParseUnsupportedSelectParseUnsupportedCaseParseUnsupportedCaseClauseParseUnsupportedAliasParseUnsupportedSyntaxParseUnknownOperatorParseMissingIdentAfterAtParseUnexpectedOperatorParseUnexpectedTermParseUnexpectedTokenParseUnexpectedKeywordParseExpectedExpressionParseExpectedLeftParenAfterCastParseExpectedLeftParenValueConstructorParseExpectedLeftParenBuiltinFunctionCallParseExpectedArgumentDelimiterParseCastArityParseInvalidTypeParamParseEmptySelectParseSelectMissingFromParseExpectedIdentForGroupNameParseExpectedIdentForAliasParseUnsupportedCallWithStarParseNonUnaryAgregateFunctionCallParseMalformedJoinParseExpectedIdentForAtParseAsteriskIsNotAloneInSelectListParseCannotMixSqbAndWildcardInSelectListParseInvalidContextForWildcardInSelectListIncorrectSQLFunctionArgumentTypeValueParseFailureEvaluatorInvalidArgumentsIntegerOverflowLikeInvalidInputsCastFailedInvalidCastEvaluatorInvalidTimestampFormatPatternEvaluatorInvalidTimestampFormatPatternSymbolForParsingEvaluatorTimestampFormatPatternDuplicateFieldsEvaluatorTimestampFormatPatternHourClockAmPmMismatchEvaluatorUnterminatedTimestampFormatPatternTokenEvaluatorInvalidTimestampFormatPatternTokenEvaluatorInvalidTimestampFormatPatternSymbolEvaluatorBindingDoesNotExistMissingHeadersInvalidColumnIndexAdminConfigNotificationTargetsFailedAdminProfilerNotEnabledInvalidDecompressedSizeAddUserInvalidArgumentAdminAccountNotEligibleAccountNotEligibleServiceAccountNotFoundPostPolicyConditionInvalidFormat" +const _APIErrorCode_name = "NoneAccessDeniedBadDigestEntityTooSmallEntityTooLargePolicyTooLargeIncompleteBodyInternalErrorInvalidAccessKeyIDInvalidBucketNameInvalidDigestInvalidRangeInvalidRangePartNumberInvalidCopyPartRangeInvalidCopyPartRangeSourceInvalidMaxKeysInvalidEncodingMethodInvalidMaxUploadsInvalidMaxPartsInvalidPartNumberMarkerInvalidPartNumberInvalidRequestBodyInvalidCopySourceInvalidMetadataDirectiveInvalidCopyDestInvalidPolicyDocumentInvalidObjectStateMalformedXMLMissingContentLengthMissingContentMD5MissingRequestBodyErrorMissingSecurityHeaderNoSuchBucketNoSuchBucketPolicyNoSuchBucketLifecycleNoSuchLifecycleConfigurationNoSuchBucketSSEConfigNoSuchCORSConfigurationNoSuchWebsiteConfigurationReplicationConfigurationNotFoundErrorRemoteDestinationNotFoundErrorReplicationDestinationMissingLockRemoteTargetNotFoundErrorReplicationRemoteConnectionErrorBucketRemoteIdenticalToSourceBucketRemoteAlreadyExistsBucketRemoteLabelInUseBucketRemoteArnTypeInvalidBucketRemoteArnInvalidBucketRemoteRemoveDisallowedRemoteTargetNotVersionedErrorReplicationSourceNotVersionedErrorReplicationNeedsVersioningErrorReplicationBucketNeedsVersioningErrorObjectRestoreAlreadyInProgressNoSuchKeyNoSuchUploadInvalidVersionIDNoSuchVersionNotImplementedPreconditionFailedRequestTimeTooSkewedSignatureDoesNotMatchMethodNotAllowedInvalidPartInvalidPartOrderAuthorizationHeaderMalformedMalformedPOSTRequestPOSTFileRequiredSignatureVersionNotSupportedBucketNotEmptyAllAccessDisabledMalformedPolicyMissingFieldsMissingCredTagCredMalformedInvalidRegionInvalidServiceS3InvalidServiceSTSInvalidRequestVersionMissingSignTagMissingSignHeadersTagMalformedDateMalformedPresignedDateMalformedCredentialDateMalformedCredentialRegionMalformedExpiresNegativeExpiresAuthHeaderEmptyExpiredPresignRequestRequestNotReadyYetUnsignedHeadersMissingDateHeaderInvalidQuerySignatureAlgoInvalidQueryParamsBucketAlreadyOwnedByYouInvalidDurationBucketAlreadyExistsMetadataTooLargeUnsupportedMetadataMaximumExpiresSlowDownInvalidPrefixMarkerBadRequestKeyTooLongErrorInvalidBucketObjectLockConfigurationObjectLockConfigurationNotFoundObjectLockConfigurationNotAllowedNoSuchObjectLockConfigurationObjectLockedInvalidRetentionDatePastObjectLockRetainDateUnknownWORMModeDirectiveBucketTaggingNotFoundObjectLockInvalidHeadersInvalidTagDirectiveInvalidEncryptionMethodInsecureSSECustomerRequestSSEMultipartEncryptedSSEEncryptedObjectInvalidEncryptionParametersInvalidSSECustomerAlgorithmInvalidSSECustomerKeyMissingSSECustomerKeyMissingSSECustomerKeyMD5SSECustomerKeyMD5MismatchInvalidSSECustomerParametersIncompatibleEncryptionMethodKMSNotConfiguredKMSAuthFailureNoAccessKeyInvalidTokenEventNotificationARNNotificationRegionNotificationOverlappingFilterNotificationFilterNameInvalidFilterNamePrefixFilterNameSuffixFilterValueInvalidOverlappingConfigsUnsupportedNotificationContentSHA256MismatchReadQuorumWriteQuorumParentIsObjectStorageFullRequestBodyParseObjectExistsAsDirectoryInvalidObjectNameInvalidObjectNamePrefixSlashInvalidResourceNameServerNotInitializedOperationTimedOutClientDisconnectedOperationMaxedOutInvalidRequestInvalidStorageClassBackendDownMalformedJSONAdminNoSuchUserAdminNoSuchGroupAdminGroupNotEmptyAdminNoSuchPolicyAdminInvalidArgumentAdminInvalidAccessKeyAdminInvalidSecretKeyAdminConfigNoQuorumAdminConfigTooLargeAdminConfigBadJSONAdminConfigDuplicateKeysAdminCredentialsMismatchInsecureClientRequestObjectTamperedAdminBucketQuotaExceededAdminNoSuchQuotaConfigurationHealNotImplementedHealNoSuchProcessHealInvalidClientTokenHealMissingBucketHealAlreadyRunningHealOverlappingPathsIncorrectContinuationTokenEmptyRequestBodyUnsupportedFunctionInvalidExpressionTypeBusyUnauthorizedAccessExpressionTooLongIllegalSQLFunctionArgumentInvalidKeyPathInvalidCompressionFormatInvalidFileHeaderInfoInvalidJSONTypeInvalidQuoteFieldsInvalidRequestParameterInvalidDataTypeInvalidTextEncodingInvalidDataSourceInvalidTableAliasMissingRequiredParameterObjectSerializationConflictUnsupportedSQLOperationUnsupportedSQLStructureUnsupportedSyntaxUnsupportedRangeHeaderLexerInvalidCharLexerInvalidOperatorLexerInvalidLiteralLexerInvalidIONLiteralParseExpectedDatePartParseExpectedKeywordParseExpectedTokenTypeParseExpected2TokenTypesParseExpectedNumberParseExpectedRightParenBuiltinFunctionCallParseExpectedTypeNameParseExpectedWhenClauseParseUnsupportedTokenParseUnsupportedLiteralsGroupByParseExpectedMemberParseUnsupportedSelectParseUnsupportedCaseParseUnsupportedCaseClauseParseUnsupportedAliasParseUnsupportedSyntaxParseUnknownOperatorParseMissingIdentAfterAtParseUnexpectedOperatorParseUnexpectedTermParseUnexpectedTokenParseUnexpectedKeywordParseExpectedExpressionParseExpectedLeftParenAfterCastParseExpectedLeftParenValueConstructorParseExpectedLeftParenBuiltinFunctionCallParseExpectedArgumentDelimiterParseCastArityParseInvalidTypeParamParseEmptySelectParseSelectMissingFromParseExpectedIdentForGroupNameParseExpectedIdentForAliasParseUnsupportedCallWithStarParseNonUnaryAgregateFunctionCallParseMalformedJoinParseExpectedIdentForAtParseAsteriskIsNotAloneInSelectListParseCannotMixSqbAndWildcardInSelectListParseInvalidContextForWildcardInSelectListIncorrectSQLFunctionArgumentTypeValueParseFailureEvaluatorInvalidArgumentsIntegerOverflowLikeInvalidInputsCastFailedInvalidCastEvaluatorInvalidTimestampFormatPatternEvaluatorInvalidTimestampFormatPatternSymbolForParsingEvaluatorTimestampFormatPatternDuplicateFieldsEvaluatorTimestampFormatPatternHourClockAmPmMismatchEvaluatorUnterminatedTimestampFormatPatternTokenEvaluatorInvalidTimestampFormatPatternTokenEvaluatorInvalidTimestampFormatPatternSymbolEvaluatorBindingDoesNotExistMissingHeadersInvalidColumnIndexAdminConfigNotificationTargetsFailedAdminProfilerNotEnabledInvalidDecompressedSizeAddUserInvalidArgumentAdminAccountNotEligibleAccountNotEligibleAdminServiceAccountNotFoundPostPolicyConditionInvalidFormat" -var _APIErrorCode_index = [...]uint16{0, 4, 16, 25, 39, 53, 67, 81, 94, 112, 129, 142, 154, 176, 196, 222, 236, 257, 274, 289, 312, 329, 347, 364, 388, 403, 424, 442, 454, 474, 491, 514, 535, 547, 565, 586, 614, 635, 658, 684, 721, 751, 784, 809, 841, 870, 895, 917, 943, 965, 993, 1022, 1056, 1087, 1124, 1154, 1163, 1175, 1191, 1204, 1218, 1236, 1256, 1277, 1293, 1304, 1320, 1348, 1368, 1384, 1412, 1426, 1443, 1458, 1471, 1485, 1498, 1511, 1527, 1544, 1565, 1579, 1600, 1613, 1635, 1658, 1683, 1699, 1714, 1729, 1750, 1768, 1783, 1800, 1825, 1843, 1866, 1881, 1900, 1916, 1935, 1949, 1957, 1976, 1986, 2001, 2037, 2068, 2101, 2130, 2142, 2162, 2186, 2210, 2231, 2255, 2274, 2297, 2323, 2344, 2362, 2389, 2416, 2437, 2458, 2482, 2507, 2535, 2563, 2579, 2593, 2604, 2616, 2633, 2648, 2666, 2695, 2712, 2728, 2744, 2762, 2780, 2803, 2824, 2834, 2845, 2859, 2870, 2886, 2909, 2926, 2954, 2973, 2993, 3010, 3028, 3045, 3059, 3078, 3089, 3102, 3117, 3133, 3151, 3168, 3188, 3209, 3230, 3249, 3268, 3286, 3310, 3334, 3355, 3369, 3393, 3422, 3440, 3457, 3479, 3496, 3514, 3534, 3560, 3576, 3595, 3616, 3620, 3638, 3655, 3681, 3695, 3719, 3740, 3755, 3773, 3796, 3811, 3830, 3847, 3864, 3888, 3915, 3938, 3961, 3978, 4000, 4016, 4036, 4055, 4077, 4098, 4118, 4140, 4164, 4183, 4225, 4246, 4269, 4290, 4321, 4340, 4362, 4382, 4408, 4429, 4451, 4471, 4495, 4518, 4537, 4557, 4579, 4602, 4633, 4671, 4712, 4742, 4756, 4777, 4793, 4815, 4845, 4871, 4899, 4932, 4950, 4973, 5008, 5048, 5090, 5122, 5139, 5164, 5179, 5196, 5206, 5217, 5255, 5309, 5355, 5407, 5455, 5498, 5542, 5570, 5584, 5602, 5638, 5661, 5684, 5706, 5729, 5747, 5769, 5801} +var _APIErrorCode_index = [...]uint16{0, 4, 16, 25, 39, 53, 67, 81, 94, 112, 129, 142, 154, 176, 196, 222, 236, 257, 274, 289, 312, 329, 347, 364, 388, 403, 424, 442, 454, 474, 491, 514, 535, 547, 565, 586, 614, 635, 658, 684, 721, 751, 784, 809, 841, 870, 895, 917, 943, 965, 993, 1022, 1056, 1087, 1124, 1154, 1163, 1175, 1191, 1204, 1218, 1236, 1256, 1277, 1293, 1304, 1320, 1348, 1368, 1384, 1412, 1426, 1443, 1458, 1471, 1485, 1498, 1511, 1527, 1544, 1565, 1579, 1600, 1613, 1635, 1658, 1683, 1699, 1714, 1729, 1750, 1768, 1783, 1800, 1825, 1843, 1866, 1881, 1900, 1916, 1935, 1949, 1957, 1976, 1986, 2001, 2037, 2068, 2101, 2130, 2142, 2162, 2186, 2210, 2231, 2255, 2274, 2297, 2323, 2344, 2362, 2389, 2416, 2437, 2458, 2482, 2507, 2535, 2563, 2579, 2593, 2604, 2616, 2633, 2648, 2666, 2695, 2712, 2728, 2744, 2762, 2780, 2803, 2824, 2834, 2845, 2859, 2870, 2886, 2909, 2926, 2954, 2973, 2993, 3010, 3028, 3045, 3059, 3078, 3089, 3102, 3117, 3133, 3151, 3168, 3188, 3209, 3230, 3249, 3268, 3286, 3310, 3334, 3355, 3369, 3393, 3422, 3440, 3457, 3479, 3496, 3514, 3534, 3560, 3576, 3595, 3616, 3620, 3638, 3655, 3681, 3695, 3719, 3740, 3755, 3773, 3796, 3811, 3830, 3847, 3864, 3888, 3915, 3938, 3961, 3978, 4000, 4016, 4036, 4055, 4077, 4098, 4118, 4140, 4164, 4183, 4225, 4246, 4269, 4290, 4321, 4340, 4362, 4382, 4408, 4429, 4451, 4471, 4495, 4518, 4537, 4557, 4579, 4602, 4633, 4671, 4712, 4742, 4756, 4777, 4793, 4815, 4845, 4871, 4899, 4932, 4950, 4973, 5008, 5048, 5090, 5122, 5139, 5164, 5179, 5196, 5206, 5217, 5255, 5309, 5355, 5407, 5455, 5498, 5542, 5570, 5584, 5602, 5638, 5661, 5684, 5706, 5729, 5747, 5774, 5806} func (i APIErrorCode) String() string { if i < 0 || i >= APIErrorCode(len(_APIErrorCode_index)-1) { diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go index d7112cc08..aff3ce613 100644 --- a/cmd/auth-handler.go +++ b/cmd/auth-handler.go @@ -194,24 +194,21 @@ func mustGetClaimsFromToken(r *http.Request) map[string]interface{} { // Fetch claims in the security token returned by the client. func getClaimsFromToken(token string) (map[string]interface{}, error) { - claims := xjwt.NewMapClaims() if token == "" { + claims := xjwt.NewMapClaims() return claims.Map(), nil } - stsTokenCallback := func(claims *xjwt.MapClaims) ([]byte, error) { - // JWT token for x-amz-security-token is signed with admin - // secret key, temporary credentials become invalid if - // server admin credentials change. This is done to ensure - // that clients cannot decode the token using the temp - // secret keys and generate an entirely new claim by essentially - // hijacking the policies. We need to make sure that this is - // based an admin credential such that token cannot be decoded - // on the client side and is treated like an opaque value. - return []byte(globalActiveCred.SecretKey), nil - } - - if err := xjwt.ParseWithClaims(token, claims, stsTokenCallback); err != nil { + // JWT token for x-amz-security-token is signed with admin + // secret key, temporary credentials become invalid if + // server admin credentials change. This is done to ensure + // that clients cannot decode the token using the temp + // secret keys and generate an entirely new claim by essentially + // hijacking the policies. We need to make sure that this is + // based an admin credential such that token cannot be decoded + // on the client side and is treated like an opaque value. + claims, err := auth.ExtractClaims(token, globalActiveCred.SecretKey) + if err != nil { return nil, errAuthentication } diff --git a/cmd/config/identity/ldap/config.go b/cmd/config/identity/ldap/config.go index dd5a87408..258593375 100644 --- a/cmd/config/identity/ldap/config.go +++ b/cmd/config/identity/ldap/config.go @@ -260,6 +260,67 @@ func (l *Config) lookupUserDN(conn *ldap.Conn, username string) (string, error) return searchResult.Entries[0].DN, nil } +func (l *Config) searchForUserGroups(conn *ldap.Conn, username, bindDN string) ([]string, error) { + // User groups lookup. + var groups []string + if l.GroupSearchFilter != "" { + for _, groupSearchBase := range l.GroupSearchBaseDistNames { + filter := strings.Replace(l.GroupSearchFilter, "%s", ldap.EscapeFilter(username), -1) + filter = strings.Replace(filter, "%d", ldap.EscapeFilter(bindDN), -1) + searchRequest := ldap.NewSearchRequest( + groupSearchBase, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + filter, + nil, + nil, + ) + + var newGroups []string + newGroups, err := getGroups(conn, searchRequest) + if err != nil { + errRet := fmt.Errorf("Error finding groups of %s: %v", bindDN, err) + return nil, errRet + } + + groups = append(groups, newGroups...) + } + } + + return groups, nil +} + +// LookupUserDN searches for the full DN ang groups of a given username +func (l *Config) LookupUserDN(username string) (string, []string, error) { + if !l.isUsingLookupBind { + return "", nil, errors.New("current lookup mode does not support searching for User DN") + } + + conn, err := l.Connect() + if err != nil { + return "", nil, err + } + defer conn.Close() + + // Bind to the lookup user account + if err = l.lookupBind(conn); err != nil { + return "", nil, err + } + + // Lookup user DN + bindDN, err := l.lookupUserDN(conn, username) + if err != nil { + errRet := fmt.Errorf("Unable to find user DN: %w", err) + return "", nil, errRet + } + + groups, err := l.searchForUserGroups(conn, username, bindDN) + if err != nil { + return "", nil, err + } + + return bindDN, groups, nil +} + // Bind - binds to ldap, searches LDAP and returns the distinguished name of the // user and the list of groups. func (l *Config) Bind(username, password string) (string, []string, error) { @@ -310,28 +371,9 @@ func (l *Config) Bind(username, password string) (string, []string, error) { } // User groups lookup. - var groups []string - if l.GroupSearchFilter != "" { - for _, groupSearchBase := range l.GroupSearchBaseDistNames { - filter := strings.Replace(l.GroupSearchFilter, "%s", ldap.EscapeFilter(username), -1) - filter = strings.Replace(filter, "%d", ldap.EscapeFilter(bindDN), -1) - searchRequest := ldap.NewSearchRequest( - groupSearchBase, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - filter, - nil, - nil, - ) - - var newGroups []string - newGroups, err = getGroups(conn, searchRequest) - if err != nil { - errRet := fmt.Errorf("Error finding groups of %s: %v", bindDN, err) - return "", nil, errRet - } - - groups = append(groups, newGroups...) - } + groups, err := l.searchForUserGroups(conn, username, bindDN) + if err != nil { + return "", nil, err } return bindDN, groups, nil diff --git a/cmd/iam.go b/cmd/iam.go index 5a663d7c4..b4299b08d 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -1050,19 +1050,25 @@ func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus) return nil } +type newServiceAccountOpts struct { + sessionPolicy *iampolicy.Policy + accessKey string + secretKey string +} + // NewServiceAccount - create a new service account -func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, groups []string, sessionPolicy *iampolicy.Policy) (auth.Credentials, error) { +func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, groups []string, opts newServiceAccountOpts) (auth.Credentials, error) { if !sys.Initialized() { return auth.Credentials{}, errServerNotInitialized } var policyBuf []byte - if sessionPolicy != nil { - err := sessionPolicy.Validate() + if opts.sessionPolicy != nil { + err := opts.sessionPolicy.Validate() if err != nil { return auth.Credentials{}, err } - policyBuf, err = json.Marshal(sessionPolicy) + policyBuf, err = json.Marshal(opts.sessionPolicy) if err != nil { return auth.Credentials{}, err } @@ -1115,13 +1121,22 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro m[iamPolicyClaimNameSA()] = "inherited-policy" } - secret := globalActiveCred.SecretKey - cred, err := auth.GetNewCredentialsWithMetadata(m, secret) + var ( + cred auth.Credentials + err error + ) + + if len(opts.accessKey) > 0 { + cred, err = auth.CreateNewCredentialsWithMetadata(opts.accessKey, opts.secretKey, m, globalActiveCred.SecretKey) + } else { + cred, err = auth.GetNewCredentialsWithMetadata(m, globalActiveCred.SecretKey) + } if err != nil { return auth.Credentials{}, err } cred.ParentUser = parentUser cred.Groups = groups + cred.Status = string(madmin.AccountEnabled) u := newUserIdentity(cred) @@ -1134,8 +1149,69 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro return cred, nil } +type updateServiceAccountOpts struct { + sessionPolicy *iampolicy.Policy + secretKey string + status string +} + +// UpdateServiceAccount - edit a service account +func (sys *IAMSys) UpdateServiceAccount(ctx context.Context, accessKey string, opts updateServiceAccountOpts) error { + if !sys.Initialized() { + return errServerNotInitialized + } + + sys.store.lock() + defer sys.store.unlock() + + cr, ok := sys.iamUsersMap[accessKey] + if !ok || !cr.IsServiceAccount() { + return errNoSuchServiceAccount + } + + if opts.secretKey != "" { + cr.SecretKey = opts.secretKey + } + + if opts.status != "" { + cr.Status = opts.status + } + + if opts.sessionPolicy != nil { + m := make(map[string]interface{}) + err := opts.sessionPolicy.Validate() + if err != nil { + return err + } + policyBuf, err := json.Marshal(opts.sessionPolicy) + if err != nil { + return err + } + if len(policyBuf) > 16*humanize.KiByte { + return fmt.Errorf("Session policy should not exceed 16 KiB characters") + } + + m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf) + m[iamPolicyClaimNameSA()] = "embedded-policy" + m[parentClaim] = cr.ParentUser + cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, globalActiveCred.SecretKey) + if err != nil { + return err + } + } + + u := newUserIdentity(cr) + if err := sys.store.saveUserIdentity(context.Background(), u.Credentials.AccessKey, srvAccUser, u); err != nil { + return err + } + + sys.iamUsersMap[u.Credentials.AccessKey] = u.Credentials + + return nil +} + // ListServiceAccounts - lists all services accounts associated to a specific user -func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]string, error) { +func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]auth.Credentials, error) { if !sys.Initialized() { return nil, errServerNotInitialized } @@ -1145,30 +1221,53 @@ func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([ sys.store.rlock() defer sys.store.runlock() - var serviceAccounts []string - for k, v := range sys.iamUsersMap { + var serviceAccounts []auth.Credentials + for _, v := range sys.iamUsersMap { if v.IsServiceAccount() && v.ParentUser == accessKey { - serviceAccounts = append(serviceAccounts, k) + // Hide secret key & session key here + v.SecretKey = "" + v.SessionToken = "" + serviceAccounts = append(serviceAccounts, v) } } return serviceAccounts, nil } -// GetServiceAccountParent - gets information about a service account -func (sys *IAMSys) GetServiceAccountParent(ctx context.Context, accessKey string) (string, error) { +// GetServiceAccount - gets information about a service account +func (sys *IAMSys) GetServiceAccount(ctx context.Context, accessKey string) (auth.Credentials, *iampolicy.Policy, error) { if !sys.Initialized() { - return "", errServerNotInitialized + return auth.Credentials{}, nil, errServerNotInitialized } sys.store.rlock() defer sys.store.runlock() sa, ok := sys.iamUsersMap[accessKey] - if ok && sa.IsServiceAccount() { - return sa.ParentUser, nil + if !ok || !sa.IsServiceAccount() { + return auth.Credentials{}, nil, errNoSuchServiceAccount } - return "", nil + + var embeddedPolicy *iampolicy.Policy + + jwtClaims, err := auth.ExtractClaims(sa.SessionToken, globalActiveCred.SecretKey) + if err == nil { + pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA()) + sp, spok := jwtClaims.Lookup(iampolicy.SessionPolicyName) + if ptok && spok && pt == "embedded-policy" { + p, err := iampolicy.ParseConfig(bytes.NewReader([]byte(sp))) + if err == nil { + embeddedPolicy = &iampolicy.Policy{} + embeddedPolicy.Merge(*p) + } + } + } + + // Hide secret & session keys + sa.SecretKey = "" + sa.SessionToken = "" + + return sa, embeddedPolicy, nil } // DeleteServiceAccount - delete a service account diff --git a/cmd/typed-errors.go b/cmd/typed-errors.go index 443de284f..053dba0d4 100644 --- a/cmd/typed-errors.go +++ b/cmd/typed-errors.go @@ -77,6 +77,9 @@ var errInvalidDecompressedSize = errors.New("Invalid Decompressed Size") // error returned in IAM subsystem when user doesn't exist. var errNoSuchUser = errors.New("Specified user does not exist") +// error returned when service account is not found +var errNoSuchServiceAccount = errors.New("Specified service account does not exist") + // error returned in IAM subsystem when groups doesn't exist. var errNoSuchGroup = errors.New("Specified group does not exist") diff --git a/pkg/auth/credentials.go b/pkg/auth/credentials.go index 4fd135e1e..2752b1adc 100644 --- a/pkg/auth/credentials.go +++ b/pkg/auth/credentials.go @@ -28,6 +28,7 @@ import ( "time" jwtgo "github.com/dgrijalva/jwt-go" + "github.com/minio/minio/cmd/jwt" ) const ( @@ -210,16 +211,33 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string) for i := 0; i < accessKeyMaxLen; i++ { keyBytes[i] = alphaNumericTable[keyBytes[i]%alphaNumericTableLen] } - cred.AccessKey = string(keyBytes) + accessKey := string(keyBytes) // Generate secret key. keyBytes, err = readBytes(secretKeyMaxLen) if err != nil { return cred, err } - cred.SecretKey = strings.Replace(string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLen]), + + secretKey := strings.Replace(string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLen]), "/", "+", -1) + return CreateNewCredentialsWithMetadata(accessKey, secretKey, m, tokenSecret) +} + +// CreateNewCredentialsWithMetadata - creates new credentials using the specified access & secret keys +// and generate a session token if a secret token is provided. +func CreateNewCredentialsWithMetadata(accessKey, secretKey string, m map[string]interface{}, tokenSecret string) (cred Credentials, err error) { + if len(accessKey) < accessKeyMinLen || len(accessKey) > accessKeyMaxLen { + return Credentials{}, fmt.Errorf("access key length should be between %d and %d", accessKeyMinLen, accessKeyMaxLen) + } + + if len(secretKey) < secretKeyMinLen || len(secretKey) > secretKeyMaxLen { + return Credentials{}, fmt.Errorf("secret key length should be between %d and %d", secretKeyMinLen, secretKeyMaxLen) + } + + cred.AccessKey = accessKey + cred.SecretKey = secretKey cred.Status = AccountOn if tokenSecret == "" { @@ -231,12 +249,9 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string) if err != nil { return cred, err } - - m["accessKey"] = cred.AccessKey - jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims(m)) - cred.Expiration = time.Unix(expiry, 0).UTC() - cred.SessionToken, err = jwt.SignedString([]byte(tokenSecret)) + + cred.SessionToken, err = JWTSignWithAccessKey(cred.AccessKey, m, tokenSecret) if err != nil { return cred, err } @@ -244,6 +259,31 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string) return cred, nil } +// JWTSignWithAccessKey - generates a session token. +func JWTSignWithAccessKey(accessKey string, m map[string]interface{}, tokenSecret string) (string, error) { + m["accessKey"] = accessKey + jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims(m)) + return jwt.SignedString([]byte(tokenSecret)) +} + +// ExtractClaims extracts JWT claims from a security token using a secret key +func ExtractClaims(token, secretKey string) (*jwt.MapClaims, error) { + if token == "" || secretKey == "" { + return nil, errors.New("invalid argument") + } + + claims := jwt.NewMapClaims() + stsTokenCallback := func(claims *jwt.MapClaims) ([]byte, error) { + return []byte(secretKey), nil + } + + if err := jwt.ParseWithClaims(token, claims, stsTokenCallback); err != nil { + return nil, err + } + + return claims, nil +} + // GetNewCredentials generates and returns new credential. func GetNewCredentials() (cred Credentials, err error) { return GetNewCredentialsWithMetadata(map[string]interface{}{}, "") diff --git a/pkg/iam/policy/admin-action.go b/pkg/iam/policy/admin-action.go index 5dc1b9e13..8c5307f67 100644 --- a/pkg/iam/policy/admin-action.go +++ b/pkg/iam/policy/admin-action.go @@ -77,6 +77,17 @@ const ( // GetUserAdminAction - allows GET permission on user info GetUserAdminAction = "admin:GetUser" + // Service account Actions + + // CreateServiceAccountAdminAction - allow create a service account for a user + CreateServiceAccountAdminAction = "admin:CreateServiceAccount" + // UpdateServiceAccountAdminAction - allow updating a service account + UpdateServiceAccountAdminAction = "admin:UpdateServiceAccount" + // RemoveServiceAccountAdminAction - allow removing a service account + RemoveServiceAccountAdminAction = "admin:RemoveServiceAccount" + // ListServiceAccountsAdminAction - allow listing service accounts + ListServiceAccountsAdminAction = "admin:ListServiceAccounts" + // Group Actions // AddUserToGroupAdminAction - allow adding user to group permission @@ -125,43 +136,47 @@ const ( // List of all supported admin actions. var supportedAdminActions = map[AdminAction]struct{}{ - HealAdminAction: {}, - StorageInfoAdminAction: {}, - DataUsageInfoAdminAction: {}, - TopLocksAdminAction: {}, - ProfilingAdminAction: {}, - TraceAdminAction: {}, - ConsoleLogAdminAction: {}, - KMSKeyStatusAdminAction: {}, - ServerInfoAdminAction: {}, - HealthInfoAdminAction: {}, - BandwidthMonitorAction: {}, - ServerUpdateAdminAction: {}, - ServiceRestartAdminAction: {}, - ServiceStopAdminAction: {}, - ConfigUpdateAdminAction: {}, - CreateUserAdminAction: {}, - DeleteUserAdminAction: {}, - ListUsersAdminAction: {}, - EnableUserAdminAction: {}, - DisableUserAdminAction: {}, - GetUserAdminAction: {}, - AddUserToGroupAdminAction: {}, - RemoveUserFromGroupAdminAction: {}, - GetGroupAdminAction: {}, - ListGroupsAdminAction: {}, - EnableGroupAdminAction: {}, - DisableGroupAdminAction: {}, - CreatePolicyAdminAction: {}, - DeletePolicyAdminAction: {}, - GetPolicyAdminAction: {}, - AttachPolicyAdminAction: {}, - ListUserPoliciesAdminAction: {}, - SetBucketQuotaAdminAction: {}, - GetBucketQuotaAdminAction: {}, - SetBucketTargetAction: {}, - GetBucketTargetAction: {}, - AllAdminActions: {}, + HealAdminAction: {}, + StorageInfoAdminAction: {}, + DataUsageInfoAdminAction: {}, + TopLocksAdminAction: {}, + ProfilingAdminAction: {}, + TraceAdminAction: {}, + ConsoleLogAdminAction: {}, + KMSKeyStatusAdminAction: {}, + ServerInfoAdminAction: {}, + HealthInfoAdminAction: {}, + BandwidthMonitorAction: {}, + ServerUpdateAdminAction: {}, + ServiceRestartAdminAction: {}, + ServiceStopAdminAction: {}, + ConfigUpdateAdminAction: {}, + CreateUserAdminAction: {}, + DeleteUserAdminAction: {}, + ListUsersAdminAction: {}, + EnableUserAdminAction: {}, + DisableUserAdminAction: {}, + GetUserAdminAction: {}, + AddUserToGroupAdminAction: {}, + RemoveUserFromGroupAdminAction: {}, + GetGroupAdminAction: {}, + ListGroupsAdminAction: {}, + EnableGroupAdminAction: {}, + DisableGroupAdminAction: {}, + CreateServiceAccountAdminAction: {}, + UpdateServiceAccountAdminAction: {}, + RemoveServiceAccountAdminAction: {}, + ListServiceAccountsAdminAction: {}, + CreatePolicyAdminAction: {}, + DeletePolicyAdminAction: {}, + GetPolicyAdminAction: {}, + AttachPolicyAdminAction: {}, + ListUserPoliciesAdminAction: {}, + SetBucketQuotaAdminAction: {}, + GetBucketQuotaAdminAction: {}, + SetBucketTargetAction: {}, + GetBucketTargetAction: {}, + AllAdminActions: {}, } // IsValid - checks if action is valid or not. @@ -172,40 +187,45 @@ func (action AdminAction) IsValid() bool { // adminActionConditionKeyMap - holds mapping of supported condition key for an action. var adminActionConditionKeyMap = map[Action]condition.KeySet{ - AllAdminActions: condition.NewKeySet(condition.AllSupportedAdminKeys...), - HealAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - StorageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ServerInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - DataUsageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - HealthInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - BandwidthMonitorAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - TopLocksAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ProfilingAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - TraceAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ConsoleLogAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - KMSKeyStatusAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ServerUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ServiceRestartAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ServiceStopAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ConfigUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - CreateUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - DeleteUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ListUsersAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - EnableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - DisableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - GetUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - AddUserToGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - RemoveUserFromGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ListGroupsAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - EnableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - DisableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - CreatePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - DeletePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - GetPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - AttachPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ListUserPoliciesAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - SetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - GetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - SetBucketTargetAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - GetBucketTargetAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + AllAdminActions: condition.NewKeySet(condition.AllSupportedAdminKeys...), + HealAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + StorageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ServerInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + DataUsageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + HealthInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + BandwidthMonitorAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + TopLocksAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ProfilingAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + TraceAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ConsoleLogAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + KMSKeyStatusAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ServerUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ServiceRestartAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ServiceStopAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ConfigUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + CreateUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + DeleteUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ListUsersAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + EnableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + DisableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + GetUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + AddUserToGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + RemoveUserFromGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ListGroupsAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + EnableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + DisableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + CreateServiceAccountAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + UpdateServiceAccountAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + RemoveServiceAccountAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ListServiceAccountsAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + + CreatePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + DeletePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + GetPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + AttachPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ListUserPoliciesAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + SetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + GetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + SetBucketTargetAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + GetBucketTargetAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), } diff --git a/pkg/madmin/examples/service-accounts.go b/pkg/madmin/examples/service-accounts.go index 28315954a..20bcbf8bc 100644 --- a/pkg/madmin/examples/service-accounts.go +++ b/pkg/madmin/examples/service-accounts.go @@ -56,14 +56,14 @@ func main() { } // Create a new service account - creds, err := madmClnt.AddServiceAccount(context.Background(), &p) + creds, err := madmClnt.AddServiceAccount(context.Background(), madmin.AddServiceAccountReq{Policy: &p}) if err != nil { log.Fatalln(err) } fmt.Println(creds) // List all services accounts - list, err := madmClnt.ListServiceAccounts(context.Background()) + list, err := madmClnt.ListServiceAccounts(context.Background(), "") if err != nil { log.Fatalln(err) } diff --git a/pkg/madmin/user-commands.go b/pkg/madmin/user-commands.go index ffe67997d..a322a4c20 100644 --- a/pkg/madmin/user-commands.go +++ b/pkg/madmin/user-commands.go @@ -266,9 +266,12 @@ func (adm *AdminClient) SetUserStatus(ctx context.Context, accessKey string, sta return nil } -// AddServiceAccountReq is the request body of the add service account admin call +// AddServiceAccountReq is the request options of the add service account admin call type AddServiceAccountReq struct { - Policy *iampolicy.Policy `json:"policy,omitempty"` + Policy *iampolicy.Policy `json:"policy,omitempty"` + TargetUser string `json:"targetUser,omitempty"` + AccessKey string `json:"accessKey,omitempty"` + SecretKey string `json:"secretKey,omitempty"` } // AddServiceAccountResp is the response body of the add service account admin call @@ -278,16 +281,14 @@ type AddServiceAccountResp struct { // AddServiceAccount - creates a new service account belonging to the user sending // the request while restricting the service account permission by the given policy document. -func (adm *AdminClient) AddServiceAccount(ctx context.Context, policy *iampolicy.Policy) (auth.Credentials, error) { - if policy != nil { - if err := policy.Validate(); err != nil { +func (adm *AdminClient) AddServiceAccount(ctx context.Context, opts AddServiceAccountReq) (auth.Credentials, error) { + if opts.Policy != nil { + if err := opts.Policy.Validate(); err != nil { return auth.Credentials{}, err } } - data, err := json.Marshal(AddServiceAccountReq{ - Policy: policy, - }) + data, err := json.Marshal(opts) if err != nil { return auth.Credentials{}, err } @@ -325,15 +326,67 @@ func (adm *AdminClient) AddServiceAccount(ctx context.Context, policy *iampolicy return serviceAccountResp.Credentials, nil } +// UpdateServiceAccountReq is the request options of the edit service account admin call +type UpdateServiceAccountReq struct { + NewPolicy *iampolicy.Policy `json:"newPolicy,omitempty"` + NewSecretKey string `json:"newSecretKey,omitempty"` + NewStatus string `json:"newStatus,omityempty"` +} + +// UpdateServiceAccount - edit an existing service account +func (adm *AdminClient) UpdateServiceAccount(ctx context.Context, accessKey string, opts UpdateServiceAccountReq) error { + if opts.NewPolicy != nil { + if err := opts.NewPolicy.Validate(); err != nil { + return err + } + } + + data, err := json.Marshal(opts) + if err != nil { + return err + } + + econfigBytes, err := EncryptData(adm.getSecretKey(), data) + if err != nil { + return err + } + + queryValues := url.Values{} + queryValues.Set("accessKey", accessKey) + + reqData := requestData{ + relPath: adminAPIPrefix + "/update-service-account", + content: econfigBytes, + queryValues: queryValues, + } + + // Execute POST on /minio/admin/v3/update-service-account to edit a service account + resp, err := adm.executeMethod(ctx, http.MethodPost, reqData) + defer closeResponse(resp) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusNoContent { + return httpRespToErrorResponse(resp) + } + + return nil +} + // ListServiceAccountsResp is the response body of the list service accounts call type ListServiceAccountsResp struct { Accounts []string `json:"accounts"` } // ListServiceAccounts - list service accounts belonging to the specified user -func (adm *AdminClient) ListServiceAccounts(ctx context.Context) (ListServiceAccountsResp, error) { +func (adm *AdminClient) ListServiceAccounts(ctx context.Context, user string) (ListServiceAccountsResp, error) { + queryValues := url.Values{} + queryValues.Set("user", user) + reqData := requestData{ - relPath: adminAPIPrefix + "/list-service-accounts", + relPath: adminAPIPrefix + "/list-service-accounts", + queryValues: queryValues, } // Execute GET on /minio/admin/v3/list-service-accounts @@ -359,6 +412,47 @@ func (adm *AdminClient) ListServiceAccounts(ctx context.Context) (ListServiceAcc return listResp, nil } +// InfoServiceAccountResp is the response body of the info service account call +type InfoServiceAccountResp struct { + ParentUser string `json:"parentUser"` + AccountStatus string `json:"accountStatus"` + ImpliedPolicy bool `json:"impliedPolicy"` + Policy string `json:"policy"` +} + +// InfoServiceAccount - returns the info of service account belonging to the specified user +func (adm *AdminClient) InfoServiceAccount(ctx context.Context, accessKey string) (InfoServiceAccountResp, error) { + queryValues := url.Values{} + queryValues.Set("accessKey", accessKey) + + reqData := requestData{ + relPath: adminAPIPrefix + "/info-service-account", + queryValues: queryValues, + } + + // Execute GET on /minio/admin/v3/info-service-account + resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) + defer closeResponse(resp) + if err != nil { + return InfoServiceAccountResp{}, err + } + + if resp.StatusCode != http.StatusOK { + return InfoServiceAccountResp{}, httpRespToErrorResponse(resp) + } + + data, err := DecryptData(adm.getSecretKey(), resp.Body) + if err != nil { + return InfoServiceAccountResp{}, err + } + + var infoResp InfoServiceAccountResp + if err = json.Unmarshal(data, &infoResp); err != nil { + return InfoServiceAccountResp{}, err + } + return infoResp, nil +} + // DeleteServiceAccount - delete a specified service account. The server will reject // the request if the service account does not belong to the user initiating the request func (adm *AdminClient) DeleteServiceAccount(ctx context.Context, serviceAccount string) error {