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
This commit is contained in:
Harshavardhana 2019-02-27 17:46:55 -08:00 committed by kannappanr
parent ce588d1489
commit c3ca954684
27 changed files with 446 additions and 89 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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()

View file

@ -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)
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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))
}

View file

@ -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)

View file

@ -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:"-"`

View file

@ -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.",

View file

@ -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)

View file

@ -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)))

View file

@ -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.

View file

@ -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
```

97
docs/sts/assume-role.md Normal file
View file

@ -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
```
<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleResult>
<AssumedRoleUser>
<Arn/>
<AssumeRoleId/>
</AssumedRoleUser>
<Credentials>
<AccessKeyId>Y4RJU1RNFGK48LGO9I2S</AccessKeyId>
<SecretAccessKey>sYLRKS1Z7hSjluf6gEbb9066hnx315wHTiACPAjg</SecretAccessKey>
<Expiration>2018-11-09T16:51:11-08:00</Expiration>
<SessionToken>eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJZNFJKVTFSTkZHSzQ4TEdPOUkyUyIsImF1ZCI6IlBvRWdYUDZ1Vk80NUlzRU5SbmdEWGo1QXU1WWEiLCJhenAiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiZXhwIjoxNTQxODExMDcxLCJpYXQiOjE1NDE4MDc0NzEsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojk0NDMvb2F1dGgyL3Rva2VuIiwianRpIjoiYTBiMjc2MjktZWUxYS00M2JmLTg3MzktZjMzNzRhNGNkYmMwIn0.ewHqKVFTaP-j_kgZrcOEKroNUjk10GEp8bqQjxBbYVovV0nHO985VnRESFbcT6XMDDKHZiWqN2vi_ETX_u3Q-w</SessionToken>
</Credentials>
</AssumeRoleResult>
<ResponseMetadata>
<RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
</ResponseMetadata>
</AssumeRoleResponse>
```
#### 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"
}
}
```

View file

@ -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
{

View file

@ -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.

View file

@ -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/
```

View file

@ -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.

View file

@ -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

View file

@ -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
}

View file

@ -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)