mirror of
https://github.com/minio/minio.git
synced 2024-12-24 06:05:55 -05:00
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:
parent
ce588d1489
commit
c3ca954684
@ -32,7 +32,7 @@ import (
|
|||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"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"
|
"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.
|
// We only support admin credentials to access admin APIs.
|
||||||
|
|
||||||
var owner bool
|
var owner bool
|
||||||
_, owner, s3Err = getReqAccessKeyV4(r, region)
|
_, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return s3Err
|
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
|
// we only support V4 (no presign) with auth body
|
||||||
s3Err = isReqAuthenticated(ctx, r, region)
|
s3Err = isReqAuthenticated(ctx, r, region, serviceS3)
|
||||||
}
|
}
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("requestHeaders", dumpRequest(r))
|
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:
|
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
|
||||||
region = ""
|
region = ""
|
||||||
}
|
}
|
||||||
if s3Err = isReqAuthenticated(ctx, r, region); s3Err != ErrNone {
|
if s3Err = isReqAuthenticated(ctx, r, region, serviceS3); s3Err != ErrNone {
|
||||||
return s3Err
|
return s3Err
|
||||||
}
|
}
|
||||||
cred, owner, s3Err = getReqAccessKeyV4(r, region)
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
}
|
}
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return s3Err
|
return s3Err
|
||||||
@ -314,21 +314,21 @@ func isReqAuthenticatedV2(r *http.Request) (s3Error APIErrorCode) {
|
|||||||
return doesPresignV2SignatureMatch(r)
|
return doesPresignV2SignatureMatch(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func reqSignatureV4Verify(r *http.Request, region string) (s3Error APIErrorCode) {
|
func reqSignatureV4Verify(r *http.Request, region string, stype serviceType) (s3Error APIErrorCode) {
|
||||||
sha256sum := getContentSha256Cksum(r)
|
sha256sum := getContentSha256Cksum(r, stype)
|
||||||
switch {
|
switch {
|
||||||
case isRequestSignatureV4(r):
|
case isRequestSignatureV4(r):
|
||||||
return doesSignatureMatch(sha256sum, r, region)
|
return doesSignatureMatch(sha256sum, r, region, stype)
|
||||||
case isRequestPresignedSignatureV4(r):
|
case isRequestPresignedSignatureV4(r):
|
||||||
return doesPresignedSignatureMatch(sha256sum, r, region)
|
return doesPresignedSignatureMatch(sha256sum, r, region, stype)
|
||||||
default:
|
default:
|
||||||
return ErrAccessDenied
|
return ErrAccessDenied
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if request has valid AWS Signature Version '4'.
|
// Verify if request has valid AWS Signature Version '4'.
|
||||||
func isReqAuthenticated(ctx context.Context, r *http.Request, region string) (s3Error APIErrorCode) {
|
func isReqAuthenticated(ctx context.Context, r *http.Request, region string, stype serviceType) (s3Error APIErrorCode) {
|
||||||
if errCode := reqSignatureV4Verify(r, region); errCode != ErrNone {
|
if errCode := reqSignatureV4Verify(r, region, stype); errCode != ErrNone {
|
||||||
return errCode
|
return errCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,7 +432,7 @@ func isPutAllowed(atype authType, bucketName, objectName string, r *http.Request
|
|||||||
cred, owner, s3Err = getReqAccessKeyV2(r)
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
||||||
region := globalServerConfig.GetRegion()
|
region := globalServerConfig.GetRegion()
|
||||||
cred, owner, s3Err = getReqAccessKeyV4(r, region)
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
}
|
}
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return s3Err
|
return s3Err
|
||||||
|
@ -381,7 +381,8 @@ func TestIsReqAuthenticated(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
// Validates all testcases.
|
// Validates all testcases.
|
||||||
for i, testCase := range 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 {
|
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)
|
t.Fatalf("Test %d: Unexpected S3 error: want %d - got %d (got after reading request %s)", i, testCase.s3Error, s3Error, toAPIError(ctx, err).Code)
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ func getRedirectPostRawQuery(objInfo ObjectInfo) string {
|
|||||||
|
|
||||||
// Returns access credentials in the request Authorization header.
|
// Returns access credentials in the request Authorization header.
|
||||||
func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) {
|
func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) {
|
||||||
cred, _, _ = getReqAccessKeyV4(r, region)
|
cred, _, _ = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
if cred.AccessKey == "" {
|
if cred.AccessKey == "" {
|
||||||
cred, _, _ = getReqAccessKeyV2(r)
|
cred, _, _ = getReqAccessKeyV2(r)
|
||||||
}
|
}
|
||||||
|
23
cmd/iam.go
23
cmd/iam.go
@ -28,7 +28,7 @@ import (
|
|||||||
"github.com/minio/minio-go/pkg/set"
|
"github.com/minio/minio-go/pkg/set"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"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"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -326,6 +326,27 @@ func (sys *IAMSys) SetTempUser(accessKey string, cred auth.Credentials, policyNa
|
|||||||
return nil
|
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.
|
// ListUsers - list all users.
|
||||||
func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) {
|
func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) {
|
||||||
objectAPI := newObjectLayerFn()
|
objectAPI := newObjectLayerFn()
|
||||||
|
@ -1200,12 +1200,12 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
case authTypePresigned, authTypeSigned:
|
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))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !skipContentSha256Cksum(r) {
|
if !skipContentSha256Cksum(r) {
|
||||||
sha256hex = getContentSha256Cksum(r)
|
sha256hex = getContentSha256Cksum(r, serviceS3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1868,13 +1868,13 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case authTypePresigned, authTypeSigned:
|
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))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !skipContentSha256Cksum(r) {
|
if !skipContentSha256Cksum(r) {
|
||||||
sha256hex = getContentSha256Cksum(r)
|
sha256hex = getContentSha256Cksum(r, serviceS3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,7 +499,7 @@ func testPostPolicyBucketHandlerRedirect(obj ObjectLayer, instanceType string, t
|
|||||||
// postPresignSignatureV4 - presigned signature for PostPolicy requests.
|
// postPresignSignatureV4 - presigned signature for PostPolicy requests.
|
||||||
func postPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, location string) string {
|
func postPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, location string) string {
|
||||||
// Get signining key.
|
// Get signining key.
|
||||||
signingkey := getSigningKey(secretAccessKey, t, location)
|
signingkey := getSigningKey(secretAccessKey, t, location, "s3")
|
||||||
// Calculate signature.
|
// Calculate signature.
|
||||||
signature := getSignature(signingkey, policyBase64)
|
signature := getSignature(signingkey, policyBase64)
|
||||||
return signature
|
return signature
|
||||||
|
@ -47,8 +47,8 @@ func (c credentialHeader) getScope() string {
|
|||||||
}, "/")
|
}, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getReqAccessKeyV4(r *http.Request, region string) (auth.Credentials, bool, APIErrorCode) {
|
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)
|
ch, err := parseCredentialHeader("Credential="+r.URL.Query().Get("X-Amz-Credential"), region, stype)
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
// Strip off the Algorithm prefix.
|
// Strip off the Algorithm prefix.
|
||||||
v4Auth := strings.TrimPrefix(r.Header.Get("Authorization"), signV4Algorithm)
|
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 {
|
if len(authFields) != 3 {
|
||||||
return auth.Credentials{}, false, ErrMissingFields
|
return auth.Credentials{}, false, ErrMissingFields
|
||||||
}
|
}
|
||||||
ch, err = parseCredentialHeader(authFields[0], region)
|
ch, err = parseCredentialHeader(authFields[0], region, stype)
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return auth.Credentials{}, false, err
|
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.
|
// 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), "=")
|
creds := strings.Split(strings.TrimSpace(credElement), "=")
|
||||||
if len(creds) != 2 {
|
if len(creds) != 2 {
|
||||||
return ch, ErrMissingFields
|
return ch, ErrMissingFields
|
||||||
@ -107,7 +107,7 @@ func parseCredentialHeader(credElement string, region string) (ch credentialHead
|
|||||||
return ch, ErrAuthorizationHeaderMalformed
|
return ch, ErrAuthorizationHeaderMalformed
|
||||||
|
|
||||||
}
|
}
|
||||||
if credElements[2] != "s3" {
|
if credElements[2] != string(stype) {
|
||||||
return ch, ErrInvalidService
|
return ch, ErrInvalidService
|
||||||
}
|
}
|
||||||
cred.scope.service = credElements[2]
|
cred.scope.service = credElements[2]
|
||||||
@ -185,7 +185,7 @@ func doesV4PresignParamsExist(query url.Values) APIErrorCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parses all the presigned signature values into separate elements.
|
// 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.
|
// verify whether the required query params exist.
|
||||||
err := doesV4PresignParamsExist(query)
|
err := doesV4PresignParamsExist(query)
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
@ -201,7 +201,7 @@ func parsePreSignV4(query url.Values, region string) (psv preSignValues, aec API
|
|||||||
preSignV4Values := preSignValues{}
|
preSignV4Values := preSignValues{}
|
||||||
|
|
||||||
// Save credential.
|
// 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 {
|
if err != ErrNone {
|
||||||
return psv, err
|
return psv, err
|
||||||
}
|
}
|
||||||
@ -249,7 +249,7 @@ func parsePreSignV4(query url.Values, region string) (psv preSignValues, aec API
|
|||||||
// Authorization: algorithm Credential=accessKeyID/credScope, \
|
// Authorization: algorithm Credential=accessKeyID/credScope, \
|
||||||
// SignedHeaders=signedHeaders, Signature=signature
|
// 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
|
// Replace all spaced strings, some clients can send spaced
|
||||||
// parameters and some won't. So we pro-actively remove any spaces
|
// parameters and some won't. So we pro-actively remove any spaces
|
||||||
// to make parsing easier.
|
// to make parsing easier.
|
||||||
@ -275,7 +275,7 @@ func parseSignV4(v4Auth string, region string) (sv signValues, aec APIErrorCode)
|
|||||||
|
|
||||||
var err APIErrorCode
|
var err APIErrorCode
|
||||||
// Save credentail values.
|
// Save credentail values.
|
||||||
signV4Values.Credential, err = parseCredentialHeader(authFields[0], region)
|
signV4Values.Credential, err = parseCredentialHeader(authFields[0], region, stype)
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return sv, err
|
return sv, err
|
||||||
}
|
}
|
||||||
|
@ -219,7 +219,7 @@ func TestParseCredentialHeader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
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.
|
// validating the credential fields.
|
||||||
if testCase.expectedErrCode != actualErrCode {
|
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)
|
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 {
|
for i, testCase := range testCases {
|
||||||
parsedAuthField, actualErrCode := parseSignV4(testCase.inputV4AuthStr, "")
|
parsedAuthField, actualErrCode := parseSignV4(testCase.inputV4AuthStr, "", "s3")
|
||||||
|
|
||||||
if testCase.expectedErrCode != actualErrCode {
|
if testCase.expectedErrCode != actualErrCode {
|
||||||
t.Fatalf("Test %d: Expected the APIErrCode to be %d, got %d", i+1, 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])
|
inputQuery.Set(testCase.inputQueryKeyVals[j], testCase.inputQueryKeyVals[j+1])
|
||||||
}
|
}
|
||||||
// call the function under test.
|
// call the function under test.
|
||||||
parsedPreSign, actualErrCode := parsePreSignV4(inputQuery, "")
|
parsedPreSign, actualErrCode := parsePreSignV4(inputQuery, "", serviceS3)
|
||||||
if testCase.expectedErrCode != actualErrCode {
|
if testCase.expectedErrCode != actualErrCode {
|
||||||
t.Fatalf("Test %d: Expected the APIErrCode to be %d, got %d", i+1, testCase.expectedErrCode, actualErrCode)
|
t.Fatalf("Test %d: Expected the APIErrCode to be %d, got %d", i+1, testCase.expectedErrCode, actualErrCode)
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,16 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
|
"encoding/hex"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/sha256-simd"
|
"github.com/minio/sha256-simd"
|
||||||
)
|
)
|
||||||
@ -53,7 +58,18 @@ func skipContentSha256Cksum(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns SHA256 for calculating canonical-request.
|
// 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 (
|
var (
|
||||||
defaultSha256Cksum string
|
defaultSha256Cksum string
|
||||||
v []string
|
v []string
|
||||||
|
@ -248,7 +248,7 @@ func TestGetContentSha256Cksum(t *testing.T) {
|
|||||||
if testCase.h != "" {
|
if testCase.h != "" {
|
||||||
r.Header.Set("x-amz-content-sha256", testCase.h)
|
r.Header.Set("x-amz-content-sha256", testCase.h)
|
||||||
}
|
}
|
||||||
got := getContentSha256Cksum(r)
|
got := getContentSha256Cksum(r, serviceS3)
|
||||||
if got != testCase.expected {
|
if got != testCase.expected {
|
||||||
t.Errorf("Test %d: got:%s expected:%s", i+1, got, testCase.expected)
|
t.Errorf("Test %d: got:%s expected:%s", i+1, got, testCase.expected)
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,13 @@ const (
|
|||||||
yyyymmdd = "20060102"
|
yyyymmdd = "20060102"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type serviceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
serviceS3 serviceType = "s3"
|
||||||
|
serviceSTS serviceType = "sts"
|
||||||
|
)
|
||||||
|
|
||||||
// getCanonicalHeaders generate a list of request headers with their values
|
// getCanonicalHeaders generate a list of request headers with their values
|
||||||
func getCanonicalHeaders(signedHeaders http.Header) string {
|
func getCanonicalHeaders(signedHeaders http.Header) string {
|
||||||
var headers []string
|
var headers []string
|
||||||
@ -110,7 +117,7 @@ func getScope(t time.Time, region string) string {
|
|||||||
scope := strings.Join([]string{
|
scope := strings.Join([]string{
|
||||||
t.Format(yyyymmdd),
|
t.Format(yyyymmdd),
|
||||||
region,
|
region,
|
||||||
"s3",
|
string(serviceS3),
|
||||||
"aws4_request",
|
"aws4_request",
|
||||||
}, "/")
|
}, "/")
|
||||||
return scope
|
return scope
|
||||||
@ -126,10 +133,10 @@ func getStringToSign(canonicalRequest string, t time.Time, scope string) string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getSigningKey hmac seed to calculate final signature.
|
// 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)))
|
date := sumHMAC([]byte("AWS4"+secretKey), []byte(t.Format(yyyymmdd)))
|
||||||
regionBytes := sumHMAC(date, []byte(region))
|
regionBytes := sumHMAC(date, []byte(region))
|
||||||
service := sumHMAC(regionBytes, []byte("s3"))
|
service := sumHMAC(regionBytes, []byte(stype))
|
||||||
signingKey := sumHMAC(service, []byte("aws4_request"))
|
signingKey := sumHMAC(service, []byte("aws4_request"))
|
||||||
return signingKey
|
return signingKey
|
||||||
}
|
}
|
||||||
@ -165,7 +172,7 @@ func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode {
|
|||||||
region := globalServerConfig.GetRegion()
|
region := globalServerConfig.GetRegion()
|
||||||
|
|
||||||
// Parse credential tag.
|
// 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 {
|
if err != ErrNone {
|
||||||
return ErrMissingFields
|
return ErrMissingFields
|
||||||
}
|
}
|
||||||
@ -176,7 +183,7 @@ func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get signing key.
|
// 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.
|
// Get signature.
|
||||||
newSignature := getSignature(signingKey, formValues.Get("Policy"))
|
newSignature := getSignature(signingKey, formValues.Get("Policy"))
|
||||||
@ -193,12 +200,12 @@ func doesPolicySignatureV4Match(formValues http.Header) APIErrorCode {
|
|||||||
// doesPresignedSignatureMatch - Verify query headers with presigned signature
|
// doesPresignedSignatureMatch - Verify query headers with presigned signature
|
||||||
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
|
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
|
||||||
// returns ErrNone if the signature matches.
|
// 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
|
// Copy request
|
||||||
req := *r
|
req := *r
|
||||||
|
|
||||||
// Parse request query string.
|
// Parse request query string.
|
||||||
pSignValues, err := parsePreSignV4(req.URL.Query(), region)
|
pSignValues, err := parsePreSignV4(req.URL.Query(), region, stype)
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return err
|
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-Date", t.Format(iso8601Format))
|
||||||
query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds))
|
query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds))
|
||||||
query.Set("X-Amz-SignedHeaders", getSignedHeaders(extractedSignedHeaders))
|
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.
|
// Save other headers available in the request parameters.
|
||||||
for k, v := range req.URL.Query() {
|
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())
|
presignedStringToSign := getStringToSign(presignedCanonicalReq, t, pSignValues.Credential.getScope())
|
||||||
|
|
||||||
// Get hmac presigned signing key.
|
// 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.
|
// Get new signature.
|
||||||
newSignature := getSignature(presignedSigningKey, presignedStringToSign)
|
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
|
// doesSignatureMatch - Verify authorization header with calculated header in accordance with
|
||||||
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
|
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
|
||||||
// returns ErrNone if signature matches.
|
// 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.
|
// Copy request.
|
||||||
req := *r
|
req := *r
|
||||||
|
|
||||||
@ -314,7 +322,7 @@ func doesSignatureMatch(hashedPayload string, r *http.Request, region string) AP
|
|||||||
v4Auth := req.Header.Get("Authorization")
|
v4Auth := req.Header.Get("Authorization")
|
||||||
|
|
||||||
// Parse signature version '4' header.
|
// Parse signature version '4' header.
|
||||||
signV4Values, err := parseSignV4(v4Auth, region)
|
signV4Values, err := parseSignV4(v4Auth, region, stype)
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -354,7 +362,8 @@ func doesSignatureMatch(hashedPayload string, r *http.Request, region string) AP
|
|||||||
stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope())
|
stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope())
|
||||||
|
|
||||||
// Get hmac signing key.
|
// 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.
|
// Calculate signature.
|
||||||
newSignature := getSignature(signingKey, stringToSign)
|
newSignature := getSignature(signingKey, stringToSign)
|
||||||
|
@ -74,7 +74,7 @@ func TestDoesPolicySignatureMatch(t *testing.T) {
|
|||||||
"X-Amz-Date": []string{now.Format(iso8601Format)},
|
"X-Amz-Date": []string{now.Format(iso8601Format)},
|
||||||
"X-Amz-Signature": []string{
|
"X-Amz-Signature": []string{
|
||||||
getSignature(getSigningKey(globalServerConfig.GetCredential().SecretKey, now,
|
getSignature(getSigningKey(globalServerConfig.GetCredential().SecretKey, now,
|
||||||
globalMinioDefaultRegion), "policy"),
|
globalMinioDefaultRegion, serviceS3), "policy"),
|
||||||
},
|
},
|
||||||
"Policy": []string{"policy"},
|
"Policy": []string{"policy"},
|
||||||
},
|
},
|
||||||
@ -293,7 +293,7 @@ func TestDoesPresignedSignatureMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if it matches!
|
// Check if it matches!
|
||||||
err := doesPresignedSignatureMatch(payloadSHA256, req, testCase.region)
|
err := doesPresignedSignatureMatch(payloadSHA256, req, testCase.region, serviceS3)
|
||||||
if err != testCase.expected {
|
if err != testCase.expected {
|
||||||
t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(err))
|
t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(err))
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ func getChunkSignature(cred auth.Credentials, seedSignature string, region strin
|
|||||||
hashedChunk
|
hashedChunk
|
||||||
|
|
||||||
// Get hmac signing key.
|
// Get hmac signing key.
|
||||||
signingKey := getSigningKey(cred.SecretKey, date, region)
|
signingKey := getSigningKey(cred.SecretKey, date, region, serviceS3)
|
||||||
|
|
||||||
// Calculate signature.
|
// Calculate signature.
|
||||||
newSignature := getSignature(signingKey, stringToSign)
|
newSignature := getSignature(signingKey, stringToSign)
|
||||||
@ -72,7 +72,7 @@ func calculateSeedSignature(r *http.Request) (cred auth.Credentials, signature s
|
|||||||
v4Auth := req.Header.Get("Authorization")
|
v4Auth := req.Header.Get("Authorization")
|
||||||
|
|
||||||
// Parse signature version '4' header.
|
// Parse signature version '4' header.
|
||||||
signV4Values, errCode := parseSignV4(v4Auth, globalServerConfig.GetRegion())
|
signV4Values, errCode := parseSignV4(v4Auth, globalServerConfig.GetRegion(), serviceS3)
|
||||||
if errCode != ErrNone {
|
if errCode != ErrNone {
|
||||||
return cred, "", "", time.Time{}, errCode
|
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())
|
stringToSign := getStringToSign(canonicalRequest, date, signV4Values.Credential.getScope())
|
||||||
|
|
||||||
// Get hmac signing key.
|
// 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.
|
// Calculate signature.
|
||||||
newSignature := getSignature(signingKey, stringToSign)
|
newSignature := getSignature(signingKey, stringToSign)
|
||||||
|
@ -42,6 +42,39 @@ type AssumedRoleUser struct {
|
|||||||
// contains filtered or unexported fields
|
// 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.
|
// AssumeRoleWithWebIdentityResponse contains the result of successful AssumeRoleWithWebIdentity request.
|
||||||
type AssumeRoleWithWebIdentityResponse struct {
|
type AssumeRoleWithWebIdentityResponse struct {
|
||||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithWebIdentityResponse" json:"-"`
|
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithWebIdentityResponse" json:"-"`
|
||||||
|
@ -53,6 +53,7 @@ type STSErrorCode int
|
|||||||
// Error codes, non exhaustive list - http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithSAML.html
|
// Error codes, non exhaustive list - http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithSAML.html
|
||||||
const (
|
const (
|
||||||
ErrSTSNone STSErrorCode = iota
|
ErrSTSNone STSErrorCode = iota
|
||||||
|
ErrSTSAccessDenied
|
||||||
ErrSTSMissingParameter
|
ErrSTSMissingParameter
|
||||||
ErrSTSInvalidParameterValue
|
ErrSTSInvalidParameterValue
|
||||||
ErrSTSWebIdentityExpiredToken
|
ErrSTSWebIdentityExpiredToken
|
||||||
@ -76,6 +77,11 @@ func (e stsErrorCodeMap) ToSTSErr(errCode STSErrorCode) STSError {
|
|||||||
// error code to STSError structure, these fields carry respective
|
// error code to STSError structure, these fields carry respective
|
||||||
// descriptions for all the error responses.
|
// descriptions for all the error responses.
|
||||||
var stsErrCodes = stsErrorCodeMap{
|
var stsErrCodes = stsErrorCodeMap{
|
||||||
|
ErrSTSAccessDenied: {
|
||||||
|
Code: "AccessDenied",
|
||||||
|
Description: "Generating temporary credentials not allowed for this request.",
|
||||||
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
ErrSTSMissingParameter: {
|
ErrSTSMissingParameter: {
|
||||||
Code: "MissingParameter",
|
Code: "MissingParameter",
|
||||||
Description: "A required parameter for the specified action is not supplied.",
|
Description: "A required parameter for the specified action is not supplied.",
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -17,6 +17,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ const (
|
|||||||
// STS API action constants
|
// STS API action constants
|
||||||
clientGrants = "AssumeRoleWithClientGrants"
|
clientGrants = "AssumeRoleWithClientGrants"
|
||||||
webIdentity = "AssumeRoleWithWebIdentity"
|
webIdentity = "AssumeRoleWithWebIdentity"
|
||||||
|
assumeRole = "AssumeRole"
|
||||||
)
|
)
|
||||||
|
|
||||||
// stsAPIHandlers implements and provides http handlers for AWS STS API.
|
// stsAPIHandlers implements and provides http handlers for AWS STS API.
|
||||||
@ -46,6 +48,11 @@ func registerSTSRouter(router *mux.Router) {
|
|||||||
// STS Router
|
// STS Router
|
||||||
stsRouter := router.NewRoute().PathPrefix("/").Subrouter()
|
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.
|
// Assume roles with JWT handler, handles both ClientGrants and WebIdentity.
|
||||||
stsRouter.Methods("POST").HeadersRegexp("Content-Type", "application/x-www-form-urlencoded*").
|
stsRouter.Methods("POST").HeadersRegexp("Content-Type", "application/x-www-form-urlencoded*").
|
||||||
HandlerFunc(httpTraceAll(sts.AssumeRoleWithJWT))
|
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) {
|
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.
|
// Parse the incoming form data.
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
@ -74,6 +209,11 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.Form.Get("Policy") != "" {
|
||||||
|
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if r.Form.Get("Version") != stsAPIVersion {
|
if r.Form.Get("Version") != stsAPIVersion {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Invalid STS API version %s, expecting %s", 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))
|
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSMissingParameter))
|
||||||
@ -169,19 +309,23 @@ func (sts *stsAPIHandlers) AssumeRoleWithJWT(w http.ResponseWriter, r *http.Requ
|
|||||||
var encodedSuccessResponse []byte
|
var encodedSuccessResponse []byte
|
||||||
switch action {
|
switch action {
|
||||||
case clientGrants:
|
case clientGrants:
|
||||||
encodedSuccessResponse = encodeResponse(&AssumeRoleWithClientGrantsResponse{
|
clientGrantsResponse := &AssumeRoleWithClientGrantsResponse{
|
||||||
Result: ClientGrantsResult{
|
Result: ClientGrantsResult{
|
||||||
Credentials: cred,
|
Credentials: cred,
|
||||||
SubjectFromToken: subFromToken,
|
SubjectFromToken: subFromToken,
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
clientGrantsResponse.ResponseMetadata.RequestID = w.Header().Get(responseRequestIDKey)
|
||||||
|
encodedSuccessResponse = encodeResponse(clientGrantsResponse)
|
||||||
case webIdentity:
|
case webIdentity:
|
||||||
encodedSuccessResponse = encodeResponse(&AssumeRoleWithWebIdentityResponse{
|
webIdentityResponse := &AssumeRoleWithWebIdentityResponse{
|
||||||
Result: WebIdentityResult{
|
Result: WebIdentityResult{
|
||||||
Credentials: cred,
|
Credentials: cred,
|
||||||
SubjectFromWebIdentityToken: subFromToken,
|
SubjectFromWebIdentityToken: subFromToken,
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
webIdentityResponse.ResponseMetadata.RequestID = w.Header().Get(responseRequestIDKey)
|
||||||
|
encodedSuccessResponse = encodeResponse(webIdentityResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||||
|
@ -680,7 +680,7 @@ func signStreamingRequest(req *http.Request, accessKey, secretKey string, currTi
|
|||||||
scope := strings.Join([]string{
|
scope := strings.Join([]string{
|
||||||
currTime.Format(yyyymmdd),
|
currTime.Format(yyyymmdd),
|
||||||
globalMinioDefaultRegion,
|
globalMinioDefaultRegion,
|
||||||
"s3",
|
string(serviceS3),
|
||||||
"aws4_request",
|
"aws4_request",
|
||||||
}, "/")
|
}, "/")
|
||||||
|
|
||||||
@ -690,7 +690,7 @@ func signStreamingRequest(req *http.Request, accessKey, secretKey string, currTi
|
|||||||
|
|
||||||
date := sumHMAC([]byte("AWS4"+secretKey), []byte(currTime.Format(yyyymmdd)))
|
date := sumHMAC([]byte("AWS4"+secretKey), []byte(currTime.Format(yyyymmdd)))
|
||||||
region := sumHMAC(date, []byte(globalMinioDefaultRegion))
|
region := sumHMAC(date, []byte(globalMinioDefaultRegion))
|
||||||
service := sumHMAC(region, []byte("s3"))
|
service := sumHMAC(region, []byte(string(serviceS3)))
|
||||||
signingKey := sumHMAC(service, []byte("aws4_request"))
|
signingKey := sumHMAC(service, []byte("aws4_request"))
|
||||||
|
|
||||||
signature := hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
|
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{
|
scope := strings.Join([]string{
|
||||||
currTime.Format(yyyymmdd),
|
currTime.Format(yyyymmdd),
|
||||||
regionStr,
|
regionStr,
|
||||||
"s3",
|
string(serviceS3),
|
||||||
"aws4_request",
|
"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)))
|
date := sumHMAC([]byte("AWS4"+secretKey), []byte(currTime.Format(yyyymmdd)))
|
||||||
region := sumHMAC(date, []byte(regionStr))
|
region := sumHMAC(date, []byte(regionStr))
|
||||||
service := sumHMAC(region, []byte("s3"))
|
service := sumHMAC(region, []byte(serviceS3))
|
||||||
signingKey := sumHMAC(service, []byte("aws4_request"))
|
signingKey := sumHMAC(service, []byte("aws4_request"))
|
||||||
|
|
||||||
signature = hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
|
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)
|
queryStr := strings.Replace(query.Encode(), "+", "%20", -1)
|
||||||
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, unsignedPayload, queryStr, req.URL.Path, req.Method)
|
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, unsignedPayload, queryStr, req.URL.Path, req.Method)
|
||||||
stringToSign := getStringToSign(canonicalRequest, date, scope)
|
stringToSign := getStringToSign(canonicalRequest, date, scope)
|
||||||
signingKey := getSigningKey(secretAccessKey, date, region)
|
signingKey := getSigningKey(secretAccessKey, date, region, serviceS3)
|
||||||
signature := getSignature(signingKey, stringToSign)
|
signature := getSignature(signingKey, stringToSign)
|
||||||
|
|
||||||
req.URL.RawQuery = query.Encode()
|
req.URL.RawQuery = query.Encode()
|
||||||
@ -1035,7 +1035,7 @@ func signRequestV4(req *http.Request, accessKey, secretKey string) error {
|
|||||||
scope := strings.Join([]string{
|
scope := strings.Join([]string{
|
||||||
currTime.Format(yyyymmdd),
|
currTime.Format(yyyymmdd),
|
||||||
region,
|
region,
|
||||||
"s3",
|
string(serviceS3),
|
||||||
"aws4_request",
|
"aws4_request",
|
||||||
}, "/")
|
}, "/")
|
||||||
|
|
||||||
@ -1045,7 +1045,7 @@ func signRequestV4(req *http.Request, accessKey, secretKey string) error {
|
|||||||
|
|
||||||
date := sumHMAC([]byte("AWS4"+secretKey), []byte(currTime.Format(yyyymmdd)))
|
date := sumHMAC([]byte("AWS4"+secretKey), []byte(currTime.Format(yyyymmdd)))
|
||||||
regionHMAC := sumHMAC(date, []byte(region))
|
regionHMAC := sumHMAC(date, []byte(region))
|
||||||
service := sumHMAC(regionHMAC, []byte("s3"))
|
service := sumHMAC(regionHMAC, []byte(serviceS3))
|
||||||
signingKey := sumHMAC(service, []byte("aws4_request"))
|
signingKey := sumHMAC(service, []byte("aws4_request"))
|
||||||
|
|
||||||
signature := hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
|
signature := hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
|
||||||
|
@ -47,7 +47,7 @@ import (
|
|||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/handlers"
|
"github.com/minio/minio/pkg/handlers"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"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/ioutil"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"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)
|
extractedSignedHeaders.Set("host", host)
|
||||||
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, unsignedPayload, queryStr, path, "GET")
|
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, unsignedPayload, queryStr, path, "GET")
|
||||||
stringToSign := getStringToSign(canonicalRequest, date, getScope(date, region))
|
stringToSign := getStringToSign(canonicalRequest, date, getScope(date, region))
|
||||||
signingKey := getSigningKey(secretKey, date, region)
|
signingKey := getSigningKey(secretKey, date, region, serviceS3)
|
||||||
signature := getSignature(signingKey, stringToSign)
|
signature := getSignature(signingKey, stringToSign)
|
||||||
|
|
||||||
// Construct the final presigned URL.
|
// Construct the final presigned URL.
|
||||||
|
@ -13,27 +13,29 @@ Following are advantages for using temporary credentials:
|
|||||||
## Identity Federation
|
## 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.
|
- [**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.
|
- [**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
|
## Get started
|
||||||
In this document we will explain in detail on how to configure all the prerequisites, primarily WSO2, OPA (open policy agent).
|
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
|
### 1. Prerequisites
|
||||||
- [Configuring wso2](https://github.com/minio/minio/blob/master/docs/sts/wso2.md)
|
- [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)
|
- [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.
|
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_ACCESS_KEY=minio
|
||||||
export MINIO_SECRET_KEY=minio123
|
export MINIO_SECRET_KEY=minio123
|
||||||
export MINIO_IAM_JWKS_URL=https://localhost:9443/oauth2/jwks
|
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
|
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.
|
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.
|
> 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_ACCESS_KEY=aws_access_key
|
||||||
export MINIO_SECRET_KEY=aws_secret_key
|
export MINIO_SECRET_KEY=aws_secret_key
|
||||||
export MINIO_IAM_JWKS_URL=https://localhost:9443/oauth2/jwks
|
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
|
export MINIO_ETCD_ENDPOINTS=http://localhost:2379
|
||||||
minio gateway s3
|
minio gateway s3
|
||||||
```
|
```
|
||||||
|
97
docs/sts/assume-role.md
Normal file
97
docs/sts/assume-role.md
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -1,13 +1,13 @@
|
|||||||
## AssumeRoleWithClientGrants [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
|
## 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.
|
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.
|
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
|
### Request Parameters
|
||||||
#### DurationSeconds
|
#### 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 |
|
| 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)
|
> 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
|
##### Credentials
|
||||||
{
|
{
|
||||||
|
@ -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 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.
|
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.
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
## AssumeRoleWithWebIdentity [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io)
|
## 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.
|
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
|
### Request Parameters
|
||||||
#### DurationSeconds
|
#### 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 |
|
| 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.
|
> 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/
|
2018/12/26 17:49:36 listening on http://localhost:8080/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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. |
|
|iat| _integer_ | The token issue time. |
|
||||||
|exp| _integer_ | The token expiration time. |
|
|exp| _integer_ | The token expiration time. |
|
||||||
|jti| _string_ | Unique identifier for the JWT token. |
|
|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.
|
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
|
### 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.
|
Minio server expects environment variable for JWKS url as `MINIO_IAM_JWKS_URL`, this environment variable takes a single entry.
|
||||||
|
@ -20,6 +20,8 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -104,6 +106,27 @@ func (cred Credentials) Equal(ccred Credentials) bool {
|
|||||||
|
|
||||||
var timeSentinel = time.Unix(0, 0).UTC()
|
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.
|
// GetNewCredentialsWithMetadata generates and returns new credential with expiry.
|
||||||
func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string) (cred Credentials, err error) {
|
func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string) (cred Credentials, err error) {
|
||||||
readBytes := func(size int) (data []byte, 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.SecretKey = strings.Replace(string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLen]), "/", "+", -1)
|
||||||
cred.Status = "enabled"
|
cred.Status = "enabled"
|
||||||
|
|
||||||
expiry, ok := m["exp"].(float64)
|
expiry, err := expToInt64(m["exp"])
|
||||||
if !ok {
|
if err != nil {
|
||||||
|
return cred, err
|
||||||
|
}
|
||||||
|
if expiry == 0 {
|
||||||
cred.Expiration = timeSentinel
|
cred.Expiration = timeSentinel
|
||||||
return cred, nil
|
return cred, nil
|
||||||
}
|
}
|
||||||
@ -144,7 +170,7 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string)
|
|||||||
m["accessKey"] = cred.AccessKey
|
m["accessKey"] = cred.AccessKey
|
||||||
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims(m))
|
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))
|
cred.SessionToken, err = jwt.SignedString([]byte(tokenSecret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cred, err
|
return cred, err
|
||||||
|
@ -130,23 +130,24 @@ func expToInt64(expI interface{}) (expAt int64, err error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return 0, errors.New("invalid expiry value")
|
return 0, ErrInvalidDuration
|
||||||
}
|
}
|
||||||
return expAt, nil
|
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.
|
defaultExpiryDuration := time.Duration(60) * time.Minute // Defaults to 1hr.
|
||||||
if dsecs != "" {
|
if dsecs != "" {
|
||||||
expirySecs, err := strconv.ParseInt(dsecs, 10, 64)
|
expirySecs, err := strconv.ParseInt(dsecs, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, ErrInvalidDuration
|
||||||
}
|
}
|
||||||
// The duration, in seconds, of the role session.
|
// The duration, in seconds, of the role session.
|
||||||
// The value can range from 900 seconds (15 minutes)
|
// The value can range from 900 seconds (15 minutes)
|
||||||
// to 12 hours.
|
// to 12 hours.
|
||||||
if expirySecs < 900 || expirySecs > 43200 {
|
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
|
defaultExpiryDuration = time.Duration(expirySecs) * time.Second
|
||||||
@ -201,7 +202,7 @@ func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !jwtToken.Valid {
|
if !jwtToken.Valid {
|
||||||
return nil, fmt.Errorf("Invalid token: %v", token)
|
return nil, ErrTokenExpired
|
||||||
}
|
}
|
||||||
|
|
||||||
expAt, err := expToInt64(claims["exp"])
|
expAt, err := expToInt64(claims["exp"])
|
||||||
@ -209,7 +210,7 @@ func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultExpiryDuration, err := getDefaultExpiration(dsecs)
|
defaultExpiryDuration, err := GetDefaultExpiration(dsecs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ func TestDefaultExpiryDuration(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
d, err := getDefaultExpiration(u.Query().Get("DurationSeconds"))
|
d, err := GetDefaultExpiration(u.Query().Get("DurationSeconds"))
|
||||||
gotErr := (err != nil)
|
gotErr := (err != nil)
|
||||||
if testCase.expectErr != gotErr {
|
if testCase.expectErr != gotErr {
|
||||||
t.Errorf("Test %d: Expected %v, got %v with error %s", i+1, testCase.expectErr, gotErr, err)
|
t.Errorf("Test %d: Expected %v, got %v with error %s", i+1, testCase.expectErr, gotErr, err)
|
||||||
|
Loading…
Reference in New Issue
Block a user