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.
This commit is contained in:
Aditya Manthramurthy
2021-11-26 19:22:40 -08:00
committed by GitHub
parent 4ce6d35e30
commit 4c0f48c548
12 changed files with 582 additions and 80 deletions

View File

@@ -33,7 +33,9 @@ import (
humanize "github.com/dustin/go-humanize"
"github.com/minio/madmin-go"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/arn"
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/color"
"github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/iam/policy"
etcd "go.etcd.io/etcd/client/v3"
@@ -66,6 +68,8 @@ type IAMSys struct {
usersSysType UsersSysType
rolesMap map[arn.ARN]string
// Persistence layer for IAM subsystem
store *IAMStoreSys
@@ -215,6 +219,7 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
r := rand.New(rand.NewSource(time.Now().UnixNano()))
// Migrate storage format if needed.
for {
// let one of the server acquire the lock, if not let them timeout.
// which shall be retried again by this loop.
@@ -263,6 +268,7 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
break
}
// Load IAM data from storage.
for {
if err := sys.Load(retryCtx); err != nil {
if configRetriableErrors(err) {
@@ -308,7 +314,40 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
}()
}
// Start watching changes to storage.
go sys.watch(ctx)
// Load RoleARN
if roleARN, rolePolicy, enabled := globalOpenIDConfig.GetRoleInfo(); enabled {
numPolicies := len(strings.Split(rolePolicy, ","))
validPolicies, _ := sys.store.FilterPolicies(rolePolicy, "")
numValidPolicies := len(strings.Split(validPolicies, ","))
if numPolicies != numValidPolicies {
logger.LogIf(ctx, fmt.Errorf("Some specified role policies (%s) were not defined - role based policies will not be enabled.", rolePolicy))
return
}
sys.rolesMap = map[arn.ARN]string{
roleARN: rolePolicy,
}
}
sys.printIAMRoles()
}
// Prints IAM role ARNs.
func (sys *IAMSys) printIAMRoles() {
arns := sys.GetRoleARNs()
if len(arns) == 0 {
return
}
msgs := make([]string, 0, len(arns))
for _, arn := range arns {
msgs = append(msgs, color.Bold(arn))
}
logStartupMessage(fmt.Sprintf("%s %s", color.Blue("IAM Roles:"), strings.Join(msgs, " ")))
}
// 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
return err
}
// GetRoleARNs - returns a list of enabled role ARNs.
func (sys *IAMSys) GetRoleARNs() []string {
var res []string
for arn := range sys.rolesMap {
res = append(res, arn.String())
}
return res
}
// GetRolePolicy - returns policies associated with a role ARN.
func (sys *IAMSys) GetRolePolicy(arnStr string) (string, error) {
arn, err := arn.Parse(arnStr)
if err != nil {
return "", fmt.Errorf("RoleARN parse err: %v", err)
}
rolePolicy, ok := sys.rolesMap[arn]
if !ok {
return "", fmt.Errorf("RoleARN %s is not defined.", arnStr)
}
return rolePolicy, nil
}
// DeletePolicy - deletes a canned policy from backend or etcd.
func (sys *IAMSys) DeletePolicy(ctx context.Context, policyName string) error {
if !sys.Initialized() {
@@ -1029,7 +1090,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
return false
}
// Check policy for this service account.
// Check policy for parent user of service account.
svcPolicies, err := sys.PolicyDBGet(parentUser, false, args.Groups...)
if err != nil {
logger.LogIf(GlobalContext, err)
@@ -1037,9 +1098,20 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
}
if len(svcPolicies) == 0 {
// If parent user has no policies, look in OpenID claims in case it exists.
policySet, ok := iampolicy.GetPoliciesFromClaims(args.Claims, iamPolicyClaimNameOpenID())
if ok {
// If parent user has no policies, check for OpenID
// claims/RoleARN in case it exists.
roleArn := args.GetRoleArn()
if roleArn != "" {
arn, err := arn.Parse(roleArn)
if err != nil {
logger.LogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
return false
}
svcPolicies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
} else {
// If there is no roleArn claim, check the OpenID
// provider's policy claim.
policySet, _ := iampolicy.GetPoliciesFromClaims(args.Claims, iamPolicyClaimNameOpenID())
svcPolicies = policySet.ToSlice()
}
if len(svcPolicies) == 0 {
@@ -1156,35 +1228,49 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
return sys.IsAllowedLDAPSTS(args, parentUser)
}
policies, ok := args.GetPolicies(iamPolicyClaimNameOpenID())
if !ok {
// When claims are set, it should have a policy claim field.
return false
var policies []string
roleArn := args.GetRoleArn()
if roleArn != "" {
arn, err := arn.Parse(roleArn)
if err != nil {
logger.LogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
return false
}
policies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
} else {
// If roleArn is not used, we fall back to using policy claim
// from JWT.
policySet, ok := args.GetPolicies(iamPolicyClaimNameOpenID())
if !ok {
// When claims are set, it should have a policy claim field.
return false
}
// When claims are set, it should have policies as claim.
if policySet.IsEmpty() {
// No policy, no access!
return false
}
// If policy is available for given user, check the policy.
mp, ok := sys.store.GetMappedPolicy(args.AccountName, false)
if !ok {
// No policy set for the user that we can find, no access!
return false
}
if !policySet.Equals(mp.policySet()) {
// When claims has a policy, it should match the
// policy of args.AccountName which server remembers.
// if not reject such requests.
return false
}
policies = policySet.ToSlice()
}
// When claims are set, it should have policies as claim.
if policies.IsEmpty() {
// No policy, no access!
return false
}
// If policy is available for given user, check the policy.
mp, ok := sys.store.GetMappedPolicy(args.AccountName, false)
if !ok {
// No policy set for the user that we can find, no access!
return false
}
if !policies.Equals(mp.policySet()) {
// When claims has a policy, it should match the
// policy of args.AccountName which server remembers.
// if not reject such requests.
return false
}
combinedPolicy, err := sys.store.GetPolicy(strings.Join(policies.ToSlice(), ","))
combinedPolicy, err := sys.store.GetPolicy(strings.Join(policies, ","))
if err == errNoSuchPolicy {
for pname := range policies {
for _, pname := range policies {
_, err := sys.store.GetPolicy(pname)
if err == errNoSuchPolicy {
// all policies presented in the claim should exist