// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "encoding/base64" "errors" "fmt" "net/http" "strings" "github.com/gorilla/mux" "github.com/minio/minio/cmd/config/identity/openid" xhttp "github.com/minio/minio/cmd/http" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" iampolicy "github.com/minio/minio/pkg/iam/policy" "github.com/minio/minio/pkg/wildcard" ) const ( // STS API version. stsAPIVersion = "2011-06-15" stsVersion = "Version" stsAction = "Action" stsPolicy = "Policy" stsToken = "Token" stsWebIdentityToken = "WebIdentityToken" stsDurationSeconds = "DurationSeconds" stsLDAPUsername = "LDAPUsername" stsLDAPPassword = "LDAPPassword" // STS API action constants clientGrants = "AssumeRoleWithClientGrants" webIdentity = "AssumeRoleWithWebIdentity" ldapIdentity = "AssumeRoleWithLDAPIdentity" assumeRole = "AssumeRole" stsRequestBodyLimit = 10 * (1 << 20) // 10 MiB // JWT claim keys expClaim = "exp" subClaim = "sub" issClaim = "iss" // JWT claim to check the parent user parentClaim = "parent" // LDAP claim keys ldapUser = "ldapUser" ) // stsAPIHandlers implements and provides http handlers for AWS STS API. type stsAPIHandlers struct{} // registerSTSRouter - registers AWS STS compatible APIs. func registerSTSRouter(router *mux.Router) { // Initialize STS. sts := &stsAPIHandlers{} // STS Router stsRouter := router.NewRoute().PathPrefix(SlashSeparator).Subrouter() // Assume roles with no JWT, handles AssumeRole. stsRouter.Methods(http.MethodPost).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool { ctypeOk := wildcard.MatchSimple("application/x-www-form-urlencoded*", r.Header.Get(xhttp.ContentType)) authOk := wildcard.MatchSimple(signV4Algorithm+"*", r.Header.Get(xhttp.Authorization)) noQueries := len(r.URL.Query()) == 0 return ctypeOk && authOk && noQueries }).HandlerFunc(httpTraceAll(sts.AssumeRole)) // Assume roles with JWT handler, handles both ClientGrants and WebIdentity. stsRouter.Methods(http.MethodPost).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool { ctypeOk := wildcard.MatchSimple("application/x-www-form-urlencoded*", r.Header.Get(xhttp.ContentType)) noQueries := len(r.URL.Query()) == 0 return ctypeOk && noQueries }).HandlerFunc(httpTraceAll(sts.AssumeRoleWithSSO)) // AssumeRoleWithClientGrants stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)). Queries(stsAction, clientGrants). Queries(stsVersion, stsAPIVersion). Queries(stsToken, "{Token:.*}") // AssumeRoleWithWebIdentity stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithWebIdentity)). Queries(stsAction, webIdentity). Queries(stsVersion, stsAPIVersion). Queries(stsWebIdentityToken, "{Token:.*}") // AssumeRoleWithLDAPIdentity stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithLDAPIdentity)). Queries(stsAction, ldapIdentity). Queries(stsVersion, stsAPIVersion). Queries(stsLDAPUsername, "{LDAPUsername:.*}"). Queries(stsLDAPPassword, "{LDAPPassword:.*}") } func checkAssumeRoleAuth(ctx context.Context, r *http.Request) (user auth.Credentials, isErrCodeSTS bool, stsErr STSErrorCode) { switch getRequestAuthType(r) { default: return user, true, ErrSTSAccessDenied case authTypeSigned: s3Err := isReqAuthenticated(ctx, r, globalServerRegion, serviceSTS) if APIErrorCode(s3Err) != ErrNone { return user, false, STSErrorCode(s3Err) } user, _, s3Err = getReqAccessKeyV4(r, globalServerRegion, serviceSTS) if APIErrorCode(s3Err) != ErrNone { return user, false, STSErrorCode(s3Err) } // Temporary credentials or Service accounts cannot generate further temporary credentials. if user.IsTemp() || user.IsServiceAccount() { return user, true, ErrSTSAccessDenied } } // Session tokens are not allowed in STS AssumeRole requests. if getSessionToken(r) != "" { return user, true, ErrSTSAccessDenied } return user, true, ErrSTSNone } // AssumeRole - implementation of AWS STS API AssumeRole to get temporary // credentials for regular users on Minio. // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html func (sts *stsAPIHandlers) AssumeRole(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "AssumeRole") user, isErrCodeSTS, stsErr := checkAssumeRoleAuth(ctx, r) if stsErr != ErrSTSNone { writeSTSErrorResponse(ctx, w, isErrCodeSTS, stsErr, nil) return } if err := r.ParseForm(); err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } if r.Form.Get(stsVersion) != stsAPIVersion { writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get(stsVersion), stsAPIVersion)) return } action := r.Form.Get(stsAction) switch action { case assumeRole: default: writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Unsupported action %s", action)) return } ctx = newContext(r, w, action) defer logger.AuditLog(ctx, w, r, nil) sessionPolicyStr := r.Form.Get(stsPolicy) // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html // The plain text that you use for both inline and managed session // policies shouldn't exceed 2048 characters. if len(sessionPolicyStr) > 2048 { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Session policy shouldn't exceed 2048 characters")) return } if len(sessionPolicyStr) > 0 { sessionPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(sessionPolicyStr))) if err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } // Version in policy must not be empty if sessionPolicy.Version == "" { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Version cannot be empty expecting '2012-10-17'")) return } } var err error m := make(map[string]interface{}) m[expClaim], err = openid.GetDefaultExpiration(r.Form.Get(stsDurationSeconds)) if err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } policies, err := globalIAMSys.PolicyDBGet(user.AccessKey, false) if err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } policyName := strings.Join(policies, ",") // This policy is the policy associated with the user // requesting for temporary credentials. The temporary // credentials will inherit the same policy requirements. m[iamPolicyClaimNameOpenID()] = policyName if len(sessionPolicyStr) > 0 { m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString([]byte(sessionPolicyStr)) } secret := globalActiveCred.SecretKey cred, err := auth.GetNewCredentialsWithMetadata(m, secret) if err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err) return } // Set the parent of the temporary access key, this is useful // in obtaining service accounts by this cred. cred.ParentUser = user.AccessKey // Set the newly generated credentials. if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err) return } // Notify all other MinIO peers to reload temp users for _, nerr := range globalNotificationSys.LoadUser(cred.AccessKey, true) { if nerr.Err != nil { logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) logger.LogIf(ctx, nerr.Err) } } assumeRoleResponse := &AssumeRoleResponse{ Result: AssumeRoleResult{ Credentials: cred, }, } assumeRoleResponse.ResponseMetadata.RequestID = w.Header().Get(xhttp.AmzRequestID) writeSuccessResponseXML(w, encodeResponse(assumeRoleResponse)) } func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "AssumeRoleSSOCommon") // Parse the incoming form data. if err := r.ParseForm(); err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } if r.Form.Get(stsVersion) != stsAPIVersion { writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion)) return } action := r.Form.Get(stsAction) switch action { case ldapIdentity: sts.AssumeRoleWithLDAPIdentity(w, r) return case clientGrants, webIdentity: default: writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Unsupported action %s", action)) return } ctx = newContext(r, w, action) defer logger.AuditLog(ctx, w, r, nil) if globalOpenIDValidators == nil { writeSTSErrorResponse(ctx, w, true, ErrSTSNotInitialized, errServerNotInitialized) return } v, err := globalOpenIDValidators.Get("jwt") if err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } token := r.Form.Get(stsToken) if token == "" { token = r.Form.Get(stsWebIdentityToken) } m, err := v.Validate(token, r.Form.Get(stsDurationSeconds)) if err != nil { switch err { case openid.ErrTokenExpired: switch action { case clientGrants: writeSTSErrorResponse(ctx, w, true, ErrSTSClientGrantsExpiredToken, err) case webIdentity: writeSTSErrorResponse(ctx, w, true, ErrSTSWebIdentityExpiredToken, err) } return case auth.ErrInvalidDuration: writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } var subFromToken string if v, ok := m[subClaim]; ok { subFromToken, _ = v.(string) } if subFromToken == "" { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, errors.New("STS JWT Token has `sub` claim missing, `sub` claim is mandatory")) return } var issFromToken string if v, ok := m[issClaim]; ok { issFromToken, _ = v.(string) } // JWT has requested a custom claim with policy value set. // This is a MinIO STS API specific value, this value should // be set and configured on your identity provider as part of // JWT custom claims. var policyName string policySet, ok := iampolicy.GetPoliciesFromClaims(m, iamPolicyClaimNameOpenID()) if ok { policyName = globalIAMSys.CurrentPolicies(strings.Join(policySet.ToSlice(), ",")) } if policyName == "" && globalPolicyOPA == nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID())) return } m[iamPolicyClaimNameOpenID()] = policyName sessionPolicyStr := r.Form.Get(stsPolicy) // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html // The plain text that you use for both inline and managed session // policies shouldn't exceed 2048 characters. if len(sessionPolicyStr) > 2048 { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Session policy should not exceed 2048 characters")) return } if len(sessionPolicyStr) > 0 { sessionPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(sessionPolicyStr))) if err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } // Version in policy must not be empty if sessionPolicy.Version == "" { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Invalid session policy version")) return } m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString([]byte(sessionPolicyStr)) } secret := globalActiveCred.SecretKey cred, err := auth.GetNewCredentialsWithMetadata(m, secret) if err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err) return } // https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability // claim is only considered stable when subject and iss are used together // this is to ensure that ParentUser doesn't change and we get to use // parentUser as per the requirements for service accounts for OpenID // based logins. cred.ParentUser = "jwt:" + subFromToken + ":" + issFromToken // Set the newly generated credentials. if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err) return } // Notify all other MinIO peers to reload temp users for _, nerr := range globalNotificationSys.LoadUser(cred.AccessKey, true) { if nerr.Err != nil { logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) logger.LogIf(ctx, nerr.Err) } } var encodedSuccessResponse []byte switch action { case clientGrants: clientGrantsResponse := &AssumeRoleWithClientGrantsResponse{ Result: ClientGrantsResult{ Credentials: cred, SubjectFromToken: subFromToken, }, } clientGrantsResponse.ResponseMetadata.RequestID = w.Header().Get(xhttp.AmzRequestID) encodedSuccessResponse = encodeResponse(clientGrantsResponse) case webIdentity: webIdentityResponse := &AssumeRoleWithWebIdentityResponse{ Result: WebIdentityResult{ Credentials: cred, SubjectFromWebIdentityToken: subFromToken, }, } webIdentityResponse.ResponseMetadata.RequestID = w.Header().Get(xhttp.AmzRequestID) encodedSuccessResponse = encodeResponse(webIdentityResponse) } writeSuccessResponseXML(w, encodedSuccessResponse) } // AssumeRoleWithWebIdentity - implementation of AWS STS API supporting OAuth2.0 // users from web identity provider such as Facebook, Google, or any OpenID // Connect-compatible identity provider. // // Eg:- // $ curl https://minio:9000/?Action=AssumeRoleWithWebIdentity&WebIdentityToken= func (sts *stsAPIHandlers) AssumeRoleWithWebIdentity(w http.ResponseWriter, r *http.Request) { sts.AssumeRoleWithSSO(w, r) } // AssumeRoleWithClientGrants - implementation of AWS STS extension API supporting // OAuth2.0 client credential grants. // // Eg:- // $ curl https://minio:9000/?Action=AssumeRoleWithClientGrants&Token= func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *http.Request) { sts.AssumeRoleWithSSO(w, r) } // AssumeRoleWithLDAPIdentity - implements user auth against LDAP server func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "AssumeRoleWithLDAPIdentity") defer logger.AuditLog(ctx, w, r, nil, stsLDAPPassword) // Parse the incoming form data. if err := r.ParseForm(); err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } if r.Form.Get(stsVersion) != stsAPIVersion { writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion)) return } ldapUsername := r.Form.Get(stsLDAPUsername) ldapPassword := r.Form.Get(stsLDAPPassword) if ldapUsername == "" || ldapPassword == "" { writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, fmt.Errorf("LDAPUsername and LDAPPassword cannot be empty")) return } action := r.Form.Get(stsAction) switch action { case ldapIdentity: default: writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Unsupported action %s", action)) return } sessionPolicyStr := r.Form.Get(stsPolicy) // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html // The plain text that you use for both inline and managed session // policies shouldn't exceed 2048 characters. if len(sessionPolicyStr) > 2048 { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Session policy should not exceed 2048 characters")) return } if len(sessionPolicyStr) > 0 { sessionPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(sessionPolicyStr))) if err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } // Version in policy must not be empty if sessionPolicy.Version == "" { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Version needs to be specified in session policy")) return } } ldapUserDN, groupDistNames, err := globalLDAPConfig.Bind(ldapUsername, ldapPassword) if err != nil { err = fmt.Errorf("LDAP server error: %w", err) writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err) return } // Check if this user or their groups have a policy applied. ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, false, groupDistNames...) if len(ldapPolicies) == 0 { writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("expecting a policy to be set for user `%s` or one of their groups: `%s` - rejecting this request", ldapUserDN, strings.Join(groupDistNames, "`,`"))) return } expiryDur := globalLDAPConfig.GetExpiryDuration() m := map[string]interface{}{ expClaim: UTCNow().Add(expiryDur).Unix(), ldapUser: ldapUserDN, } if len(sessionPolicyStr) > 0 { m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString([]byte(sessionPolicyStr)) } secret := globalActiveCred.SecretKey cred, err := auth.GetNewCredentialsWithMetadata(m, secret) if err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err) return } // Set the parent of the temporary access key, this is useful // in obtaining service accounts by this cred. cred.ParentUser = ldapUserDN // Set this value to LDAP groups, LDAP user can be part // of large number of groups cred.Groups = groupDistNames // Set the newly generated credentials, policyName is empty on purpose // LDAP policies are applied automatically using their ldapUser, ldapGroups // mapping. if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, ""); err != nil { writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err) return } // Notify all other MinIO peers to reload temp users for _, nerr := range globalNotificationSys.LoadUser(cred.AccessKey, true) { if nerr.Err != nil { logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String()) logger.LogIf(ctx, nerr.Err) } } ldapIdentityResponse := &AssumeRoleWithLDAPResponse{ Result: LDAPIdentityResult{ Credentials: cred, }, } ldapIdentityResponse.ResponseMetadata.RequestID = w.Header().Get(xhttp.AmzRequestID) encodedSuccessResponse := encodeResponse(ldapIdentityResponse) writeSuccessResponseXML(w, encodedSuccessResponse) }