History

Add role ARN support for OIDC identity provider (#13651)

- Allows setting a role policy parameter when configuring OIDC provider - When role policy is set, the server prints a role ARN usable in STS API requests - The given role policy is applied to STS API requests when the roleARN parameter is provided. - Service accounts for role policy are also possible and work as expected.

Aditya Manthramurthy committed 4 years ago

Commit 4c0f48c1 parent 4ce6d3512 files · +579 −77Browse files

Unified · Split

12 files
  1. buildscripts/minio-upgrade.sh +3 −0
  2. cmd/auth-handler.go +3 −2
  3. cmd/config-current.go +2 −2
  4. cmd/iam.go +113 −27
  5. cmd/sts-handlers.go +51 −34
  6. cmd/sts-handlers_test.go +167 −3
  7. docs/sts/web-identity.md +16 −0
  8. go.mod +2 −2
  9. go.sum +5 −4
  10. internal/arn/arn.go +147 −0
  11. internal/config/identity/openid/help.go +18 −0
  12. internal/config/identity/openid/jwt.go +52 −3
buildscripts/minio-upgrade.sh+3−0
@@ -43,6 +43,9 @@ add_alias() { · 42 unmodified lines
4343 echo "...waiting... for 5secs" && sleep 5
4444 done
4545 done
46+
47+ echo "Sleeping for nginx"
48+ sleep 20
4649 }
4750
4851 __init__() {
cmd/auth-handler.go+3−2
@@ -245,9 +245,10 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) { · 244 unmodified lines
245245
246246 // Session token must have a policy, reject requests without policy
247247 // claim.
248− _, pokOpenID := claims.MapClaims[iamPolicyClaimNameOpenID()]
248+ _, pokOpenIDClaimName := claims.MapClaims[iamPolicyClaimNameOpenID()]
249+ _, pokOpenIDRoleArn := claims.MapClaims[roleArnClaim]
249250 _, pokSA := claims.MapClaims[iamPolicyClaimNameSA()]
250− if !pokOpenID && !pokSA {
251+ if !pokOpenIDClaimName && !pokOpenIDRoleArn && !pokSA {
251252 return nil, errAuthentication
252253 }
253254
cmd/config-current.go+2−2
@@ -327,7 +327,7 @@ func validateConfig(s config.Config) error { · 326 unmodified lines
327327 }
328328 }
329329 if _, err := openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
330− NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
330+ NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region); err != nil {
331331 return err
332332 }
333333
@@ -513,7 +513,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) { · 179 unmodified lines
513513 }
514514
515515 globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
516− NewGatewayHTTPTransport(), xhttp.DrainBody)
516+ NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region)
517517 if err != nil {
518518 logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
519519 }
cmd/iam.go+113−27
@@ -33,7 +33,9 @@ import ( · 32 unmodified lines
3333 humanize "github.com/dustin/go-humanize"
3434 "github.com/minio/madmin-go"
3535 "github.com/minio/minio-go/v7/pkg/set"
36+ "github.com/minio/minio/internal/arn"
3637 "github.com/minio/minio/internal/auth"
38+ "github.com/minio/minio/internal/color"
3739 "github.com/minio/minio/internal/logger"
3840 iampolicy "github.com/minio/pkg/iam/policy"
3941 etcd "go.etcd.io/etcd/client/v3"
@@ -66,6 +68,8 @@ type IAMSys struct { · 26 unmodified lines
6668
6769 usersSysType UsersSysType
6870
71+ rolesMap map[arn.ARN]string
72+
6973 // Persistence layer for IAM subsystem
7074 store *IAMStoreSys
7175
@@ -215,6 +219,7 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc · 143 unmodified lines
215219
216220 r := rand.New(rand.NewSource(time.Now().UnixNano()))
217221
222+ // Migrate storage format if needed.
218223 for {
219224 // let one of the server acquire the lock, if not let them timeout.
220225 // which shall be retried again by this loop.
@@ -263,6 +268,7 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc · 42 unmodified lines
263268 break
264269 }
265270
271+ // Load IAM data from storage.
266272 for {
267273 if err := sys.Load(retryCtx); err != nil {
268274 if configRetriableErrors(err) {
@@ -308,7 +314,40 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc · 39 unmodified lines
308314 }()
309315 }
310316
317+ // Start watching changes to storage.
311318 go sys.watch(ctx)
319+
320+ // Load RoleARN
321+ if roleARN, rolePolicy, enabled := globalOpenIDConfig.GetRoleInfo(); enabled {
322+ numPolicies := len(strings.Split(rolePolicy, ","))
323+ validPolicies, _ := sys.store.FilterPolicies(rolePolicy, "")
324+ numValidPolicies := len(strings.Split(validPolicies, ","))
325+ if numPolicies != numValidPolicies {
326+ logger.LogIf(ctx, fmt.Errorf("Some specified role policies (%s) were not defined - role based policies will not be enabled.", rolePolicy))
327+ return
328+ }
329+ sys.rolesMap = map[arn.ARN]string{
330+ roleARN: rolePolicy,
331+ }
332+ }
333+
334+ sys.printIAMRoles()
335+}
336+
337+// Prints IAM role ARNs.
338+func (sys *IAMSys) printIAMRoles() {
339+ arns := sys.GetRoleARNs()
340+
341+ if len(arns) == 0 {
342+ return
343+ }
344+
345+ msgs := make([]string, 0, len(arns))
346+ for _, arn := range arns {
347+ msgs = append(msgs, color.Bold(arn))
348+ }
349+
350+ logStartupMessage(fmt.Sprintf("%s %s", color.Blue("IAM Roles:"), strings.Join(msgs, " ")))
312351 }
313352
314353 // HasWatcher - returns if the IAM system has a watcher to be notified of
@@ -389,6 +428,28 @@ func (sys *IAMSys) loadWatchedEvent(ctx context.Context, event iamWatchEvent) (e · 74 unmodified lines
389428 return err
390429 }
391430
431+// GetRoleARNs - returns a list of enabled role ARNs.
432+func (sys *IAMSys) GetRoleARNs() []string {
433+ var res []string
434+ for arn := range sys.rolesMap {
435+ res = append(res, arn.String())
436+ }
437+ return res
438+}
439+
440+// GetRolePolicy - returns policies associated with a role ARN.
441+func (sys *IAMSys) GetRolePolicy(arnStr string) (string, error) {
442+ arn, err := arn.Parse(arnStr)
443+ if err != nil {
444+ return "", fmt.Errorf("RoleARN parse err: %v", err)
445+ }
446+ rolePolicy, ok := sys.rolesMap[arn]
447+ if !ok {
448+ return "", fmt.Errorf("RoleARN %s is not defined.", arnStr)
449+ }
450+ return rolePolicy, nil
451+}
452+
392453 // DeletePolicy - deletes a canned policy from backend or etcd.
393454 func (sys *IAMSys) DeletePolicy(ctx context.Context, policyName string) error {
394455 if !sys.Initialized() {
@@ -1029,7 +1090,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin · 634 unmodified lines
10291090 return false
10301091 }
10311092
1032− // Check policy for this service account.
1093+ // Check policy for parent user of service account.
10331094 svcPolicies, err := sys.PolicyDBGet(parentUser, false, args.Groups...)
10341095 if err != nil {
10351096 logger.LogIf(GlobalContext, err)
@@ -1037,9 +1098,20 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin · 1 unmodified line
10371098 }
10381099
10391100 if len(svcPolicies) == 0 {
1040− // If parent user has no policies, look in OpenID claims in case it exists.
1041− policySet, ok := iampolicy.GetPoliciesFromClaims(args.Claims, iamPolicyClaimNameOpenID())
1042− if ok {
1101+ // If parent user has no policies, check for OpenID
1102+ // claims/RoleARN in case it exists.
1103+ roleArn := args.GetRoleArn()
1104+ if roleArn != "" {
1105+ arn, err := arn.Parse(roleArn)
1106+ if err != nil {
1107+ logger.LogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
1108+ return false
1109+ }
1110+ svcPolicies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
1111+ } else {
1112+ // If there is no roleArn claim, check the OpenID
1113+ // provider's policy claim.
1114+ policySet, _ := iampolicy.GetPoliciesFromClaims(args.Claims, iamPolicyClaimNameOpenID())
10431115 svcPolicies = policySet.ToSlice()
10441116 }
10451117 if len(svcPolicies) == 0 {
@@ -1156,35 +1228,49 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool { · 110 unmodified lines
11561228 return sys.IsAllowedLDAPSTS(args, parentUser)
11571229 }
11581230
1159− policies, ok := args.GetPolicies(iamPolicyClaimNameOpenID())
1160− if !ok {
1161− // When claims are set, it should have a policy claim field.
1162− return false
1163− }
1231+ var policies []string
1232+ roleArn := args.GetRoleArn()
1233+ if roleArn != "" {
1234+ arn, err := arn.Parse(roleArn)
1235+ if err != nil {
1236+ logger.LogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
1237+ return false
1238+ }
1239+ policies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
1240+ } else {
1241+ // If roleArn is not used, we fall back to using policy claim
1242+ // from JWT.
1243+ policySet, ok := args.GetPolicies(iamPolicyClaimNameOpenID())
1244+ if !ok {
1245+ // When claims are set, it should have a policy claim field.
1246+ return false
1247+ }
1248+ // When claims are set, it should have policies as claim.
1249+ if policySet.IsEmpty() {
1250+ // No policy, no access!
1251+ return false
1252+ }
11641253
1165− // When claims are set, it should have policies as claim.
1166− if policies.IsEmpty() {
1167− // No policy, no access!
1168− return false
1169− }
1254+ // If policy is available for given user, check the policy.
1255+ mp, ok := sys.store.GetMappedPolicy(args.AccountName, false)
1256+ if !ok {
1257+ // No policy set for the user that we can find, no access!
1258+ return false
1259+ }
11701260
1171− // If policy is available for given user, check the policy.
1172− mp, ok := sys.store.GetMappedPolicy(args.AccountName, false)
1173− if !ok {
1174− // No policy set for the user that we can find, no access!
1175− return false
1176− }
1261+ if !policySet.Equals(mp.policySet()) {
1262+ // When claims has a policy, it should match the
1263+ // policy of args.AccountName which server remembers.
1264+ // if not reject such requests.
1265+ return false
1266+ }
11771267
1178− if !policies.Equals(mp.policySet()) {
1179− // When claims has a policy, it should match the
1180− // policy of args.AccountName which server remembers.
1181− // if not reject such requests.
1182− return false
1268+ policies = policySet.ToSlice()
11831269 }
11841270
1185− combinedPolicy, err := sys.store.GetPolicy(strings.Join(policies.ToSlice(), ","))
1271+ combinedPolicy, err := sys.store.GetPolicy(strings.Join(policies, ","))
11861272 if err == errNoSuchPolicy {
1187− for pname := range policies {
1273+ for _, pname := range policies {
11881274 _, err := sys.store.GetPolicy(pname)
11891275 if err == errNoSuchPolicy {
11901276 // all policies presented in the claim should exist
cmd/sts-handlers.go+51−34
@@ -45,6 +45,7 @@ const ( · 44 unmodified lines
4545 stsAction = "Action"
4646 stsPolicy = "Policy"
4747 stsToken = "Token"
48+ stsRoleArn = "RoleArn"
4849 stsWebIdentityToken = "WebIdentityToken"
4950 stsWebIdentityAccessToken = "WebIdentityAccessToken" // only valid if UserInfo is enabled.
5051 stsDurationSeconds = "DurationSeconds"
@@ -73,6 +74,9 @@ const ( · 22 unmodified lines
7374 // LDAP claim keys
7475 ldapUser = "ldapUser"
7576 ldapUserN = "ldapUsername"
77+
78+ // Role Claim key
79+ roleArnClaim = "roleArn"
7680 )
7781
7882 func parseOpenIDParentUser(parentUser string) (userID string, err error) {
@@ -399,45 +403,42 @@ func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Requ · 320 unmodified lines
399403 }
400404 }
401405
402− var subFromToken string
403− if v, ok := m[subClaim]; ok {
404− subFromToken, _ = v.(string)
405− }
406−
407− if subFromToken == "" {
408− writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
409− errors.New("STS JWT Token has `sub` claim missing, `sub` claim is mandatory"))
410− return
411− }
412−
413− var issFromToken string
414− if v, ok := m[issClaim]; ok {
415− issFromToken, _ = v.(string)
416− }
417−
418− // JWT has requested a custom claim with policy value set.
419− // This is a MinIO STS API specific value, this value should
420− // be set and configured on your identity provider as part of
421− // JWT custom claims.
422406 var policyName string
423− policySet, ok := iampolicy.GetPoliciesFromClaims(m, iamPolicyClaimNameOpenID())
424− policies := strings.Join(policySet.ToSlice(), ",")
425− if ok {
426− policyName = globalIAMSys.CurrentPolicies(policies)
427− }
428−
429− if globalPolicyOPA == nil {
430− if !ok {
431− writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
432− fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID()))
433− return
434− } else if policyName == "" {
407+ roleArn := r.Form.Get(stsRoleArn)
408+ if roleArn != "" {
409+ _, err := globalIAMSys.GetRolePolicy(roleArn)
410+ if err != nil {
435411 writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
436− fmt.Errorf("None of the given policies (`%s`) are defined, credentials will not be generated", policies))
412+ fmt.Errorf("Error processing %s parameter: %v", stsRoleArn, err))
437413 return
438414 }
415+ // If roleArn is used, we set it as a claim, and use the
416+ // associated policy when credentials are used.
417+ m[roleArnClaim] = roleArn
418+ } else {
419+ // JWT has requested a custom claim with policy value set.
420+ // This is a MinIO STS API specific value, this value should
421+ // be set and configured on your identity provider as part of
422+ // JWT custom claims.
423+ policySet, ok := iampolicy.GetPoliciesFromClaims(m, iamPolicyClaimNameOpenID())
424+ policies := strings.Join(policySet.ToSlice(), ",")
425+ if ok {
426+ policyName = globalIAMSys.CurrentPolicies(policies)
427+ }
428+
429+ if globalPolicyOPA == nil {
430+ if !ok {
431+ writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
432+ fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID()))
433+ return
434+ } else if policyName == "" {
435+ writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
436+ fmt.Errorf("None of the given policies (`%s`) are defined, credentials will not be generated", policies))
437+ return
438+ }
439+ }
440+ m[iamPolicyClaimNameOpenID()] = policyName
439441 }
440− m[iamPolicyClaimNameOpenID()] = policyName
441442
442443 sessionPolicyStr := r.Form.Get(stsPolicy)
443444 // https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
@@ -476,6 +477,22 @@ func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Requ · 32 unmodified lines
476477 // this is to ensure that ParentUser doesn't change and we get to use
477478 // parentUser as per the requirements for service accounts for OpenID
478479 // based logins.
480+ var subFromToken string
481+ if v, ok := m[subClaim]; ok {
482+ subFromToken, _ = v.(string)
483+ }
484+
485+ if subFromToken == "" {
486+ writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
487+ errors.New("STS JWT Token has `sub` claim missing, `sub` claim is mandatory"))
488+ return
489+ }
490+
491+ var issFromToken string
492+ if v, ok := m[issClaim]; ok {
493+ issFromToken, _ = v.(string)
494+ }
495+
479496 cred.ParentUser = "openid:" + subFromToken + ":" + issFromToken
480497
481498 // Set the newly generated credentials.
cmd/sts-handlers_test.go+167−3
@@ -741,7 +741,7 @@ const ( · 740 unmodified lines
741741
742742 // SetUpOpenID - expects to setup an OpenID test server using the test OpenID
743743 // container and canned data from https://github.com/minio/minio-ldap-testing
744−func (s *TestSuiteIAM) SetUpOpenID(c *check, serverAddr string) {
744+func (s *TestSuiteIAM) SetUpOpenID(c *check, serverAddr string, rolePolicy string) {
745745 ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
746746 defer cancel()
747747
@@ -750,10 +750,14 @@ func (s *TestSuiteIAM) SetUpOpenID(c *check, serverAddr string) { · 2 unmodified lines
750750 fmt.Sprintf("config_url=%s/.well-known/openid-configuration", serverAddr),
751751 "client_id=minio-client-app",
752752 "client_secret=minio-client-app-secret",
753− "claim_name=groups",
754753 "scopes=openid,groups",
755754 "redirect_uri=http://127.0.0.1:10000/oauth_callback",
756755 }
756+ if rolePolicy != "" {
757+ configCmds = append(configCmds, fmt.Sprintf("role_policy=%s", rolePolicy))
758+ } else {
759+ configCmds = append(configCmds, "claim_name=groups")
760+ }
757761 _, err := s.adm.SetConfigKV(ctx, strings.Join(configCmds, " "))
758762 if err != nil {
759763 c.Fatalf("unable to setup OpenID for tests: %v", err)
@@ -797,7 +801,7 @@ func TestIAMWithOpenIDServerSuite(t *testing.T) { · 37 unmodified lines
797801 }
798802
799803 suite.SetUpSuite(c)
800− suite.SetUpOpenID(c, openIDServer)
804+ suite.SetUpOpenID(c, openIDServer, "")
801805 suite.TestOpenIDSTS(c)
802806 suite.TestOpenIDServiceAcc(c)
803807 suite.TearDownSuite(c)
@@ -805,3 +809,163 @@ func TestIAMWithOpenIDServerSuite(t *testing.T) { · 1 unmodified line
805809 )
806810 }
807811 }
812+
813+func TestIAMWithOpenIDWithRolePolicyServerSuite(t *testing.T) {
814+ baseTestCases := []TestSuiteCommon{
815+ // Init and run test on FS backend with signature v4.
816+ {serverType: "FS", signer: signerV4},
817+ // Init and run test on FS backend, with tls enabled.
818+ {serverType: "FS", signer: signerV4, secure: true},
819+ // Init and run test on Erasure backend.
820+ {serverType: "Erasure", signer: signerV4},
821+ // Init and run test on ErasureSet backend.
822+ {serverType: "ErasureSet", signer: signerV4},
823+ }
824+ testCases := []*TestSuiteIAM{}
825+ for _, bt := range baseTestCases {
826+ testCases = append(testCases,
827+ newTestSuiteIAM(bt, false),
828+ newTestSuiteIAM(bt, true),
829+ )
830+ }
831+ for i, testCase := range testCases {
832+ etcdStr := ""
833+ if testCase.withEtcdBackend {
834+ etcdStr = " (with etcd backend)"
835+ }
836+ t.Run(
837+ fmt.Sprintf("Test: %d, ServerType: %s%s", i+1, testCase.serverType, etcdStr),
838+ func(t *testing.T) {
839+ c := &check{t, testCase.serverType}
840+ suite := testCase
841+
842+ openIDServer := os.Getenv(EnvTestOpenIDServer)
843+ if openIDServer == "" {
844+ c.Skip("Skipping OpenID test as no OpenID server is provided.")
845+ }
846+
847+ suite.SetUpSuite(c)
848+ suite.SetUpOpenID(c, openIDServer, "readwrite")
849+ suite.TestOpenIDSTSWithRolePolicy(c)
850+ suite.TestOpenIDServiceAccWithRolePolicy(c)
851+ suite.TearDownSuite(c)
852+ },
853+ )
854+ }
855+}
856+
857+const (
858+ testRoleARN = "arn:minio:iam:::role/127.0.0.1_minio-cl"
859+)
860+
861+func (s *TestSuiteIAM) TestOpenIDSTSWithRolePolicy(c *check) {
862+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
863+ defer cancel()
864+
865+ bucket := getRandomBucketName()
866+ err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
867+ if err != nil {
868+ c.Fatalf("bucket create error: %v", err)
869+ }
870+
871+ // Generate web identity STS token by interacting with OpenID IDP.
872+ token, err := mockTestUserInteraction(ctx, testProvider, "dillon@example.io", "dillon")
873+ if err != nil {
874+ c.Fatalf("mock user err: %v", err)
875+ }
876+
877+ webID := cr.STSWebIdentity{
878+ Client: s.TestSuiteCommon.client,
879+ STSEndpoint: s.endPoint,
880+ GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) {
881+ return &cr.WebIdentityToken{
882+ Token: token,
883+ }, nil
884+ },
885+ RoleARN: testRoleARN,
886+ }
887+
888+ value, err := webID.Retrieve()
889+ if err != nil {
890+ c.Fatalf("Expected to generate STS creds, got err: %#v", err)
891+ }
892+ // fmt.Printf("value: %#v\n", value)
893+
894+ minioClient, err := minio.New(s.endpoint, &minio.Options{
895+ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken),
896+ Secure: s.secure,
897+ Transport: s.TestSuiteCommon.client.Transport,
898+ })
899+ if err != nil {
900+ c.Fatalf("Error initializing client: %v", err)
901+ }
902+
903+ // Validate that the client from sts creds can access the bucket.
904+ c.mustListObjects(ctx, minioClient, bucket)
905+}
906+
907+func (s *TestSuiteIAM) TestOpenIDServiceAccWithRolePolicy(c *check) {
908+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
909+ defer cancel()
910+
911+ bucket := getRandomBucketName()
912+ err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
913+ if err != nil {
914+ c.Fatalf("bucket create error: %v", err)
915+ }
916+
917+ // Generate web identity STS token by interacting with OpenID IDP.
918+ token, err := mockTestUserInteraction(ctx, testProvider, "dillon@example.io", "dillon")
919+ if err != nil {
920+ c.Fatalf("mock user err: %v", err)
921+ }
922+
923+ webID := cr.STSWebIdentity{
924+ Client: s.TestSuiteCommon.client,
925+ STSEndpoint: s.endPoint,
926+ GetWebIDTokenExpiry: func() (*cr.WebIdentityToken, error) {
927+ return &cr.WebIdentityToken{
928+ Token: token,
929+ }, nil
930+ },
931+ RoleARN: testRoleARN,
932+ }
933+
934+ value, err := webID.Retrieve()
935+ if err != nil {
936+ c.Fatalf("Expected to generate STS creds, got err: %#v", err)
937+ }
938+
939+ // Create an madmin client with user creds
940+ userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
941+ Creds: cr.NewStaticV4(value.AccessKeyID, value.SecretAccessKey, value.SessionToken),
942+ Secure: s.secure,
943+ })
944+ if err != nil {
945+ c.Fatalf("Err creating user admin client: %v", err)
946+ }
947+ userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport)
948+
949+ // Create svc acc
950+ cr := c.mustCreateSvcAccount(ctx, value.AccessKeyID, userAdmClient)
951+
952+ // 1. Check that svc account appears in listing
953+ c.assertSvcAccAppearsInListing(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey)
954+
955+ // 2. Check that svc account info can be queried
956+ c.assertSvcAccInfoQueryable(ctx, userAdmClient, value.AccessKeyID, cr.AccessKey, true)
957+
958+ // 3. Check S3 access
959+ c.assertSvcAccS3Access(ctx, s, cr, bucket)
960+
961+ // 4. Check that svc account can restrict the policy, and that the
962+ // session policy can be updated.
963+ c.assertSvcAccSessionPolicyUpdate(ctx, s, userAdmClient, value.AccessKeyID, bucket)
964+
965+ // 4. Check that service account's secret key and account status can be
966+ // updated.
967+ c.assertSvcAccSecretKeyAndStatusUpdate(ctx, s, userAdmClient, value.AccessKeyID, bucket)
968+
969+ // 5. Check that service account can be deleted.
970+ c.assertSvcAccDeletion(ctx, s, userAdmClient, value.AccessKeyID, bucket)
971+}
docs/sts/web-identity.md+16−0
@@ -6,6 +6,14 @@ Calling AssumeRoleWithWebIdentity does not require the use of MinIO default cred · 5 unmodified lines
66
77 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 of 365 days.
88
9+## Access Control Policies
10+
11+MinIO's AssumeRoleWithWebIdentity supports specifying access control policies in two ways:
12+
13+1. Role Policy (Recommended): When specified, all users authenticating via this API are authorized to (only) use the specified role policy. The policy to associate with such users is specified when configuring OpenID provider in the server, via the `role_policy` configuration parameter or the `MINIO_IDENTITY_OPENID_ROLE_POLICY` environment variable. The value is a comma-separated list of IAM access policy names already defined in the server. In this situation, the server prints a role ARN at startup that must be specified as a `RoleARN` API request parameter in the STS AssumeRoleWithWebIdentity API call.
14+
15+2. `id_token` claims: When the role policy is not configured, MinIO looks for a specific claim in the `id_token` (JWT) returned by the OpenID provider. The default claim is `policy` and can be overridden by the `claim_name` configuration parameter or the `MINIO_IDENTITY_OPENID_CLAIM_NAME` environment variable. The claim value can be a string (comma-separated list) or an array of IAM access policy names defined in the server. A `RoleARN` API request parameter *must not* be specified in the STS AssumeRoleWithWebIdentity API call.
16+
917 ## API Request Parameters
1018 ### WebIdentityToken
1119 The OAuth 2.0 id_token that is provided by the web identity provider. Application must get this token by authenticating the user who is using your application with a web identity provider before the application makes an AssumeRoleWithWebIdentity call.
@@ -24,6 +32,14 @@ There are situations when identity provider does not provide user claims in `id_ · 12 unmodified lines
2432 | *Type* | *String* |
2533 | *Required* | *No* |
2634
35+### RoleARN
36+The role ARN to use. This must be specified if and only if the web identity provider is configured with a role policy.
37+
38+| Params | Value |
39+| :-- | :-- |
40+| *Type* | *String* |
41+| *Required* | *No* |
42+
2743 ### Version
2844 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.
2945
go.mod+2−2
@@ -49,7 +49,8 @@ require ( · 48 unmodified lines
4949 github.com/minio/highwayhash v1.0.2
5050 github.com/minio/kes v0.14.0
5151 github.com/minio/madmin-go v1.1.16
52− github.com/minio/minio-go/v7 v7.0.16-0.20211108161804-a7a36ee131df
52+ github.com/minio/mc v0.0.0-20211118223026-df75eed32e9e // indirect
53+ github.com/minio/minio-go/v7 v7.0.16-0.20211117164632-e517704ccb36
5354 github.com/minio/parquet-go v1.1.0
5455 github.com/minio/pkg v1.1.9
5556 github.com/minio/selfupdate v0.3.1
@@ -166,7 +167,6 @@ require ( · 110 unmodified lines
166167 github.com/minio/colorjson v1.0.1 // indirect
167168 github.com/minio/direct-csi v1.3.5-0.20210601185811-f7776f7961bf // indirect
168169 github.com/minio/filepath v1.0.0 // indirect
169− github.com/minio/mc v0.0.0-20211115052100-7fd441ec6c5b // indirect
170170 github.com/minio/md5-simd v1.1.2 // indirect
171171 github.com/minio/operator v0.0.0-20211011212245-31460bbbc4b7 // indirect
172172 github.com/minio/operator/logsearchapi v0.0.0-20211011212245-31460bbbc4b7 // indirect
go.sum+5−4
@@ -1086,12 +1086,12 @@ github.com/minio/kes v0.14.0/go.mod h1:OUensXz2BpgMfiogslKxv7Anyx/wj+6bFC6qA7BQc · 1085 unmodified lines
10861086 github.com/minio/madmin-go v1.0.12/go.mod h1:BK+z4XRx7Y1v8SFWXsuLNqQqnq5BO/axJ8IDJfgyvfs=
10871087 github.com/minio/madmin-go v1.1.11-0.20211102182201-e51fd3d6b104/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=
10881088 github.com/minio/madmin-go v1.1.12/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=
1089−github.com/minio/madmin-go v1.1.13/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=
1089+github.com/minio/madmin-go v1.1.15/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=
10901090 github.com/minio/madmin-go v1.1.16 h1:c96vQBF3W9sPXiY04rjNa06FfOmWDjeFuChuqtOzLmE=
10911091 github.com/minio/madmin-go v1.1.16/go.mod h1:Iu0OnrMWNBYx1lqJTW+BFjBMx0Hi0wjw8VmqhiOs2Jo=
10921092 github.com/minio/mc v0.0.0-20211110003602-1461b652d920/go.mod h1:V8NmUfU0W3G/mrifeO6nm4CWFTiXY2nx7FJyMge/aHk=
1093−github.com/minio/mc v0.0.0-20211115052100-7fd441ec6c5b h1:crCI2lSbzWzMuk/U6fMqSl5eF2V2VKDFNX+ILSD1sxU=
1094−github.com/minio/mc v0.0.0-20211115052100-7fd441ec6c5b/go.mod h1:2fFAzMBmEYcN4mjcmQdlLuSabP+bvQC5UpqfLzRgrQQ=
1093+github.com/minio/mc v0.0.0-20211118223026-df75eed32e9e h1:6EoG2tWc6y89CTX6h2jvbAaSSjd78zBKaL4U1wEJ3yA=
1094+github.com/minio/mc v0.0.0-20211118223026-df75eed32e9e/go.mod h1:sXbvyABnNzmpnMEFT2aOexxnI8O0x802lZxbXo8aDgA=
10951095 github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
10961096 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
10971097 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
@@ -1099,8 +1099,9 @@ github.com/minio/minio-go/v7 v7.0.10/go.mod h1:td4gW1ldOsj1PbSNS+WYK43j+P1XVhX/8 · 1 unmodified line
10991099 github.com/minio/minio-go/v7 v7.0.11-0.20210302210017-6ae69c73ce78/go.mod h1:mTh2uJuAbEqdhMVl6CMIIZLUeiMiWtJR4JB8/5g2skw=
11001100 github.com/minio/minio-go/v7 v7.0.15-0.20211004160302-3b57c1e369ca/go.mod h1:pUV0Pc+hPd1nccgmzQF/EXh48l/Z/yps6QPF1aaie4g=
11011101 github.com/minio/minio-go/v7 v7.0.15/go.mod h1:pUV0Pc+hPd1nccgmzQF/EXh48l/Z/yps6QPF1aaie4g=
1102−github.com/minio/minio-go/v7 v7.0.16-0.20211108161804-a7a36ee131df h1:7BfpVODGh5reCjIx2lUqE7CxRMjo58XJw7ZjKKNW/vc=
11031102 github.com/minio/minio-go/v7 v7.0.16-0.20211108161804-a7a36ee131df/go.mod h1:pUV0Pc+hPd1nccgmzQF/EXh48l/Z/yps6QPF1aaie4g=
1103+github.com/minio/minio-go/v7 v7.0.16-0.20211117164632-e517704ccb36 h1:amnEPz1PuZxUUSKQvQn7E4Pd+B7tIqmqiFeuc9yy2r4=
1104+github.com/minio/minio-go/v7 v7.0.16-0.20211117164632-e517704ccb36/go.mod h1:pUV0Pc+hPd1nccgmzQF/EXh48l/Z/yps6QPF1aaie4g=
11041105 github.com/minio/operator v0.0.0-20211011212245-31460bbbc4b7 h1:dkfuMNslMjGoJ4ArAMSoQhidYNdm3SgzLBP+f96O3/E=
11051106 github.com/minio/operator v0.0.0-20211011212245-31460bbbc4b7/go.mod h1:lDpuz8nwsfhKlfiBaA3Z8AW019fWEAjO2gltfLbdorE=
11061107 github.com/minio/operator/logsearchapi v0.0.0-20211011212245-31460bbbc4b7 h1:vFtQqCt67ETp0JAkOKRWTKkgwFv14Vc1jJSxmQ8wJE0=
internal/arn/arn.go+147−0
@@ -0,0 +1,147 @@
1+// Copyright (c) 2015-2021 MinIO, Inc.
2+//
3+// This file is part of MinIO Object Storage stack
4+//
5+// This program is free software: you can redistribute it and/or modify
6+// it under the terms of the GNU Affero General Public License as published by
7+// the Free Software Foundation, either version 3 of the License, or
8+// (at your option) any later version.
9+//
10+// This program is distributed in the hope that it will be useful
11+// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+// GNU Affero General Public License for more details.
14+//
15+// You should have received a copy of the GNU Affero General Public License
16+// along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+package arn
19+
20+import (
21+ "fmt"
22+ "regexp"
23+ "strings"
24+)
25+
26+// ARN structure:
27+//
28+// arn:partition:service:region:account-id:resource-type/resource-id
29+//
30+// In this implementation, account-id is empty.
31+//
32+// Reference: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
33+
34+type arnPartition string
35+
36+const (
37+ arnPartitionMinio arnPartition = "minio"
38+)
39+
40+type arnService string
41+
42+const (
43+ arnServiceIAM arnService = "iam"
44+)
45+
46+type arnResourceType string
47+
48+const (
49+ arnResourceTypeRole arnResourceType = "role"
50+)
51+
52+// ARN - representation of resources based on AWS ARNs.
53+type ARN struct {
54+ Partition arnPartition
55+ Service arnService
56+ Region string
57+ ResourceType arnResourceType
58+ ResourceID string
59+}
60+
61+var (
62+ // Allows lower-case chars, numbers, '.', '-', '_' and '/'. Starts with
63+ // a letter or digit. At least 1 character long.
64+ validResourceIDRegex = regexp.MustCompile(`^[a-z0-9][a-z0-9_/\.-]*$`)
65+)
66+
67+// NewIAMRoleARN - returns an ARN for a role in MinIO.
68+func NewIAMRoleARN(resourceID, serverRegion string) (ARN, error) {
69+ if !validResourceIDRegex.MatchString(resourceID) {
70+ return ARN{}, fmt.Errorf("Invalid resource ID: %s", resourceID)
71+ }
72+ return ARN{
73+ Partition: arnPartitionMinio,
74+ Service: arnServiceIAM,
75+ Region: serverRegion,
76+ ResourceType: arnResourceTypeRole,
77+ ResourceID: resourceID,
78+ }, nil
79+}
80+
81+// String - returns string representation of the ARN.
82+func (arn ARN) String() string {
83+ return strings.Join(
84+ []string{
85+ "arn",
86+ string(arn.Partition),
87+ string(arn.Service),
88+ arn.Region,
89+ "", // account-id is always empty in this implementation
90+ string(arn.ResourceType) + "/" + arn.ResourceID,
91+ },
92+ ":",
93+ )
94+}
95+
96+// Parse - parses an ARN string into a type.
97+func Parse(arnStr string) (arn ARN, err error) {
98+ ps := strings.Split(arnStr, ":")
99+ if len(ps) != 6 ||
100+ ps[0] != "arn" {
101+ err = fmt.Errorf("Invalid ARN string format")
102+ return
103+ }
104+
105+ if ps[1] != string(arnPartitionMinio) {
106+ err = fmt.Errorf("Invalid ARN - bad partition field")
107+ return
108+ }
109+
110+ if ps[2] != string(arnServiceIAM) {
111+ err = fmt.Errorf("Invalid ARN - bad service field")
112+ return
113+ }
114+
115+ // ps[3] is region and is not validated here. If the region is invalid,
116+ // the ARN would not match any configured ARNs in the server.
117+
118+ if ps[4] != "" {
119+ err = fmt.Errorf("Invalid ARN - unsupported account-id field")
120+ return
121+ }
122+
123+ res := strings.SplitN(ps[5], "/", 2)
124+ if len(res) != 2 {
125+ err = fmt.Errorf("Invalid ARN - resource does not contain a \"/\"")
126+ return
127+ }
128+
129+ if res[0] != string(arnResourceTypeRole) {
130+ err = fmt.Errorf("Invalid ARN: resource type is invalid.")
131+ return
132+ }
133+
134+ if !validResourceIDRegex.MatchString(res[1]) {
135+ err = fmt.Errorf("Invalid resource ID: %s", res[1])
136+ return
137+ }
138+
139+ arn = ARN{
140+ Partition: arnPartitionMinio,
141+ Service: arnServiceIAM,
142+ Region: ps[3],
143+ ResourceType: arnResourceTypeRole,
144+ ResourceID: res[1],
145+ }
146+ return
147+}
internal/config/identity/openid/help.go+18−0
@@ -50,6 +50,12 @@ var ( · 49 unmodified lines
5050 Optional: true,
5151 Type: "on|off",
5252 },
53+ config.HelpKV{
54+ Key: RolePolicy,
55+ Description: `Set the IAM access policies applicable to this client application and IDP e.g. "app-bucket-write,app-bucket-list"`,
56+ Optional: true,
57+ Type: "string",
58+ },
5359 config.HelpKV{
5460 Key: Scopes,
5561 Description: `Comma separated list of OpenID scopes for server, defaults to advertised scopes from discovery document e.g. "email,admin"`,
@@ -98,5 +104,17 @@ var ( · 42 unmodified lines
98104 Optional: true,
99105 Type: "sentence",
100106 },
107+ config.HelpKV{
108+ Key: ClaimPrefix,
109+ Description: `[DEPRECATED use 'claim_name'] JWT claim namespace prefix e.g. "customer1/"`,
110+ Optional: true,
111+ Type: "string",
112+ },
113+ config.HelpKV{
114+ Key: RedirectURI,
115+ Description: `[DEPRECATED use env 'MINIO_BROWSER_REDIRECT_URL'] Configure custom redirect_uri for OpenID login flow callback`,
116+ Optional: true,
117+ Type: "string",
118+ },
101119 }
102120 )
internal/config/identity/openid/jwt.go+52−3
@@ -23,6 +23,7 @@ import ( · 22 unmodified lines
2323 "errors"
2424 "fmt"
2525 "io"
26+ "net"
2627 "net/http"
2728 "strconv"
2829 "strings"
@@ -30,6 +31,7 @@ import ( · 1 unmodified line
3031 "time"
3132
3233 jwtgo "github.com/golang-jwt/jwt/v4"
34+ "github.com/minio/minio/internal/arn"
3335 "github.com/minio/minio/internal/auth"
3436 "github.com/minio/minio/internal/config"
3537 "github.com/minio/minio/internal/config/identity/openid/provider"
@@ -56,7 +58,9 @@ type Config struct { · 20 unmodified lines
5658 DiscoveryDoc DiscoveryDoc
5759 ClientID string
5860 ClientSecret string
61+ RolePolicy string
5962
63+ roleArn arn.ARN
6064 provider provider.Provider
6165 publicKeys map[string]crypto.PublicKey
6266 transport *http.Transport
@@ -167,6 +171,12 @@ func (r Config) ProviderEnabled() bool { · 104 unmodified lines
167171 return r.Enabled && r.provider != nil
168172 }
169173
174+// GetRoleInfo - returns role ARN and policy if present, otherwise returns false
175+// boolean.
176+func (r Config) GetRoleInfo() (arn.ARN, string, bool) {
177+ return r.roleArn, r.RolePolicy, r.RolePolicy != ""
178+}
179+
170180 // InitializeKeycloakProvider - initializes keycloak provider
171181 func (r *Config) InitializeKeycloakProvider(adminURL, realm string) error {
172182 var err error
@@ -366,6 +376,7 @@ const ( · 193 unmodified lines
366376 ClaimPrefix = "claim_prefix"
367377 ClientID = "client_id"
368378 ClientSecret = "client_secret"
379+ RolePolicy = "role_policy"
369380
370381 Vendor = "vendor"
371382 Scopes = "scopes"
@@ -383,6 +394,7 @@ const ( · 11 unmodified lines
383394 EnvIdentityOpenIDClaimName = "MINIO_IDENTITY_OPENID_CLAIM_NAME"
384395 EnvIdentityOpenIDClaimUserInfo = "MINIO_IDENTITY_OPENID_CLAIM_USERINFO"
385396 EnvIdentityOpenIDClaimPrefix = "MINIO_IDENTITY_OPENID_CLAIM_PREFIX"
397+ EnvIdentityOpenIDRolePolicy = "MINIO_IDENTITY_OPENID_ROLE_POLICY"
386398 EnvIdentityOpenIDRedirectURI = "MINIO_IDENTITY_OPENID_REDIRECT_URI"
387399 EnvIdentityOpenIDRedirectURIDynamic = "MINIO_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC"
388400 EnvIdentityOpenIDScopes = "MINIO_IDENTITY_OPENID_SCOPES"
@@ -458,6 +470,10 @@ var ( · 69 unmodified lines
458470 Key: ClaimUserinfo,
459471 Value: "",
460472 },
473+ config.KV{
474+ Key: RolePolicy,
475+ Value: "",
476+ },
461477 config.KV{
462478 Key: ClaimPrefix,
463479 Value: "",
@@ -483,7 +499,7 @@ func Enabled(kvs config.KVS) bool { · 19 unmodified lines
483499 }
484500
485501 // LookupConfig lookup jwks from config, override with any ENVs.
486−func LookupConfig(kvs config.KVS, transport *http.Transport, closeRespFn func(io.ReadCloser)) (c Config, err error) {
502+func LookupConfig(kvs config.KVS, transport *http.Transport, closeRespFn func(io.ReadCloser), serverRegion string) (c Config, err error) {
487503 // remove this since we have removed this already.
488504 kvs.Delete(JwksURL)
489505
@@ -501,16 +517,19 @@ func LookupConfig(kvs config.KVS, transport *http.Transport, closeRespFn func(io · 11 unmodified lines
501517 publicKeys: make(map[string]crypto.PublicKey),
502518 ClientID: env.Get(EnvIdentityOpenIDClientID, kvs.Get(ClientID)),
503519 ClientSecret: env.Get(EnvIdentityOpenIDClientSecret, kvs.Get(ClientSecret)),
520+ RolePolicy: env.Get(EnvIdentityOpenIDRolePolicy, kvs.Get(RolePolicy)),
504521 transport: transport,
505522 closeRespFn: closeRespFn,
506523 }
507524
508525 configURL := env.Get(EnvIdentityOpenIDURL, kvs.Get(ConfigURL))
526+ var configURLDomain string
509527 if configURL != "" {
510528 c.URL, err = xnet.ParseHTTPURL(configURL)
511529 if err != nil {
512530 return c, err
513531 }
532+ configURLDomain, _, _ = net.SplitHostPort(c.URL.Host)
514533 c.DiscoveryDoc, err = parseDiscoveryDoc(c.URL, transport, closeRespFn)
515534 if err != nil {
516535 return c, err
@@ -534,8 +553,38 @@ func LookupConfig(kvs config.KVS, transport *http.Transport, closeRespFn func(io · 17 unmodified lines
534553 c.DiscoveryDoc.ScopesSupported = scopes
535554 }
536555
537− if c.ClaimName == "" {
538− c.ClaimName = iampolicy.PolicyName
556+ // Check if claim name is the non-default value and role policy is set.
557+ if c.ClaimName != iampolicy.PolicyName && c.RolePolicy != "" {
558+ // In the unlikely event that the user specifies
559+ // `iampolicy.PolicyName` as the claim name explicitly and sets
560+ // a role policy, this check is thwarted, but we will be using
561+ // the role policy anyway.
562+ return c, config.Errorf("Role Policy and Claim Name cannot both be set.")
563+ }
564+
565+ if c.RolePolicy != "" {
566+ // RolePolicy is valided by IAM System during its
567+ // initialization.
568+
569+ // Generate role ARN as combination of provider domain and
570+ // prefix of client ID.
571+ domain := configURLDomain
572+ if domain == "" {
573+ // Attempt to parse the JWKs URI.
574+ domain, _, _ = net.SplitHostPort(c.JWKS.URL.Host)
575+ if domain == "" {
576+ return c, config.Errorf("unable to generate a domain from the OpenID config.")
577+ }
578+ }
579+ clientIDFragment := c.ClientID[:8]
580+ if clientIDFragment == "" {
581+ return c, config.Errorf("unable to get a non-empty clientID fragment from the OpenID config.")
582+ }
583+ resourceID := domain + "_" + clientIDFragment
584+ c.roleArn, err = arn.NewIAMRoleARN(resourceID, serverRegion)
585+ if err != nil {
586+ return c, config.Errorf("unable to generate ARN from the OpenID config: %v", err)
587+ }
539588 }
540589
541590 jwksURL := c.DiscoveryDoc.JwksURI

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t