Skip to content

Commit

Permalink
Add role ARN support for OIDC identity provider
Browse files Browse the repository at this point in the history
- Allows setting a role policy parameter when configuring OIDC provider

- When role policy is set, 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.
  • Loading branch information
donatello committed Nov 16, 2021
1 parent 07c5e72 commit e3fbf7b
Show file tree
Hide file tree
Showing 10 changed files with 482 additions and 77 deletions.
4 changes: 2 additions & 2 deletions cmd/config-current.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ func validateConfig(s config.Config) error {
}
}
if _, err := openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
NewGatewayHTTPTransport(), xhttp.DrainBody, globalServerRegion); err != nil {
return err
}

Expand Down Expand Up @@ -501,7 +501,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
}

globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
NewGatewayHTTPTransport(), xhttp.DrainBody)
NewGatewayHTTPTransport(), xhttp.DrainBody, globalServerRegion)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
}
Expand Down
128 changes: 104 additions & 24 deletions cmd/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -66,6 +68,8 @@ type IAMSys struct {

usersSysType UsersSysType

rolesMap map[arn.ARN]string

// Persistence layer for IAM subsystem
store *IAMStoreSys

Expand Down Expand Up @@ -209,6 +213,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.
Expand Down Expand Up @@ -257,6 +262,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) {
Expand Down Expand Up @@ -302,7 +308,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
Expand Down Expand Up @@ -416,6 +455,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() {
Expand Down Expand Up @@ -945,7 +1006,7 @@ func (sys *IAMSys) GetUser(ctx context.Context, accessKey string) (cred auth.Cre
}

if ok && cred.IsValid() {
if cred.IsServiceAccount() || cred.IsTemp() {
if cred.IsServiceAccount() {
policies, err := sys.store.PolicyDBGet(cred.AccessKey, false)
if err != nil {
// Reject if the policy map for user doesn't exist anymore.
Expand Down Expand Up @@ -1196,35 +1257,54 @@ 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
}
p, ok := sys.rolesMap[arn]
if !ok {
return false
}
mp := newMappedPolicy(p)
policies = mp.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
}

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

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
policies = policySet.ToSlice()
}

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
Expand Down
85 changes: 51 additions & 34 deletions cmd/sts-handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
stsAction = "Action"
stsPolicy = "Policy"
stsToken = "Token"
stsRoleArn = "RoleArn"
stsWebIdentityToken = "WebIdentityToken"
stsWebIdentityAccessToken = "WebIdentityAccessToken" // only valid if UserInfo is enabled.
stsDurationSeconds = "DurationSeconds"
Expand Down Expand Up @@ -73,6 +74,9 @@ const (
// LDAP claim keys
ldapUser = "ldapUser"
ldapUserN = "ldapUsername"

// Role Claim key
roleArnClaim = "roleArn"
)

func parseOpenIDParentUser(parentUser string) (userID string, err error) {
Expand Down Expand Up @@ -399,45 +403,42 @@ func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Requ
}
}

var subFromToken string
if v, ok := m[subClaim]; ok {
subFromToken, _ = v.(string)
}

if subFromToken == "" {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
errors.New("STS JWT Token has `sub` claim missing, `sub` claim is mandatory"))
return
}

var issFromToken string
if v, ok := m[issClaim]; ok {
issFromToken, _ = v.(string)
}

// JWT has requested a custom claim with policy value set.
// This is a MinIO STS API specific value, this value should
// be set and configured on your identity provider as part of
// JWT custom claims.
var policyName string
policySet, ok := iampolicy.GetPoliciesFromClaims(m, iamPolicyClaimNameOpenID())
policies := strings.Join(policySet.ToSlice(), ",")
if ok {
policyName = globalIAMSys.CurrentPolicies(policies)
}

if globalPolicyOPA == nil {
if !ok {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID()))
return
} else if policyName == "" {
roleArn := r.Form.Get(stsRoleArn)
if roleArn != "" {
_, err := globalIAMSys.GetRolePolicy(roleArn)
if err != nil {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
fmt.Errorf("None of the given policies (`%s`) are defined, credentials will not be generated", policies))
fmt.Errorf("Error processing %s parameter: %v", stsRoleArn, err))
return
}
// If roleArn is used, we set it as a claim, and use the
// associated policy when credentials are used.
m[roleArnClaim] = roleArn
} else {
// JWT has requested a custom claim with policy value set.
// This is a MinIO STS API specific value, this value should
// be set and configured on your identity provider as part of
// JWT custom claims.
policySet, ok := iampolicy.GetPoliciesFromClaims(m, iamPolicyClaimNameOpenID())
policies := strings.Join(policySet.ToSlice(), ",")
if ok {
policyName = globalIAMSys.CurrentPolicies(policies)
}

if globalPolicyOPA == nil {
if !ok {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID()))
return
} else if policyName == "" {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
fmt.Errorf("None of the given policies (`%s`) are defined, credentials will not be generated", policies))
return
}
}
m[iamPolicyClaimNameOpenID()] = policyName
}
m[iamPolicyClaimNameOpenID()] = policyName

sessionPolicyStr := r.Form.Get(stsPolicy)
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
Expand Down Expand Up @@ -476,6 +477,22 @@ func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Requ
// this is to ensure that ParentUser doesn't change and we get to use
// parentUser as per the requirements for service accounts for OpenID
// based logins.
var subFromToken string
if v, ok := m[subClaim]; ok {
subFromToken, _ = v.(string)
}

if subFromToken == "" {
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
errors.New("STS JWT Token has `sub` claim missing, `sub` claim is mandatory"))
return
}

var issFromToken string
if v, ok := m[issClaim]; ok {
issFromToken, _ = v.(string)
}

cred.ParentUser = "openid:" + subFromToken + ":" + issFromToken

// Set the newly generated credentials.
Expand Down

0 comments on commit e3fbf7b

Please sign in to comment.