mirror of
https://github.com/minio/minio.git
synced 2024-12-25 14:45:54 -05:00
9d07cde385
It would seem like the PR #11623 had chewed more than it wanted to, non-fips build shouldn't really be forced to use slower crypto/sha256 even for presumed "non-performance" codepaths. In MinIO there are really no "non-performance" codepaths. This assumption seems to have had an adverse effect in certain areas of CPU usage. This PR ensures that we stick to sha256-simd on all non-FIPS builds, our most common build to ensure we get the best out of the CPU at any given point in time.
947 lines
32 KiB
Go
947 lines
32 KiB
Go
/// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/minio/madmin-go"
|
|
"github.com/minio/minio/internal/auth"
|
|
"github.com/minio/minio/internal/config/identity/openid"
|
|
"github.com/minio/minio/internal/hash/sha256"
|
|
xhttp "github.com/minio/minio/internal/http"
|
|
"github.com/minio/minio/internal/logger"
|
|
iampolicy "github.com/minio/pkg/iam/policy"
|
|
"github.com/minio/pkg/wildcard"
|
|
)
|
|
|
|
const (
|
|
// STS API version.
|
|
stsAPIVersion = "2011-06-15"
|
|
stsVersion = "Version"
|
|
stsAction = "Action"
|
|
stsPolicy = "Policy"
|
|
stsToken = "Token"
|
|
stsRoleArn = "RoleArn"
|
|
stsWebIdentityToken = "WebIdentityToken"
|
|
stsWebIdentityAccessToken = "WebIdentityAccessToken" // only valid if UserInfo is enabled.
|
|
stsDurationSeconds = "DurationSeconds"
|
|
stsLDAPUsername = "LDAPUsername"
|
|
stsLDAPPassword = "LDAPPassword"
|
|
|
|
// STS API action constants
|
|
clientGrants = "AssumeRoleWithClientGrants"
|
|
webIdentity = "AssumeRoleWithWebIdentity"
|
|
ldapIdentity = "AssumeRoleWithLDAPIdentity"
|
|
clientCertificate = "AssumeRoleWithCertificate"
|
|
customTokenIdentity = "AssumeRoleWithCustomToken"
|
|
assumeRole = "AssumeRole"
|
|
|
|
stsRequestBodyLimit = 10 * (1 << 20) // 10 MiB
|
|
|
|
// JWT claim keys
|
|
expClaim = "exp"
|
|
subClaim = "sub"
|
|
audClaim = "aud"
|
|
issClaim = "iss"
|
|
|
|
// JWT claim to check the parent user
|
|
parentClaim = "parent"
|
|
|
|
// LDAP claim keys
|
|
ldapUser = "ldapUser"
|
|
ldapUserN = "ldapUsername"
|
|
|
|
// Role Claim key
|
|
roleArnClaim = "roleArn"
|
|
)
|
|
|
|
// 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.RawQuery) == 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.RawQuery) == 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:.*}")
|
|
|
|
// AssumeRoleWithCertificate
|
|
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCertificate)).
|
|
Queries(stsAction, clientCertificate).
|
|
Queries(stsVersion, stsAPIVersion)
|
|
|
|
// AssumeRoleWithCustomToken
|
|
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithCustomToken)).
|
|
Queries(stsAction, customTokenIdentity).
|
|
Queries(stsVersion, stsAPIVersion)
|
|
}
|
|
|
|
func checkAssumeRoleAuth(ctx context.Context, r *http.Request) (user auth.Credentials, isErrCodeSTS bool, stsErr STSErrorCode) {
|
|
if !isRequestSignatureV4(r) {
|
|
return user, true, ErrSTSAccessDenied
|
|
}
|
|
|
|
s3Err := isReqAuthenticated(ctx, r, globalSite.Region, serviceSTS)
|
|
if s3Err != ErrNone {
|
|
return user, false, STSErrorCode(s3Err)
|
|
}
|
|
|
|
user, _, s3Err = getReqAccessKeyV4(r, globalSite.Region, serviceSTS)
|
|
if 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
|
|
}
|
|
|
|
func parseForm(r *http.Request) error {
|
|
if err := r.ParseForm(); err != nil {
|
|
return err
|
|
}
|
|
for k, v := range r.PostForm {
|
|
if _, ok := r.Form[k]; !ok {
|
|
r.Form[k] = v
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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")
|
|
|
|
// Check auth here (otherwise r.Form will have unexpected values from
|
|
// the call to `parseForm` below), but return failure only after we are
|
|
// able to validate that it is a valid STS request, so that we are able
|
|
// to send an appropriate audit log.
|
|
user, isErrCodeSTS, stsErr := checkAssumeRoleAuth(ctx, r)
|
|
|
|
if err := parseForm(r); 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)
|
|
|
|
// Validate the authentication result here so that failures will be
|
|
// audit-logged.
|
|
if stsErr != ErrSTSNone {
|
|
writeSTSErrorResponse(ctx, w, isErrCodeSTS, stsErr, nil)
|
|
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 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
|
|
}
|
|
}
|
|
|
|
duration, err := openid.GetDefaultExpiration(r.Form.Get(stsDurationSeconds))
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err)
|
|
return
|
|
}
|
|
|
|
m := map[string]interface{}{
|
|
expClaim: UTCNow().Add(duration).Unix(),
|
|
parentClaim: user.AccessKey,
|
|
}
|
|
|
|
// Validate that user.AccessKey's policies can be retrieved - it may not
|
|
// be in case the user is disabled.
|
|
_, err = globalIAMSys.PolicyDBGet(user.AccessKey, false)
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err)
|
|
return
|
|
}
|
|
|
|
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, so that it's access
|
|
// policy is inherited from `user.AccessKey`.
|
|
cred.ParentUser = user.AccessKey
|
|
|
|
// Set the newly generated credentials.
|
|
if err = globalIAMSys.SetTempUser(ctx, cred.AccessKey, cred, ""); err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
|
|
return
|
|
}
|
|
|
|
// Call hook for site replication.
|
|
if cred.ParentUser != globalActiveCred.AccessKey {
|
|
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSTSAcc,
|
|
STSCredential: &madmin.SRSTSCredential{
|
|
AccessKey: cred.AccessKey,
|
|
SecretKey: cred.SecretKey,
|
|
SessionToken: cred.SessionToken,
|
|
ParentUser: cred.ParentUser,
|
|
},
|
|
}); err != nil {
|
|
logger.LogIf(ctx, 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 := parseForm(r); 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)
|
|
|
|
token := r.Form.Get(stsToken)
|
|
if token == "" {
|
|
token = r.Form.Get(stsWebIdentityToken)
|
|
}
|
|
|
|
accessToken := r.Form.Get(stsWebIdentityAccessToken)
|
|
|
|
roleArn := openid.DummyRoleARN
|
|
if globalIAMSys.HasRolePolicy() {
|
|
var err error
|
|
roleArnStr := r.Form.Get(stsRoleArn)
|
|
roleArn, _, err = globalIAMSys.GetRolePolicy(roleArnStr)
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
|
|
fmt.Errorf("Error processing %s parameter: %v", stsRoleArn, err))
|
|
return
|
|
}
|
|
|
|
}
|
|
|
|
// Validate JWT; check clientID in claims matches the one associated with the roleArn
|
|
m, err := globalOpenIDConfig.Validate(roleArn, token, accessToken, 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 policyName string
|
|
if globalIAMSys.HasRolePolicy() {
|
|
// If roleArn is used, we set it as a claim, and use the
|
|
// associated policy when credentials are used.
|
|
m[roleArnClaim] = roleArn.String()
|
|
} else {
|
|
// If no role policy is configured, then we use claims from the
|
|
// JWT. 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.
|
|
policySet, ok := iampolicy.GetPoliciesFromClaims(m, iamPolicyClaimNameOpenID())
|
|
policies := strings.Join(policySet.ToSlice(), ",")
|
|
if ok {
|
|
policyName = globalIAMSys.CurrentPolicies(policies)
|
|
}
|
|
|
|
if globalAuthZPlugin == nil {
|
|
if !ok {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
|
|
fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID()))
|
|
return
|
|
} else if policyName == "" {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
|
|
fmt.Errorf("None of the given policies (`%s`) are defined, credentials will not be generated", policies))
|
|
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.
|
|
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)
|
|
}
|
|
|
|
// Since issFromToken can have `/` characters (it is typically the
|
|
// provider URL), we hash and encode it to base64 here. This is needed
|
|
// because there will be a policy mapping stored on drives whose
|
|
// filename is this parentUser: therefore, it needs to have only valid
|
|
// filename characters and needs to have bounded length.
|
|
{
|
|
h := sha256.New()
|
|
h.Write([]byte("openid:" + subFromToken + ":" + issFromToken))
|
|
bs := h.Sum(nil)
|
|
cred.ParentUser = base64.RawURLEncoding.EncodeToString(bs)
|
|
}
|
|
|
|
// Set the newly generated credentials.
|
|
if err = globalIAMSys.SetTempUser(ctx, cred.AccessKey, cred, policyName); err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
|
|
return
|
|
}
|
|
|
|
// Call hook for site replication.
|
|
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSTSAcc,
|
|
STSCredential: &madmin.SRSTSCredential{
|
|
AccessKey: cred.AccessKey,
|
|
SecretKey: cred.SecretKey,
|
|
SessionToken: cred.SessionToken,
|
|
ParentUser: cred.ParentUser,
|
|
ParentPolicyMapping: policyName,
|
|
},
|
|
}); err != nil {
|
|
logger.LogIf(ctx, 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=<jwt>
|
|
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=<jwt>
|
|
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 := parseForm(r); 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 && globalAuthZPlugin == nil {
|
|
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, err := globalLDAPConfig.GetExpiryDuration(r.Form.Get(stsDurationSeconds))
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err)
|
|
return
|
|
}
|
|
|
|
m := map[string]interface{}{
|
|
expClaim: UTCNow().Add(expiryDur).Unix(),
|
|
ldapUser: ldapUserDN,
|
|
ldapUserN: ldapUsername,
|
|
}
|
|
|
|
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(ctx, cred.AccessKey, cred, ""); err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
|
|
return
|
|
}
|
|
|
|
// Call hook for site replication.
|
|
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSTSAcc,
|
|
STSCredential: &madmin.SRSTSCredential{
|
|
AccessKey: cred.AccessKey,
|
|
SecretKey: cred.SecretKey,
|
|
SessionToken: cred.SessionToken,
|
|
ParentUser: cred.ParentUser,
|
|
},
|
|
}); err != nil {
|
|
logger.LogIf(ctx, err)
|
|
}
|
|
|
|
ldapIdentityResponse := &AssumeRoleWithLDAPResponse{
|
|
Result: LDAPIdentityResult{
|
|
Credentials: cred,
|
|
},
|
|
}
|
|
ldapIdentityResponse.ResponseMetadata.RequestID = w.Header().Get(xhttp.AmzRequestID)
|
|
encodedSuccessResponse := encodeResponse(ldapIdentityResponse)
|
|
|
|
writeSuccessResponseXML(w, encodedSuccessResponse)
|
|
}
|
|
|
|
// AssumeRoleWithCertificate implements user authentication with client certificates.
|
|
// It verifies the client-provided X.509 certificate, maps the certificate to an S3 policy
|
|
// and returns temp. S3 credentials to the client.
|
|
//
|
|
// API endpoint: https://minio:9000?Action=AssumeRoleWithCertificate&Version=2011-06-15
|
|
func (sts *stsAPIHandlers) AssumeRoleWithCertificate(w http.ResponseWriter, r *http.Request) {
|
|
ctx := newContext(r, w, "AssumeRoleWithCertificate")
|
|
|
|
if !globalSTSTLSConfig.Enabled {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSNotInitialized, errors.New("STS API 'AssumeRoleWithCertificate' is disabled"))
|
|
return
|
|
}
|
|
|
|
// We have to establish a TLS connection and the
|
|
// client must provide exactly one client certificate.
|
|
// Otherwise, we don't have a certificate to verify or
|
|
// the policy lookup would ambigious.
|
|
if r.TLS == nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInsecureConnection, errors.New("No TLS connection attempt"))
|
|
return
|
|
}
|
|
|
|
// A client may send a certificate chain such that we end up
|
|
// with multiple peer certificates. However, we can only accept
|
|
// a single client certificate. Otherwise, the certificate to
|
|
// policy mapping would be ambigious.
|
|
// However, we can filter all CA certificates and only check
|
|
// whether they client has sent exactly one (non-CA) leaf certificate.
|
|
peerCertificates := make([]*x509.Certificate, 0, len(r.TLS.PeerCertificates))
|
|
for _, cert := range r.TLS.PeerCertificates {
|
|
if cert.IsCA {
|
|
continue
|
|
}
|
|
peerCertificates = append(peerCertificates, cert)
|
|
}
|
|
r.TLS.PeerCertificates = peerCertificates
|
|
|
|
// Now, we have to check that the client has provided exactly one leaf
|
|
// certificate that we can map to a policy.
|
|
if len(r.TLS.PeerCertificates) == 0 {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, errors.New("No client certificate provided"))
|
|
return
|
|
}
|
|
if len(r.TLS.PeerCertificates) > 1 {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, errors.New("More than one client certificate provided"))
|
|
return
|
|
}
|
|
|
|
certificate := r.TLS.PeerCertificates[0]
|
|
if !globalSTSTLSConfig.InsecureSkipVerify { // Verify whether the client certificate has been issued by a trusted CA.
|
|
_, err := certificate.Verify(x509.VerifyOptions{
|
|
KeyUsages: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageClientAuth,
|
|
},
|
|
Roots: globalRootCAs,
|
|
})
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidClientCertificate, err)
|
|
return
|
|
}
|
|
} else {
|
|
// Technically, there is no security argument for verifying the key usage
|
|
// when we don't verify that the certificate has been issued by a trusted CA.
|
|
// Any client can create a certificate with arbitrary key usage settings.
|
|
//
|
|
// However, this check ensures that a certificate with an invalid key usage
|
|
// gets rejected even when we skip certificate verification. This helps
|
|
// clients detect malformed certificates during testing instead of e.g.
|
|
// a self-signed certificate that works while a comparable certificate
|
|
// issued by a trusted CA fails due to the MinIO server being less strict
|
|
// w.r.t. key usage verification.
|
|
//
|
|
// Basically, MinIO is more consistent (from a client perspective) when
|
|
// we verify the key usage all the time.
|
|
var validKeyUsage bool
|
|
for _, usage := range certificate.ExtKeyUsage {
|
|
if usage == x509.ExtKeyUsageAny || usage == x509.ExtKeyUsageClientAuth {
|
|
validKeyUsage = true
|
|
break
|
|
}
|
|
}
|
|
if !validKeyUsage {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, errors.New("certificate is not valid for client authentication"))
|
|
return
|
|
}
|
|
}
|
|
|
|
// We map the X.509 subject common name to the policy. So, a client
|
|
// with the common name "foo" will be associated with the policy "foo".
|
|
// Other mapping functions - e.g. public-key hash based mapping - are
|
|
// possible but not implemented.
|
|
//
|
|
// Group mapping is not possible with standard X.509 certificates.
|
|
if certificate.Subject.CommonName == "" {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, errors.New("certificate subject CN cannot be empty"))
|
|
return
|
|
}
|
|
|
|
expiry, err := globalSTSTLSConfig.GetExpiryDuration(r.Form.Get(stsDurationSeconds))
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSMissingParameter, err)
|
|
return
|
|
}
|
|
|
|
// We set the expiry of the temp. credentials to the minimum of the
|
|
// configured expiry and the duration until the certificate itself
|
|
// expires.
|
|
// We must not issue credentials that out-live the certificate.
|
|
if validUntil := time.Until(certificate.NotAfter); validUntil < expiry {
|
|
expiry = validUntil
|
|
}
|
|
|
|
// Associate any service accounts to the certificate CN
|
|
parentUser := "tls:" + certificate.Subject.CommonName
|
|
|
|
tmpCredentials, err := auth.GetNewCredentialsWithMetadata(map[string]interface{}{
|
|
expClaim: UTCNow().Add(expiry).Unix(),
|
|
parentClaim: parentUser,
|
|
subClaim: certificate.Subject.CommonName,
|
|
audClaim: certificate.Subject.Organization,
|
|
issClaim: certificate.Issuer.CommonName,
|
|
}, globalActiveCred.SecretKey)
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
|
|
return
|
|
}
|
|
|
|
tmpCredentials.ParentUser = parentUser
|
|
policyName := certificate.Subject.CommonName
|
|
err = globalIAMSys.SetTempUser(ctx, tmpCredentials.AccessKey, tmpCredentials, policyName)
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
|
|
return
|
|
}
|
|
|
|
// Call hook for site replication.
|
|
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSTSAcc,
|
|
STSCredential: &madmin.SRSTSCredential{
|
|
AccessKey: tmpCredentials.AccessKey,
|
|
SecretKey: tmpCredentials.SecretKey,
|
|
SessionToken: tmpCredentials.SessionToken,
|
|
ParentUser: tmpCredentials.ParentUser,
|
|
ParentPolicyMapping: policyName,
|
|
},
|
|
}); err != nil {
|
|
logger.LogIf(ctx, err)
|
|
}
|
|
|
|
response := new(AssumeRoleWithCertificateResponse)
|
|
response.Result.Credentials = tmpCredentials
|
|
response.Metadata.RequestID = w.Header().Get(xhttp.AmzRequestID)
|
|
writeSuccessResponseXML(w, encodeResponse(response))
|
|
}
|
|
|
|
// AssumeRoleWithCustomToken implements user authentication with custom tokens.
|
|
// These tokens are opaque to MinIO and are verified by a configured (external)
|
|
// Identity Management Plugin.
|
|
//
|
|
// API endpoint: https://minio:9000?Action=AssumeRoleWithCustomToken&Token=xxx
|
|
func (sts *stsAPIHandlers) AssumeRoleWithCustomToken(w http.ResponseWriter, r *http.Request) {
|
|
ctx := newContext(r, w, "AssumeRoleWithCustomToken")
|
|
|
|
if globalAuthNPlugin == nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSNotInitialized, errors.New("STS API 'AssumeRoleWithCustomToken' is disabled"))
|
|
return
|
|
}
|
|
|
|
action := r.Form.Get(stsAction)
|
|
if action != customTokenIdentity {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Unsupported action %s", action))
|
|
return
|
|
}
|
|
|
|
defer logger.AuditLog(ctx, w, r, nil)
|
|
|
|
token := r.Form.Get(stsToken)
|
|
if token == "" {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Invalid empty `Token` parameter provided"))
|
|
return
|
|
}
|
|
|
|
durationParam := r.Form.Get(stsDurationSeconds)
|
|
var requestedDuration int
|
|
if durationParam != "" {
|
|
var err error
|
|
requestedDuration, err = strconv.Atoi(durationParam)
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, fmt.Errorf("Invalid requested duration: %s", durationParam))
|
|
return
|
|
}
|
|
}
|
|
|
|
roleArnStr := r.Form.Get(stsRoleArn)
|
|
roleArn, _, err := globalIAMSys.GetRolePolicy(roleArnStr)
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
|
|
fmt.Errorf("Error processing parameter %s: %v", stsRoleArn, err))
|
|
return
|
|
}
|
|
|
|
res, err := globalAuthNPlugin.Authenticate(roleArn, token)
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err)
|
|
return
|
|
}
|
|
|
|
// If authentication failed, return the error message to the user.
|
|
if res.Failure != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSUpstreamError, errors.New(res.Failure.Reason))
|
|
return
|
|
}
|
|
|
|
// It is required that parent user be set.
|
|
if res.Success.User == "" {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSUpstreamError, errors.New("A valid user was not returned by the authenticator."))
|
|
return
|
|
}
|
|
|
|
// Expiry is set as minimum of requested value and value allowed by auth
|
|
// plugin.
|
|
expiry := res.Success.MaxValiditySeconds
|
|
if durationParam != "" && requestedDuration < expiry {
|
|
expiry = requestedDuration
|
|
}
|
|
|
|
parentUser := "custom:" + res.Success.User
|
|
|
|
// metadata map
|
|
m := map[string]interface{}{
|
|
expClaim: UTCNow().Add(time.Duration(expiry) * time.Second).Unix(),
|
|
parentClaim: parentUser,
|
|
subClaim: parentUser,
|
|
roleArnClaim: roleArn.String(),
|
|
}
|
|
// Add all other claims from the plugin **without** replacing any
|
|
// existing claims.
|
|
for k, v := range res.Success.Claims {
|
|
if _, ok := m[k]; !ok {
|
|
m[k] = v
|
|
}
|
|
}
|
|
|
|
tmpCredentials, err := auth.GetNewCredentialsWithMetadata(m, globalActiveCred.SecretKey)
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
|
|
return
|
|
}
|
|
|
|
tmpCredentials.ParentUser = parentUser
|
|
err = globalIAMSys.SetTempUser(ctx, tmpCredentials.AccessKey, tmpCredentials, "")
|
|
if err != nil {
|
|
writeSTSErrorResponse(ctx, w, true, ErrSTSInternalError, err)
|
|
return
|
|
}
|
|
|
|
// Call hook for site replication.
|
|
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
Type: madmin.SRIAMItemSTSAcc,
|
|
STSCredential: &madmin.SRSTSCredential{
|
|
AccessKey: tmpCredentials.AccessKey,
|
|
SecretKey: tmpCredentials.SecretKey,
|
|
SessionToken: tmpCredentials.SessionToken,
|
|
ParentUser: tmpCredentials.ParentUser,
|
|
},
|
|
}); err != nil {
|
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
return
|
|
}
|
|
|
|
response := new(AssumeRoleWithCustomTokenResponse)
|
|
response.Result.Credentials = tmpCredentials
|
|
response.Result.AssumedUser = parentUser
|
|
response.Metadata.RequestID = w.Header().Get(xhttp.AmzRequestID)
|
|
writeSuccessResponseXML(w, encodeResponse(response))
|
|
}
|