From c3ca954684c685b24367da01d6fba1a92a1e2dcb Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Wed, 27 Feb 2019 17:46:55 -0800 Subject: [PATCH] Implement AssumeRole API for Minio users (#7267) For actual API reference read here https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html Documentation is added and updated as well at docs/sts/assume-role.md Fixes #6381 --- cmd/auth-handler.go | 24 ++--- cmd/auth-handler_test.go | 3 +- cmd/handler-utils.go | 2 +- cmd/iam.go | 23 ++++- cmd/object-handlers.go | 8 +- cmd/post-policy_test.go | 2 +- cmd/signature-v4-parser.go | 18 ++-- cmd/signature-v4-parser_test.go | 6 +- cmd/signature-v4-utils.go | 18 +++- cmd/signature-v4-utils_test.go | 2 +- cmd/signature-v4.go | 33 ++++--- cmd/signature-v4_test.go | 4 +- cmd/streaming-signature-v4.go | 6 +- cmd/sts-datatypes.go | 33 +++++++ cmd/sts-errors.go | 6 ++ cmd/sts-handlers.go | 156 ++++++++++++++++++++++++++++++-- cmd/test-utils_test.go | 14 +-- cmd/web-handlers.go | 4 +- docs/sts/README.md | 11 ++- docs/sts/assume-role.md | 97 ++++++++++++++++++++ docs/sts/client-grants.md | 6 +- docs/sts/opa.md | 2 + docs/sts/web-identity.md | 6 +- docs/sts/wso2.md | 4 +- pkg/auth/credentials.go | 32 ++++++- pkg/iam/validator/jwt.go | 13 +-- pkg/iam/validator/jwt_test.go | 2 +- 27 files changed, 446 insertions(+), 89 deletions(-) create mode 100644 docs/sts/assume-role.md diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go index b50fd47f1..eefc37581 100644 --- a/cmd/auth-handler.go +++ b/cmd/auth-handler.go @@ -32,7 +32,7 @@ import ( "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/hash" - "github.com/minio/minio/pkg/iam/policy" + iampolicy "github.com/minio/minio/pkg/iam/policy" "github.com/minio/minio/pkg/policy" ) @@ -126,7 +126,7 @@ func checkAdminRequestAuthType(ctx context.Context, r *http.Request, region stri // We only support admin credentials to access admin APIs. var owner bool - _, owner, s3Err = getReqAccessKeyV4(r, region) + _, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3) if s3Err != ErrNone { return s3Err } @@ -136,7 +136,7 @@ func checkAdminRequestAuthType(ctx context.Context, r *http.Request, region stri } // we only support V4 (no presign) with auth body - s3Err = isReqAuthenticated(ctx, r, region) + s3Err = isReqAuthenticated(ctx, r, region, serviceS3) } if s3Err != ErrNone { reqInfo := (&logger.ReqInfo{}).AppendTags("requestHeaders", dumpRequest(r)) @@ -241,10 +241,10 @@ func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Ac case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction: region = "" } - if s3Err = isReqAuthenticated(ctx, r, region); s3Err != ErrNone { + if s3Err = isReqAuthenticated(ctx, r, region, serviceS3); s3Err != ErrNone { return s3Err } - cred, owner, s3Err = getReqAccessKeyV4(r, region) + cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3) } if s3Err != ErrNone { return s3Err @@ -314,21 +314,21 @@ func isReqAuthenticatedV2(r *http.Request) (s3Error APIErrorCode) { return doesPresignV2SignatureMatch(r) } -func reqSignatureV4Verify(r *http.Request, region string) (s3Error APIErrorCode) { - sha256sum := getContentSha256Cksum(r) +func reqSignatureV4Verify(r *http.Request, region string, stype serviceType) (s3Error APIErrorCode) { + sha256sum := getContentSha256Cksum(r, stype) switch { case isRequestSignatureV4(r): - return doesSignatureMatch(sha256sum, r, region) + return doesSignatureMatch(sha256sum, r, region, stype) case isRequestPresignedSignatureV4(r): - return doesPresignedSignatureMatch(sha256sum, r, region) + return doesPresignedSignatureMatch(sha256sum, r, region, stype) default: return ErrAccessDenied } } // Verify if request has valid AWS Signature Version '4'. -func isReqAuthenticated(ctx context.Context, r *http.Request, region string) (s3Error APIErrorCode) { - if errCode := reqSignatureV4Verify(r, region); errCode != ErrNone { +func isReqAuthenticated(ctx context.Context, r *http.Request, region string, stype serviceType) (s3Error APIErrorCode) { + if errCode := reqSignatureV4Verify(r, region, stype); errCode != ErrNone { return errCode } @@ -432,7 +432,7 @@ func isPutAllowed(atype authType, bucketName, objectName string, r *http.Request cred, owner, s3Err = getReqAccessKeyV2(r) case authTypeStreamingSigned, authTypePresigned, authTypeSigned: region := globalServerConfig.GetRegion() - cred, owner, s3Err = getReqAccessKeyV4(r, region) + cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3) } if s3Err != ErrNone { return s3Err diff --git a/cmd/auth-handler_test.go b/cmd/auth-handler_test.go index bf977e3ee..e5540490a 100644 --- a/cmd/auth-handler_test.go +++ b/cmd/auth-handler_test.go @@ -381,7 +381,8 @@ func TestIsReqAuthenticated(t *testing.T) { ctx := context.Background() // Validates all testcases. for i, testCase := range testCases { - if s3Error := isReqAuthenticated(ctx, testCase.req, globalServerConfig.GetRegion()); s3Error != testCase.s3Error { + s3Error := isReqAuthenticated(ctx, testCase.req, globalServerConfig.GetRegion(), serviceS3) + if s3Error != testCase.s3Error { if _, err := ioutil.ReadAll(testCase.req.Body); toAPIErrorCode(ctx, err) != testCase.s3Error { t.Fatalf("Test %d: Unexpected S3 error: want %d - got %d (got after reading request %s)", i, testCase.s3Error, s3Error, toAPIError(ctx, err).Code) } diff --git a/cmd/handler-utils.go b/cmd/handler-utils.go index 3379aae80..689d21522 100644 --- a/cmd/handler-utils.go +++ b/cmd/handler-utils.go @@ -184,7 +184,7 @@ func getRedirectPostRawQuery(objInfo ObjectInfo) string { // Returns access credentials in the request Authorization header. func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) { - cred, _, _ = getReqAccessKeyV4(r, region) + cred, _, _ = getReqAccessKeyV4(r, region, serviceS3) if cred.AccessKey == "" { cred, _, _ = getReqAccessKeyV2(r) } diff --git a/cmd/iam.go b/cmd/iam.go index 8535d1c0e..4be96b80f 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -28,7 +28,7 @@ import ( "github.com/minio/minio-go/pkg/set" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" - "github.com/minio/minio/pkg/iam/policy" + iampolicy "github.com/minio/minio/pkg/iam/policy" "github.com/minio/minio/pkg/madmin" ) @@ -326,6 +326,27 @@ func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyNa return nil } +// GetUserPolicy - returns canned policy name associated with a user. +func (sys *IAMSys) GetUserPolicy(accessKey string) (policyName string, err error) { + objectAPI := newObjectLayerFn() + if objectAPI == nil { + return "", errServerNotInitialized + } + + sys.RLock() + defer sys.RUnlock() + + if _, ok := sys.iamUsersMap[accessKey]; !ok { + return "", errNoSuchUser + } + + if _, ok := sys.iamPolicyMap[accessKey]; !ok { + return "", errNoSuchUser + } + + return sys.iamPolicyMap[accessKey], nil +} + // ListUsers - list all users. func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) { objectAPI := newObjectLayerFn() diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 3305d3bc6..f79a70adb 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -1200,12 +1200,12 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req } case authTypePresigned, authTypeSigned: - if s3Err = reqSignatureV4Verify(r, globalServerConfig.GetRegion()); s3Err != ErrNone { + if s3Err = reqSignatureV4Verify(r, globalServerConfig.GetRegion(), serviceS3); s3Err != ErrNone { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) return } if !skipContentSha256Cksum(r) { - sha256hex = getContentSha256Cksum(r) + sha256hex = getContentSha256Cksum(r, serviceS3) } } @@ -1868,13 +1868,13 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http return } case authTypePresigned, authTypeSigned: - if s3Error = reqSignatureV4Verify(r, globalServerConfig.GetRegion()); s3Error != ErrNone { + if s3Error = reqSignatureV4Verify(r, globalServerConfig.GetRegion(), serviceS3); s3Error != ErrNone { writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) return } if !skipContentSha256Cksum(r) { - sha256hex = getContentSha256Cksum(r) + sha256hex = getContentSha256Cksum(r, serviceS3) } } diff --git a/cmd/post-policy_test.go b/cmd/post-policy_test.go index c4550d8f8..490e75fd6 100644 --- a/cmd/post-policy_test.go +++ b/cmd/post-policy_test.go @@ -499,7 +499,7 @@ func testPostPolicyBucketHandlerRedirect(obj ObjectLayer, instanceType string, t // postPresignSignatureV4 - presigned signature for PostPolicy requests. func postPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, location string) string { // Get signining key. - signingkey := getSigningKey(secretAccessKey, t, location) + signingkey := getSigningKey(secretAccessKey, t, location, "s3") // Calculate signature. signature := getSignature(signingkey, policyBase64) return signature diff --git a/cmd/signature-v4-parser.go b/cmd/signature-v4-parser.go index 00668e846..dede3b38e 100644 --- a/cmd/signature-v4-parser.go +++ b/cmd/signature-v4-parser.go @@ -47,8 +47,8 @@ func (c credentialHeader) getScope() string { }, "/") } -func getReqAccessKeyV4(r *http.Request, region string) (auth.Credentials, bool, APIErrorCode) { - ch, err := parseCredentialHeader("Credential="+r.URL.Query().Get("X-Amz-Credential"), region) +func getReqAccessKeyV4(r *http.Request, region string, stype serviceType) (auth.Credentials, bool, APIErrorCode) { + ch, err := parseCredentialHeader("Credential="+r.URL.Query().Get("X-Amz-Credential"), region, stype) if err != ErrNone { // Strip off the Algorithm prefix. v4Auth := strings.TrimPrefix(r.Header.Get("Authorization"), signV4Algorithm) @@ -56,7 +56,7 @@ func getReqAccessKeyV4(r *http.Request, region string) (auth.Credentials, bool, if len(authFields) != 3 { return auth.Credentials{}, false, ErrMissingFields } - ch, err = parseCredentialHeader(authFields[0], region) + ch, err = parseCredentialHeader(authFields[0], region, stype) if err != ErrNone { return auth.Credentials{}, false, err } @@ -65,7 +65,7 @@ func getReqAccessKeyV4(r *http.Request, region string) (auth.Credentials, bool, } // parse credentialHeader string into its structured form. -func parseCredentialHeader(credElement string, region string) (ch credentialHeader, aec APIErrorCode) { +func parseCredentialHeader(credElement string, region string, stype serviceType) (ch credentialHeader, aec APIErrorCode) { creds := strings.Split(strings.TrimSpace(credElement), "=") if len(creds) != 2 { return ch, ErrMissingFields @@ -107,7 +107,7 @@ func parseCredentialHeader(credElement string, region string) (ch credentialHead return ch, ErrAuthorizationHeaderMalformed } - if credElements[2] != "s3" { + if credElements[2] != string(stype) { return ch, ErrInvalidService } cred.scope.service = credElements[2] @@ -185,7 +185,7 @@ func doesV4PresignParamsExist(query url.Values) APIErrorCode { } // Parses all the presigned signature values into separate elements. -func parsePreSignV4(query url.Values, region string) (psv preSignValues, aec APIErrorCode) { +func parsePreSignV4(query url.Values, region string, stype serviceType) (psv preSignValues, aec APIErrorCode) { // verify whether the required query params exist. err := doesV4PresignParamsExist(query) if err != ErrNone { @@ -201,7 +201,7 @@ func parsePreSignV4(query url.Values, region string) (psv preSignValues, aec API preSignV4Values := preSignValues{} // Save credential. - preSignV4Values.Credential, err = parseCredentialHeader("Credential="+query.Get("X-Amz-Credential"), region) + preSignV4Values.Credential, err = parseCredentialHeader("Credential="+query.Get("X-Amz-Credential"), region, stype) if err != ErrNone { return psv, err } @@ -249,7 +249,7 @@ func parsePreSignV4(query url.Values, region string) (psv preSignValues, aec API // Authorization: algorithm Credential=accessKeyID/credScope, \ // SignedHeaders=signedHeaders, Signature=signature // -func parseSignV4(v4Auth string, region string) (sv signValues, aec APIErrorCode) { +func parseSignV4(v4Auth string, region string, stype serviceType) (sv signValues, aec APIErrorCode) { // Replace all spaced strings, some clients can send spaced // parameters and some won't. So we pro-actively remove any spaces // to make parsing easier. @@ -275,7 +275,7 @@ func parseSignV4(v4Auth string, region string) (sv signValues, aec APIErrorCode) var err APIErrorCode // Save credentail values. - signV4Values.Credential, err = parseCredentialHeader(authFields[0], region) + signV4Values.Credential, err = parseCredentialHeader(authFields[0], region, stype) if err != ErrNone { return sv, err } diff --git a/cmd/signature-v4-parser_test.go b/cmd/signature-v4-parser_test.go index 865756da5..3baec0592 100644 --- a/cmd/signature-v4-parser_test.go +++ b/cmd/signature-v4-parser_test.go @@ -219,7 +219,7 @@ func TestParseCredentialHeader(t *testing.T) { } for i, testCase := range testCases { - actualCredential, actualErrCode := parseCredentialHeader(testCase.inputCredentialStr, "us-west-1") + actualCredential, actualErrCode := parseCredentialHeader(testCase.inputCredentialStr, "us-west-1", "s3") // validating the credential fields. if testCase.expectedErrCode != actualErrCode { t.Fatalf("Test %d: Expected the APIErrCode to be %s, got %s", i+1, errorCodes[testCase.expectedErrCode].Code, errorCodes[actualErrCode].Code) @@ -446,7 +446,7 @@ func TestParseSignV4(t *testing.T) { } for i, testCase := range testCases { - parsedAuthField, actualErrCode := parseSignV4(testCase.inputV4AuthStr, "") + parsedAuthField, actualErrCode := parseSignV4(testCase.inputV4AuthStr, "", "s3") if testCase.expectedErrCode != actualErrCode { t.Fatalf("Test %d: Expected the APIErrCode to be %d, got %d", i+1, testCase.expectedErrCode, actualErrCode) @@ -813,7 +813,7 @@ func TestParsePreSignV4(t *testing.T) { inputQuery.Set(testCase.inputQueryKeyVals[j], testCase.inputQueryKeyVals[j+1]) } // call the function under test. - parsedPreSign, actualErrCode := parsePreSignV4(inputQuery, "") + parsedPreSign, actualErrCode := parsePreSignV4(inputQuery, "", serviceS3) if testCase.expectedErrCode != actualErrCode { t.Fatalf("Test %d: Expected the APIErrCode to be %d, got %d", i+1, testCase.expectedErrCode, actualErrCode) } diff --git a/cmd/signature-v4-utils.go b/cmd/signature-v4-utils.go index a042fbd10..f31ef0271 100644 --- a/cmd/signature-v4-utils.go +++ b/cmd/signature-v4-utils.go @@ -17,11 +17,16 @@ package cmd import ( + "bytes" + "context" "crypto/hmac" + "encoding/hex" + "io/ioutil" "net/http" "strconv" "strings" + "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" "github.com/minio/sha256-simd" ) @@ -53,7 +58,18 @@ func skipContentSha256Cksum(r *http.Request) bool { } // Returns SHA256 for calculating canonical-request. -func getContentSha256Cksum(r *http.Request) string { +func getContentSha256Cksum(r *http.Request, stype serviceType) string { + if stype == serviceSTS { + payload, err := ioutil.ReadAll(r.Body) + if err != nil { + logger.CriticalIf(context.Background(), err) + } + sum256 := sha256.New() + sum256.Write(payload) + r.Body = ioutil.NopCloser(bytes.NewReader(payload)) + return hex.EncodeToString(sum256.Sum(nil)) + } + var ( defaultSha256Cksum string v []string diff --git a/cmd/signature-v4-utils_test.go b/cmd/signature-v4-utils_test.go index dafea54a8..74bcc45cd 100644 --- a/cmd/signature-v4-utils_test.go +++ b/cmd/signature-v4-utils_test.go @@ -248,7 +248,7 @@ func TestGetContentSha256Cksum(t *testing.T) { if testCase.h != "" { r.Header.Set("x-amz-content-sha256", testCase.h) } - got := getContentSha256Cksum(r) + got := getContentSha256Cksum(r, serviceS3) if got != testCase.expected { t.Errorf("Test %d: got:%s expected:%s", i+1, got, testCase.expected) } diff --git a/cmd/signature-v4.go b/cmd/signature-v4.go index f393c5706..a6dc926dd 100644 --- a/cmd/signature-v4.go +++ b/cmd/signature-v4.go @@ -46,6 +46,13 @@ const ( yyyymmdd = "20060102" ) +type serviceType string + +const ( + serviceS3 serviceType = "s3" + serviceSTS serviceType = "sts" +) + // getCanonicalHeaders generate a list of request headers with their values func getCanonicalHeaders(signedHeaders http.Header) string { var headers []string @@ -110,7 +117,7 @@ func getScope(t time.Time, region string) string { scope := strings.Join([]string{ t.Format(yyyymmdd), region, - "s3", + string(serviceS3), "aws4_request", }, "/") return scope @@ -126,10 +133,10 @@ func getStringToSign(canonicalRequest string, t time.Time, scope string) string } // getSigningKey hmac seed to calculate final signature. -func getSigningKey(secretKey string, t time.Time, region string) []byte { +func getSigningKey(secretKey string, t time.Time, region string, stype serviceType) []byte { date := sumHMAC([]byte("AWS4"+secretKey), []byte(t.Format(yyyymmdd))) regionBytes := sumHMAC(date, []byte(region)) - service := sumHMAC(regionBytes, []byte("s3")) + service := sumHMAC(regionBytes, []byte(stype)) signingKey := sumHMAC(service, []byte("aws4_request")) return signingKey } @@ -165,7 +172,7 @@ func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode { region := globalServerConfig.GetRegion() // Parse credential tag. - credHeader, err := parseCredentialHeader("Credential="+formValues.Get("X-Amz-Credential"), region) + credHeader, err := parseCredentialHeader("Credential="+formValues.Get("X-Amz-Credential"), region, serviceS3) if err != ErrNone { return ErrMissingFields } @@ -176,7 +183,7 @@ func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode { } // Get signing key. - signingKey := getSigningKey(cred.SecretKey, credHeader.scope.date, credHeader.scope.region) + signingKey := getSigningKey(cred.SecretKey, credHeader.scope.date, credHeader.scope.region, serviceS3) // Get signature. newSignature := getSignature(signingKey, formValues.Get("Policy")) @@ -193,12 +200,12 @@ func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode { // doesPresignedSignatureMatch - Verify query headers with presigned signature // - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html // returns ErrNone if the signature matches. -func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region string) APIErrorCode { +func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region string, stype serviceType) APIErrorCode { // Copy request req := *r // Parse request query string. - pSignValues, err := parsePreSignV4(req.URL.Query(), region) + pSignValues, err := parsePreSignV4(req.URL.Query(), region, stype) if err != ErrNone { return err } @@ -240,7 +247,7 @@ func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region s query.Set("X-Amz-Date", t.Format(iso8601Format)) query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds)) query.Set("X-Amz-SignedHeaders", getSignedHeaders(extractedSignedHeaders)) - query.Set("X-Amz-Credential", cred.AccessKey+"/"+getScope(t, pSignValues.Credential.scope.region)) + query.Set("X-Amz-Credential", cred.AccessKey+"/"+pSignValues.Credential.getScope()) // Save other headers available in the request parameters. for k, v := range req.URL.Query() { @@ -291,7 +298,8 @@ func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region s presignedStringToSign := getStringToSign(presignedCanonicalReq, t, pSignValues.Credential.getScope()) // Get hmac presigned signing key. - presignedSigningKey := getSigningKey(cred.SecretKey, pSignValues.Credential.scope.date, pSignValues.Credential.scope.region) + presignedSigningKey := getSigningKey(cred.SecretKey, pSignValues.Credential.scope.date, + pSignValues.Credential.scope.region, stype) // Get new signature. newSignature := getSignature(presignedSigningKey, presignedStringToSign) @@ -306,7 +314,7 @@ func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region s // doesSignatureMatch - Verify authorization header with calculated header in accordance with // - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html // returns ErrNone if signature matches. -func doesSignatureMatch(hashedPayload string, r *http.Request, region string) APIErrorCode { +func doesSignatureMatch(hashedPayload string, r *http.Request, region string, stype serviceType) APIErrorCode { // Copy request. req := *r @@ -314,7 +322,7 @@ func doesSignatureMatch(hashedPayload string, r *http.Request, region string) AP v4Auth := req.Header.Get("Authorization") // Parse signature version '4' header. - signV4Values, err := parseSignV4(v4Auth, region) + signV4Values, err := parseSignV4(v4Auth, region, stype) if err != ErrNone { return err } @@ -354,7 +362,8 @@ func doesSignatureMatch(hashedPayload string, r *http.Request, region string) AP stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) // Get hmac signing key. - signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, signV4Values.Credential.scope.region) + signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, + signV4Values.Credential.scope.region, stype) // Calculate signature. newSignature := getSignature(signingKey, stringToSign) diff --git a/cmd/signature-v4_test.go b/cmd/signature-v4_test.go index 5be80ad54..bca419b7f 100644 --- a/cmd/signature-v4_test.go +++ b/cmd/signature-v4_test.go @@ -74,7 +74,7 @@ func TestDoesPolicySignatureMatch(t *testing.T) { "X-Amz-Date": []string{now.Format(iso8601Format)}, "X-Amz-Signature": []string{ getSignature(getSigningKey(globalServerConfig.GetCredential().SecretKey, now, - globalMinioDefaultRegion), "policy"), + globalMinioDefaultRegion, serviceS3), "policy"), }, "Policy": []string{"policy"}, }, @@ -293,7 +293,7 @@ func TestDoesPresignedSignatureMatch(t *testing.T) { } // Check if it matches! - err := doesPresignedSignatureMatch(payloadSHA256, req, testCase.region) + err := doesPresignedSignatureMatch(payloadSHA256, req, testCase.region, serviceS3) if err != testCase.expected { t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(err)) } diff --git a/cmd/streaming-signature-v4.go b/cmd/streaming-signature-v4.go index 84b76d45e..9f98db2f4 100644 --- a/cmd/streaming-signature-v4.go +++ b/cmd/streaming-signature-v4.go @@ -52,7 +52,7 @@ func getChunkSignature(cred auth.Credentials, seedSignature string, region strin hashedChunk // Get hmac signing key. - signingKey := getSigningKey(cred.SecretKey, date, region) + signingKey := getSigningKey(cred.SecretKey, date, region, serviceS3) // Calculate signature. newSignature := getSignature(signingKey, stringToSign) @@ -72,7 +72,7 @@ func calculateSeedSignature(r *http.Request) (cred auth.Credentials, signature s v4Auth := req.Header.Get("Authorization") // Parse signature version '4' header. - signV4Values, errCode := parseSignV4(v4Auth, globalServerConfig.GetRegion()) + signV4Values, errCode := parseSignV4(v4Auth, globalServerConfig.GetRegion(), serviceS3) if errCode != ErrNone { return cred, "", "", time.Time{}, errCode } @@ -124,7 +124,7 @@ func calculateSeedSignature(r *http.Request) (cred auth.Credentials, signature s stringToSign := getStringToSign(canonicalRequest, date, signV4Values.Credential.getScope()) // Get hmac signing key. - signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, region) + signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, region, serviceS3) // Calculate signature. newSignature := getSignature(signingKey, stringToSign) diff --git a/cmd/sts-datatypes.go b/cmd/sts-datatypes.go index c37c3bf97..de1da0c5d 100644 --- a/cmd/sts-datatypes.go +++ b/cmd/sts-datatypes.go @@ -42,6 +42,39 @@ type AssumedRoleUser struct { // contains filtered or unexported fields } +// AssumeRoleResponse contains the result of successful AssumeRole request. +type AssumeRoleResponse struct { + XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleResponse" json:"-"` + + Result AssumeRoleResult `xml:"AssumeRoleResult"` + ResponseMetadata struct { + RequestID string `xml:"RequestId,omitempty"` + } `xml:"ResponseMetadata,omitempty"` +} + +// AssumeRoleResult - Contains the response to a successful AssumeRole +// request, including temporary credentials that can be used to make +// Minio API requests. +type AssumeRoleResult struct { + // The identifiers for the temporary security credentials that the operation + // returns. + AssumedRoleUser AssumedRoleUser `xml:",omitempty"` + + // The temporary security credentials, which include an access key ID, a secret + // access key, and a security (or session) token. + // + // Note: The size of the security token that STS APIs return is not fixed. We + // strongly recommend that you make no assumptions about the maximum size. As + // of this writing, the typical size is less than 4096 bytes, but that can vary. + // Also, future updates to AWS might require larger sizes. + Credentials auth.Credentials `xml:",omitempty"` + + // A percentage value that indicates the size of the policy in packed form. + // The service rejects any policy with a packed size greater than 100 percent, + // which means the policy exceeded the allowed space. + PackedPolicySize int `xml:",omitempty"` +} + // AssumeRoleWithWebIdentityResponse contains the result of successful AssumeRoleWithWebIdentity request. type AssumeRoleWithWebIdentityResponse struct { XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithWebIdentityResponse" json:"-"` diff --git a/cmd/sts-errors.go b/cmd/sts-errors.go index 78c641180..9d26a0fb3 100644 --- a/cmd/sts-errors.go +++ b/cmd/sts-errors.go @@ -53,6 +53,7 @@ type STSErrorCode int // Error codes, non exhaustive list - http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithSAML.html const ( ErrSTSNone STSErrorCode = iota + ErrSTSAccessDenied ErrSTSMissingParameter ErrSTSInvalidParameterValue ErrSTSWebIdentityExpiredToken @@ -76,6 +77,11 @@ func (e stsErrorCodeMap) ToSTSErr(errCode STSErrorCode) STSError { // error code to STSError structure, these fields carry respective // descriptions for all the error responses. var stsErrCodes = stsErrorCodeMap{ + ErrSTSAccessDenied: { + Code: "AccessDenied", + Description: "Generating temporary credentials not allowed for this request.", + HTTPStatusCode: http.StatusForbidden, + }, ErrSTSMissingParameter: { Code: "MissingParameter", Description: "A required parameter for the specified action is not supplied.", diff --git a/cmd/sts-handlers.go b/cmd/sts-handlers.go index 6a40cb8eb..a18a50475 100644 --- a/cmd/sts-handlers.go +++ b/cmd/sts-handlers.go @@ -1,5 +1,5 @@ /* - * Minio Cloud Storage, (C) 2018 Minio, Inc. + * Minio Cloud Storage, (C) 2018, 2019 Minio, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package cmd import ( + "context" "fmt" "net/http" @@ -33,6 +34,7 @@ const ( // STS API action constants clientGrants = "AssumeRoleWithClientGrants" webIdentity = "AssumeRoleWithWebIdentity" + assumeRole = "AssumeRole" ) // stsAPIHandlers implements and provides http handlers for AWS STS API. @@ -46,6 +48,11 @@ func registerSTSRouter(router *mux.Router) { // STS Router stsRouter := router.NewRoute().PathPrefix("/").Subrouter() + // Assume roles with no JWT, handles AssumeRole. + stsRouter.Methods("POST").HeadersRegexp("Content-Type", "application/x-www-form-urlencoded*"). + HeadersRegexp("Authorization", "AWS4-HMAC-SHA256*"). + HandlerFunc(httpTraceAll(sts.AssumeRole)) + // Assume roles with JWT handler, handles both ClientGrants and WebIdentity. stsRouter.Methods("POST").HeadersRegexp("Content-Type", "application/x-www-form-urlencoded*"). HandlerFunc(httpTraceAll(sts.AssumeRoleWithJWT)) @@ -64,8 +71,136 @@ func registerSTSRouter(router *mux.Router) { } +func checkAssumeRoleAuth(ctx context.Context, r *http.Request) (user auth.Credentials, stsErr STSErrorCode) { + switch getRequestAuthType(r) { + default: + return user, ErrSTSAccessDenied + case authTypeSigned: + s3Err := isReqAuthenticated(ctx, r, globalServerConfig.GetRegion(), serviceSTS) + if STSErrorCode(s3Err) != ErrSTSNone { + return user, STSErrorCode(s3Err) + } + var owner bool + user, owner, s3Err = getReqAccessKeyV4(r, globalServerConfig.GetRegion(), serviceSTS) + if STSErrorCode(s3Err) != ErrSTSNone { + return user, STSErrorCode(s3Err) + } + // Root credentials are not allowed to use STS API + if owner { + return user, ErrSTSAccessDenied + } + } + + // Session tokens are not allowed in STS AssumeRole requests. + if getSessionToken(r) != "" { + return user, ErrSTSAccessDenied + } + + return user, 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, stsErr := checkAssumeRoleAuth(ctx, r) + if stsErr != ErrSTSNone { + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(stsErr)) + return + } + + if err := r.ParseForm(); err != nil { + logger.LogIf(ctx, err) + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue)) + return + } + + if r.Form.Get("Policy") != "" { + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue)) + return + } + + if r.Form.Get("Version") != stsAPIVersion { + logger.LogIf(ctx, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion)) + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSMissingParameter)) + return + } + + action := r.Form.Get("Action") + switch action { + case assumeRole: + default: + logger.LogIf(ctx, fmt.Errorf("Unsupported action %s", action)) + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue)) + return + } + + ctx = newContext(r, w, action) + defer logger.AuditLog(w, r, action, nil) + + var err error + m := make(map[string]interface{}) + m["exp"], err = validator.GetDefaultExpiration(r.Form.Get("DurationSeconds")) + if err != nil { + switch err { + case validator.ErrInvalidDuration: + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue)) + default: + logger.LogIf(ctx, err) + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue)) + } + return + } + + policyName, err := globalIAMSys.GetUserPolicy(user.AccessKey) + if err != nil { + logger.LogIf(ctx, err) + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue)) + return + } + + // This policy is the policy associated with the user + // requesting for temporary credentials. The temporary + // credentials will inherit the same policy requirements. + m["policy"] = policyName + + secret := globalServerConfig.GetCredential().SecretKey + cred, err := auth.GetNewCredentialsWithMetadata(m, secret) + if err != nil { + logger.LogIf(ctx, err) + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInternalError)) + return + } + + // Set the newly generated credentials. + if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil { + logger.LogIf(ctx, err) + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInternalError)) + return + } + + // Notify all other Minio peers to reload temp users + for _, nerr := range globalNotificationSys.LoadUsers() { + 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(responseRequestIDKey) + writeSuccessResponseXML(w, encodeResponse(assumeRoleResponse)) +} + func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "AssumeRoleInternalFunction") + ctx := newContext(r, w, "AssumeRoleJWTCommon") // Parse the incoming form data. if err := r.ParseForm(); err != nil { @@ -74,6 +209,11 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ return } + if r.Form.Get("Policy") != "" { + writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue)) + return + } + if r.Form.Get("Version") != stsAPIVersion { logger.LogIf(ctx, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion)) writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSMissingParameter)) @@ -169,19 +309,23 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ var encodedSuccessResponse []byte switch action { case clientGrants: - encodedSuccessResponse = encodeResponse(&AssumeRoleWithClientGrantsResponse{ + clientGrantsResponse := &AssumeRoleWithClientGrantsResponse{ Result: ClientGrantsResult{ Credentials: cred, SubjectFromToken: subFromToken, }, - }) + } + clientGrantsResponse.ResponseMetadata.RequestID = w.Header().Get(responseRequestIDKey) + encodedSuccessResponse = encodeResponse(clientGrantsResponse) case webIdentity: - encodedSuccessResponse = encodeResponse(&AssumeRoleWithWebIdentityResponse{ + webIdentityResponse := &AssumeRoleWithWebIdentityResponse{ Result: WebIdentityResult{ Credentials: cred, SubjectFromWebIdentityToken: subFromToken, }, - }) + } + webIdentityResponse.ResponseMetadata.RequestID = w.Header().Get(responseRequestIDKey) + encodedSuccessResponse = encodeResponse(webIdentityResponse) } writeSuccessResponseXML(w, encodedSuccessResponse) diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go index b2fdfc039..89b36fc55 100644 --- a/cmd/test-utils_test.go +++ b/cmd/test-utils_test.go @@ -680,7 +680,7 @@ func signStreamingRequest(req *http.Request, accessKey, secretKey string, currTi scope := strings.Join([]string{ currTime.Format(yyyymmdd), globalMinioDefaultRegion, - "s3", + string(serviceS3), "aws4_request", }, "/") @@ -690,7 +690,7 @@ func signStreamingRequest(req *http.Request, accessKey, secretKey string, currTi date := sumHMAC([]byte("AWS4"+secretKey), []byte(currTime.Format(yyyymmdd))) region := sumHMAC(date, []byte(globalMinioDefaultRegion)) - service := sumHMAC(region, []byte("s3")) + service := sumHMAC(region, []byte(string(serviceS3))) signingKey := sumHMAC(service, []byte("aws4_request")) signature := hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) @@ -760,7 +760,7 @@ func assembleStreamingChunks(req *http.Request, body io.ReadSeeker, chunkSize in scope := strings.Join([]string{ currTime.Format(yyyymmdd), regionStr, - "s3", + string(serviceS3), "aws4_request", }, "/") @@ -773,7 +773,7 @@ func assembleStreamingChunks(req *http.Request, body io.ReadSeeker, chunkSize in date := sumHMAC([]byte("AWS4"+secretKey), []byte(currTime.Format(yyyymmdd))) region := sumHMAC(date, []byte(regionStr)) - service := sumHMAC(region, []byte("s3")) + service := sumHMAC(region, []byte(serviceS3)) signingKey := sumHMAC(service, []byte("aws4_request")) signature = hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) @@ -874,7 +874,7 @@ func preSignV4(req *http.Request, accessKeyID, secretAccessKey string, expires i queryStr := strings.Replace(query.Encode(), "+", "%20", -1) canonicalRequest := getCanonicalRequest(extractedSignedHeaders, unsignedPayload, queryStr, req.URL.Path, req.Method) stringToSign := getStringToSign(canonicalRequest, date, scope) - signingKey := getSigningKey(secretAccessKey, date, region) + signingKey := getSigningKey(secretAccessKey, date, region, serviceS3) signature := getSignature(signingKey, stringToSign) req.URL.RawQuery = query.Encode() @@ -1035,7 +1035,7 @@ func signRequestV4(req *http.Request, accessKey, secretKey string) error { scope := strings.Join([]string{ currTime.Format(yyyymmdd), region, - "s3", + string(serviceS3), "aws4_request", }, "/") @@ -1045,7 +1045,7 @@ func signRequestV4(req *http.Request, accessKey, secretKey string) error { date := sumHMAC([]byte("AWS4"+secretKey), []byte(currTime.Format(yyyymmdd))) regionHMAC := sumHMAC(date, []byte(region)) - service := sumHMAC(regionHMAC, []byte("s3")) + service := sumHMAC(regionHMAC, []byte(serviceS3)) signingKey := sumHMAC(service, []byte("aws4_request")) signature := hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) diff --git a/cmd/web-handlers.go b/cmd/web-handlers.go index c13965ad9..58fda6087 100644 --- a/cmd/web-handlers.go +++ b/cmd/web-handlers.go @@ -47,7 +47,7 @@ import ( "github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/hash" - "github.com/minio/minio/pkg/iam/policy" + iampolicy "github.com/minio/minio/pkg/iam/policy" "github.com/minio/minio/pkg/ioutil" "github.com/minio/minio/pkg/policy" ) @@ -1753,7 +1753,7 @@ func presignedGet(host, bucket, object string, expiry int64, creds auth.Credenti extractedSignedHeaders.Set("host", host) canonicalRequest := getCanonicalRequest(extractedSignedHeaders, unsignedPayload, queryStr, path, "GET") stringToSign := getStringToSign(canonicalRequest, date, getScope(date, region)) - signingKey := getSigningKey(secretKey, date, region) + signingKey := getSigningKey(secretKey, date, region, serviceS3) signature := getSignature(signingKey, stringToSign) // Construct the final presigned URL. diff --git a/docs/sts/README.md b/docs/sts/README.md index 4064cd15f..d0d99f506 100644 --- a/docs/sts/README.md +++ b/docs/sts/README.md @@ -13,27 +13,29 @@ Following are advantages for using temporary credentials: ## Identity Federation - [**Client grants**](https://github.com/minio/minio/blob/master/docs/sts/client-grants.md) - Let applications request `client_grants` using any well-known third party identity provider such as KeyCloak, WSO2. This is known as the client grants approach to temporary access. Using this approach helps clients keep Minio credentials to be secured. Minio STS supports client grants, tested against identity providers such as WSO2, KeyCloak. - [**WebIdentity**](https://github.com/minio/minio/blob/master/docs/sts/web-identity.md) - Let users request temporary credentials using any OpenID(OIDC) compatible web identity providers such as Facebook, Google etc. +- [**AssumeRole**](https://github.com/minio/minio/blob/master/docs/sts/assume-role.md) - Let Minio users request temporary credentials using user access and secret keys. ## Get started In this document we will explain in detail on how to configure all the prerequisites, primarily WSO2, OPA (open policy agent). +> NOTE: If you are interested in AssumeRole API only, skip to [here](https://github.com/minio/minio/blob/master/docs/sts/assume-role.md) + ### 1. Prerequisites - [Configuring wso2](https://github.com/minio/minio/blob/master/docs/sts/wso2.md) -- [Configuring opa](https://github.com/minio/minio/blob/master/docs/sts/opa.md) +- [Configuring opa (optional)](https://github.com/minio/minio/blob/master/docs/sts/opa.md) - [Configuring etcd (optional needed only in gateway or federation mode)](https://github.com/minio/minio/blob/master/docs/sts/etcd.md) -### 2. Setup Minio with WSO2, OPA +### 2. Setup Minio with WSO2 Make sure we have followed the previous step and configured each software independently, once done we can now proceed to use Minio STS API and Minio server to use these credentials to perform object API operations. ``` export MINIO_ACCESS_KEY=minio export MINIO_SECRET_KEY=minio123 export MINIO_IAM_JWKS_URL=https://localhost:9443/oauth2/jwks -export MINIO_IAM_OPA_URL=http://localhost:8181/v1/data/httpapi/authz minio server /mnt/data ``` -### 3. Setup Minio Gateway with WSO2, OPA, ETCD +### 3. Setup Minio Gateway with WSO2, ETCD Make sure we have followed the previous step and configured each software independently, once done we can now proceed to use Minio STS API and Minio gateway to use these credentials to perform object API operations. > NOTE: Minio gateway requires etcd to be configured to use STS API. @@ -42,7 +44,6 @@ Make sure we have followed the previous step and configured each software indepe export MINIO_ACCESS_KEY=aws_access_key export MINIO_SECRET_KEY=aws_secret_key export MINIO_IAM_JWKS_URL=https://localhost:9443/oauth2/jwks -export MINIO_IAM_OPA_URL=http://localhost:8181/v1/data/httpapi/authz export MINIO_ETCD_ENDPOINTS=http://localhost:2379 minio gateway s3 ``` diff --git a/docs/sts/assume-role.md b/docs/sts/assume-role.md new file mode 100644 index 000000000..cfcc9a6a2 --- /dev/null +++ b/docs/sts/assume-role.md @@ -0,0 +1,97 @@ +## AssumeRole [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) +Returns a set of temporary security credentials that you can use to access Minio resources. AssumeRole requires authorization credentials for an existing user on Minio. The advantages of this API are + +- To be able to reliably use S3 multipart APIs feature of the SDKs without re-inventing the wheel of pre-signing the each URL in multipart API. This is very tedious to implement with all the scenarios of fault tolerance that's already implemented by the client SDK. The general client SDKs don't support multipart with presigned URLs. +- To be able to easily get the temporary credentials to upload to a prefix. Make it possible for a client to upload a whole folder using the session. The server side applications need not create a presigned URL and serve to the client for each file. Since, the client would have the session it can do it by itself. + +The temporary security credentials returned by this API consists of an access key, a secret key, and a security token. Applications can use these temporary security credentials to sign calls to Minio API operations. The policy applied to these temporary credentials is inherited from the Minio user credentials. By default, the temporary security credentials created by AssumeRole last for one hour. However, use the optional DurationSeconds parameter to specify the duration of the credentials. This value varies from 900 seconds (15 minutes) up to the maximum session duration to 12 hours. + +### Request Parameters +#### DurationSeconds +The duration, in seconds. The value can range from 900 seconds (15 minutes) up to 12 hours. If value is higher than this setting, then operation fails. By default, the value is set to 3600 seconds. + +| Params | Value | +| :-- | :-- | +| *Type* | *Integer* | +| *Valid Range* | *Minimum value of 900. Maximum value of 43200.* | +| *Required* | *No* | + +#### Version +Indicates STS API version information, the only supported value is '2011-06-15'. This value is borrowed from AWS STS API documentation for compatibility reasons. + +| Params | Value | +| :-- | :-- | +| *Type* | *String* | +| *Required* | *Yes* | + +#### AUTHPARAMS +Indicates STS API Authorization information. If you are familiar with AWS Signature V4 Authorization header, this STS API supports signature V4 authorization as mentioned [here](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) + +#### Response Elements +XML response for this API is similar to [AWS STS AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html#API_AssumeRole_ResponseElements) + +#### Errors +XML error response for this API is similar to [AWS STS AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html#API_AssumeRole_Errors) + +#### Sample Request +``` +http://minio:9000/?Action=AssumeRole&DurationSeconds=3600&Version=2011-06-15&AUTHPARAMS +``` + +#### Sample Response +``` + + + + + + + + + Y4RJU1RNFGK48LGO9I2S + sYLRKS1Z7hSjluf6gEbb9066hnx315wHTiACPAjg + 2018-11-09T16:51:11-08:00 + eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJZNFJKVTFSTkZHSzQ4TEdPOUkyUyIsImF1ZCI6IlBvRWdYUDZ1Vk80NUlzRU5SbmdEWGo1QXU1WWEiLCJhenAiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiZXhwIjoxNTQxODExMDcxLCJpYXQiOjE1NDE4MDc0NzEsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojk0NDMvb2F1dGgyL3Rva2VuIiwianRpIjoiYTBiMjc2MjktZWUxYS00M2JmLTg3MzktZjMzNzRhNGNkYmMwIn0.ewHqKVFTaP-j_kgZrcOEKroNUjk10GEp8bqQjxBbYVovV0nHO985VnRESFbcT6XMDDKHZiWqN2vi_ETX_u3Q-w + + + + c6104cbe-af31-11e0-8154-cbc7ccf896c7 + + +``` + +#### Testing +``` +$ export MINIO_ACCESS_KEY=minio +$ export MINIO_SECRET_KEY=minio123 +$ minio server ~/test +``` + +Create new users following the multi-user guide [here](https://docs.minio.io/docs/minio-multi-user-quickstart-guide.html) + +Testing with an example +> Use the same username and password created in the previous steps. + +``` +[foobar] +region = us-east-1 +aws_access_key_id = foobar +aws_secret_access_key = foo12345 +``` + +> NOTE: In the following commands `--role-arn` and `--role-session-name` are not meaningful for Minio and can be set to any value satisfying the command line requirements. + +``` +$ aws --profile foobar --endpoint-url http://localhost:9000 sts assume-role --role-arn arn:xxx:xxx:xxx:xxx --role-session-name anything +{ + "AssumedRoleUser": { + "Arn": "" + }, + "Credentials": { + "SecretAccessKey": "xbnWUoNKgFxi+uv3RI9UgqP3tULQMdI+Hj+4psd4", + "SessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJLOURUSU1VVlpYRVhKTDNBVFVPWSIsImV4cCI6MzYwMDAwMDAwMDAwMCwicG9saWN5IjoidGVzdCJ9.PetK5wWUcnCJkMYv6TEs7HqlA4x_vViykQ8b2T_6hapFGJTO34sfTwqBnHF6lAiWxRoZXco11B0R7y58WAsrQw", + "Expiration": "2019-02-20T19:56:59-08:00", + "AccessKeyId": "K9DTIMUVZXEXJL3ATUOY" + } +} +``` diff --git a/docs/sts/client-grants.md b/docs/sts/client-grants.md index 66d1e8a04..c02d97dca 100644 --- a/docs/sts/client-grants.md +++ b/docs/sts/client-grants.md @@ -1,13 +1,13 @@ ## AssumeRoleWithClientGrants [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) Returns a set of temporary security credentials for applications/clients who have been authenticated through client credential grants provided by identity provider. Example providers include WSO2, KeyCloak etc. -Calling AssumeRoleWithClientGrants does not require the use of Minio default credentials. Therefore, client application can be distributed that requests temporary security credentials without including Minio default credentials. Instead, the identity of the caller is validated by using a JWT access token from the identity provider. The temporary security credentials returned by this API consist of an access key, a secret key, and a security token. Applications can use these temporary security credentials to sign calls to Minio API operations. +Calling AssumeRoleWithClientGrants does not require the use of Minio default credentials. Therefore, client application can be distributed that requests temporary security credentials without including Minio default credentials. Instead, the identity of the caller is validated by using a JWT access token from the identity provider. The temporary security credentials returned by this API consists of an access key, a secret key, and a security token. Applications can use these temporary security credentials to sign calls to Minio API operations. By default, the temporary security credentials created by AssumeRoleWithClientGrants last for one hour. However, use the optional DurationSeconds parameter to specify the duration of the credentials. This value varies from 900 seconds (15 minutes) up to the maximum session duration to 12 hours. ### Request Parameters #### DurationSeconds -The duration, in seconds. The value can range from 900 seconds (15 minutes) up to the 12 hours. If value is higher than this setting, then operation fails. By default, the value is set to 3600 seconds. +The duration, in seconds. The value can range from 900 seconds (15 minutes) up to 12 hours. If value is higher than this setting, then operation fails. By default, the value is set to 3600 seconds. | Params | Value | | :-- | :-- | @@ -93,7 +93,7 @@ Testing with an example > Obtaining client ID and secrets follow [WSO2 configuring documentation](https://github.com/minio/minio/blob/master/docs/sts/wso2.md) ``` -go run client-grants.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga +$ go run client-grants.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga ##### Credentials { diff --git a/docs/sts/opa.md b/docs/sts/opa.md index fe204a8b1..4b010f07d 100644 --- a/docs/sts/opa.md +++ b/docs/sts/opa.md @@ -1,3 +1,5 @@ +**Using OPA is optional with Minio. We recommend using [`policy` JWT claims](https://github.com/minio/minio/blob/master/docs/sts/wso2.md#4-jwt-claims) instead, let Minio manage your policies using `mc admin policy` and apply them on the STS credentials.** + # OPA Quickstart Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) OPA is a lightweight general-purpose policy engine that can be co-located with Minio server, in this document we talk about how to use OPA HTTP API to authorize Minio STS credentials. diff --git a/docs/sts/web-identity.md b/docs/sts/web-identity.md index 27d041bb6..8548fcd8b 100644 --- a/docs/sts/web-identity.md +++ b/docs/sts/web-identity.md @@ -1,11 +1,11 @@ ## AssumeRoleWithWebIdentity [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) -Calling AssumeRoleWithWebIdentity does not require the use of Minio default credentials. Therefore, you can distribute an application (for example, on mobile devices) that requests temporary security credentials without including Minio default credentials in the application. Instead, the identity of the caller is validated by using a JWT access token from the web identity provider. The temporary security credentials returned by this API consist of an access key, a secret key, and a security token. Applications can use these temporary security credentials to sign calls to Minio API operations. +Calling AssumeRoleWithWebIdentity does not require the use of Minio default credentials. Therefore, you can distribute an application (for example, on mobile devices) that requests temporary security credentials without including Minio default credentials in the application. Instead, the identity of the caller is validated by using a JWT access token from the web identity provider. The temporary security credentials returned by this API consists of an access key, a secret key, and a security token. Applications can use these temporary security credentials to sign calls to Minio API operations. By default, the temporary security credentials created by AssumeRoleWithWebIdentity last for one hour. However, use the optional DurationSeconds parameter to specify the duration of the credentials. This value varies from 900 seconds (15 minutes) up to the maximum session duration to 12 hours. ### Request Parameters #### DurationSeconds -The duration, in seconds. The value can range from 900 seconds (15 minutes) up to the 12 hours. If value is higher than this setting, then operation fails. By default, the value is set to 3600 seconds. +The duration, in seconds. The value can range from 900 seconds (15 minutes) up to 12 hours. If value is higher than this setting, then operation fails. By default, the value is set to 3600 seconds. | Params | Value | | :-- | :-- | @@ -83,7 +83,7 @@ Testing with an example > Visit [Google Developer Console](https://console.cloud.google.com) under Project, APIs, Credentials to get your OAuth2 client credentials. Add `http://localhost:8080/oauth2/callback` as a valid OAuth2 Redirect URL. ``` -go run web-identity.go -cid 204367807228-ok7601k6gj1pgge7m09h7d79co8p35xx.apps.googleusercontent.com -csec XsT_PgPdT1nO9DD45rMLJw7G +$ go run web-identity.go -cid 204367807228-ok7601k6gj1pgge7m09h7d79co8p35xx.apps.googleusercontent.com -csec XsT_PgPdT1nO9DD45rMLJw7G 2018/12/26 17:49:36 listening on http://localhost:8080/ ``` diff --git a/docs/sts/wso2.md b/docs/sts/wso2.md index 828653def..9d60cfb27 100644 --- a/docs/sts/wso2.md +++ b/docs/sts/wso2.md @@ -60,11 +60,11 @@ The access token received is a signed JSON Web Token (JWT). Use a JWT decoder to |iat| _integer_ | The token issue time. | |exp| _integer_ | The token expiration time. | |jti| _string_ | Unique identifier for the JWT token. | -|policy| _string_ | Canned policy name to be applied for STS credentials. (Optional) | +|policy| _string_ | Canned policy name to be applied for STS credentials. (Recommended) | Using the above `access_token` we can perform an STS request to Minio to get temporary credentials for Minio API operations. Minio STS API uses [JSON Web Key Set Endpoint](https://docs.wso2.com/display/IS541/JSON+Web+Key+Set+Endpoint) to validate if JWT is valid and is properly signed. -Optionally you can also configure `policy` as a custom claim for the JWT service provider follow [here](https://docs.wso2.com/display/IS550/Configuring+Claims+for+a+Service+Provider) and [here](https://docs.wso2.com/display/IS550/Handling+Custom+Claims+with+the+JWT+Bearer+Grant+Type) for relevant docs on how to configure claims for a service provider. +**We recommend setting `policy` as a custom claim for the JWT service provider follow [here](https://docs.wso2.com/display/IS550/Configuring+Claims+for+a+Service+Provider) and [here](https://docs.wso2.com/display/IS550/Handling+Custom+Claims+with+the+JWT+Bearer+Grant+Type) for relevant docs on how to configure claims for a service provider.** ### 5. Setup Minio with JWKS URL Minio server expects environment variable for JWKS url as `MINIO_IAM_JWKS_URL`, this environment variable takes a single entry. diff --git a/pkg/auth/credentials.go b/pkg/auth/credentials.go index bba029fe6..a7a109e5b 100644 --- a/pkg/auth/credentials.go +++ b/pkg/auth/credentials.go @@ -20,6 +20,8 @@ import ( "crypto/rand" "crypto/subtle" "encoding/base64" + "encoding/json" + "errors" "fmt" "strings" "time" @@ -104,6 +106,27 @@ func (cred Credentials) Equal(ccred Credentials) bool { var timeSentinel = time.Unix(0, 0).UTC() +func expToInt64(expI interface{}) (expAt int64, err error) { + switch exp := expI.(type) { + case float64: + expAt = int64(exp) + case int64: + expAt = exp + case json.Number: + expAt, err = exp.Int64() + if err != nil { + return 0, err + } + case time.Duration: + return time.Now().UTC().Add(exp).Unix(), nil + case nil: + return 0, nil + default: + return 0, errors.New("invalid expiry value") + } + return expAt, nil +} + // GetNewCredentialsWithMetadata generates and returns new credential with expiry. func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string) (cred Credentials, err error) { readBytes := func(size int) (data []byte, err error) { @@ -135,8 +158,11 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string) cred.SecretKey = strings.Replace(string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLen]), "/", "+", -1) cred.Status = "enabled" - expiry, ok := m["exp"].(float64) - if !ok { + expiry, err := expToInt64(m["exp"]) + if err != nil { + return cred, err + } + if expiry == 0 { cred.Expiration = timeSentinel return cred, nil } @@ -144,7 +170,7 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string) m["accessKey"] = cred.AccessKey jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims(m)) - cred.Expiration = time.Unix(int64(expiry), 0) + cred.Expiration = time.Unix(expiry, 0) cred.SessionToken, err = jwt.SignedString([]byte(tokenSecret)) if err != nil { return cred, err diff --git a/pkg/iam/validator/jwt.go b/pkg/iam/validator/jwt.go index 435cb9efe..af2b513fb 100644 --- a/pkg/iam/validator/jwt.go +++ b/pkg/iam/validator/jwt.go @@ -130,23 +130,24 @@ func expToInt64(expI interface{}) (expAt int64, err error) { return 0, err } default: - return 0, errors.New("invalid expiry value") + return 0, ErrInvalidDuration } return expAt, nil } -func getDefaultExpiration(dsecs string) (time.Duration, error) { +// GetDefaultExpiration - returns the expiration seconds expected. +func GetDefaultExpiration(dsecs string) (time.Duration, error) { defaultExpiryDuration := time.Duration(60) * time.Minute // Defaults to 1hr. if dsecs != "" { expirySecs, err := strconv.ParseInt(dsecs, 10, 64) if err != nil { - return 0, err + return 0, ErrInvalidDuration } // The duration, in seconds, of the role session. // The value can range from 900 seconds (15 minutes) // to 12 hours. if expirySecs < 900 || expirySecs > 43200 { - return 0, errors.New("out of range value for duration in seconds") + return 0, ErrInvalidDuration } defaultExpiryDuration = time.Duration(expirySecs) * time.Second @@ -201,7 +202,7 @@ func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) { } if !jwtToken.Valid { - return nil, fmt.Errorf("Invalid token: %v", token) + return nil, ErrTokenExpired } expAt, err := expToInt64(claims["exp"]) @@ -209,7 +210,7 @@ func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) { return nil, err } - defaultExpiryDuration, err := getDefaultExpiration(dsecs) + defaultExpiryDuration, err := GetDefaultExpiration(dsecs) if err != nil { return nil, err } diff --git a/pkg/iam/validator/jwt_test.go b/pkg/iam/validator/jwt_test.go index 13f9712c2..2ba68e171 100644 --- a/pkg/iam/validator/jwt_test.go +++ b/pkg/iam/validator/jwt_test.go @@ -108,7 +108,7 @@ func TestDefaultExpiryDuration(t *testing.T) { if err != nil { t.Fatal(err) } - d, err := getDefaultExpiration(u.Query().Get("DurationSeconds")) + d, err := GetDefaultExpiration(u.Query().Get("DurationSeconds")) gotErr := (err != nil) if testCase.expectErr != gotErr { t.Errorf("Test %d: Expected %v, got %v with error %s", i+1, testCase.expectErr, gotErr, err)