proxy: Rework oidc role mapper to allow multiple matching roles
If multiple claims values have a valid matching for ocis roles, we'll pick the ocis role that appears first in the mapping configuration.
This commit is contained in:
committed by
Ralf Haferkamp
parent
b9dc2baaed
commit
6059747491
@@ -0,0 +1,23 @@
|
|||||||
|
Enhancement: Added possiblity to assign roles based on OIDC claims
|
||||||
|
|
||||||
|
oCIS can now be configured to update a user's role assignment from the values of a claim provided
|
||||||
|
via the IDPs userinfo endpoint. The claim name and the mapping between claim values and ocis role
|
||||||
|
name can be configured via the configuration of the proxy service. Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
role_assignment:
|
||||||
|
driver: oidc
|
||||||
|
oidc_role_mapper:
|
||||||
|
role_claim: ocisRoles
|
||||||
|
role_mapping:
|
||||||
|
- role_name: admin
|
||||||
|
claim_value: myAdminRole
|
||||||
|
- role_name: spaceadmin
|
||||||
|
claim_value: mySpaceAdminRole
|
||||||
|
- role_name: user
|
||||||
|
claim_value: myUserRole
|
||||||
|
- role_name: guest:
|
||||||
|
claim_value: myGuestRole
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/owncloud/ocis/pull/6048
|
||||||
+26
-11
@@ -50,10 +50,14 @@ role_assignment:
|
|||||||
oidc_role_mapper:
|
oidc_role_mapper:
|
||||||
role_claim: ocisRoles
|
role_claim: ocisRoles
|
||||||
role_mapping:
|
role_mapping:
|
||||||
admin: myAdminRole
|
- role_name: admin
|
||||||
user: myUserRole
|
claim_value: myAdminRole
|
||||||
spaceadmin: mySpaceAdminRole
|
- role_name: spaceadmin
|
||||||
guest: myGuestRole
|
claim_value: mySpaceAdminRole
|
||||||
|
- role_name: user
|
||||||
|
claim_value: myUserRole
|
||||||
|
- role_name: guest:
|
||||||
|
claim_value: myGuestRole
|
||||||
```
|
```
|
||||||
|
|
||||||
This would assign the role `admin` to users with the value `myAdminRole` in the claim `ocisRoles`.
|
This would assign the role `admin` to users with the value `myAdminRole` in the claim `ocisRoles`.
|
||||||
@@ -62,16 +66,27 @@ The role `user` to users with the values `myUserRole` in the claims `ocisRoles`
|
|||||||
Claim values that are not mapped to a specific ownCloud Infinite Scale role will be ignored.
|
Claim values that are not mapped to a specific ownCloud Infinite Scale role will be ignored.
|
||||||
|
|
||||||
Note: An ownCloud Infinite Scale user can only have a single role assigned. If the configured
|
Note: An ownCloud Infinite Scale user can only have a single role assigned. If the configured
|
||||||
`role_mapping` and a user's claim values result in multiple possible roles for a user, an error
|
`role_mapping` and a user's claim values result in multiple possible roles for a user, the order in
|
||||||
will be logged and the user will not be able to login.
|
which the role mappings are defined in the configuration is important. The first role in the
|
||||||
|
`role_mappings` where the `claim_value` matches a value from the user's roles claim will be assigned
|
||||||
|
to the user. So if e.g. a user's `ocisRoles` claim has the values `myUserRole` and
|
||||||
|
`mySpaceAdminRole` that user will get the ocis role `spaceadmin` assigned (because `spaceadmin`
|
||||||
|
appears before `user` in the above sample configuration).
|
||||||
|
|
||||||
The default `role_claim` (or `PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM`) is `roles`. The `role_mapping` is:
|
If a user's claim values don't match any of the configured role mappings an error will be logged and
|
||||||
|
the user will not be able to login.
|
||||||
|
|
||||||
|
The default `role_claim` (or `PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM`) is `roles`. The default `role_mapping` is:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
admin: ocisAdmin
|
- role_name: admin
|
||||||
user: ocisUser
|
claim_value: ocisAdmin
|
||||||
spaceadmin: ocisSpaceAdmin
|
- role_name: spaceadmin
|
||||||
guest: ocisGuest
|
claim_value: ocisSpaceAdmin
|
||||||
|
- role_name: user
|
||||||
|
claim_value: ocisUser
|
||||||
|
- role_name: guest:
|
||||||
|
claim_value: ocisGuest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Recommendations for Production Deployments
|
## Recommendations for Production Deployments
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
|
|||||||
userroles.WithRoleService(rolesClient),
|
userroles.WithRoleService(rolesClient),
|
||||||
userroles.WithLogger(logger),
|
userroles.WithLogger(logger),
|
||||||
userroles.WithRolesClaim(cfg.RoleAssignment.OIDCRoleMapper.RoleClaim),
|
userroles.WithRolesClaim(cfg.RoleAssignment.OIDCRoleMapper.RoleClaim),
|
||||||
userroles.WithRoleMapping(cfg.RoleAssignment.OIDCRoleMapper.RoleMapping),
|
userroles.WithRoleMapping(cfg.RoleAssignment.OIDCRoleMapper.RolesMap),
|
||||||
userroles.WithAutoProvisonCreator(autoProvsionCreator),
|
userroles.WithAutoProvisonCreator(autoProvsionCreator),
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -135,8 +135,14 @@ type RoleAssignment struct {
|
|||||||
|
|
||||||
// OIDCRoleMapper contains the configuration for the "oidc" role assignment driber
|
// OIDCRoleMapper contains the configuration for the "oidc" role assignment driber
|
||||||
type OIDCRoleMapper struct {
|
type OIDCRoleMapper struct {
|
||||||
RoleClaim string `yaml:"role_claim" env:"PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM" desc:"The OIDC claim used to create the users role assignment."`
|
RoleClaim string `yaml:"role_claim" env:"PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM" desc:"The OIDC claim used to create the users role assignment."`
|
||||||
RoleMapping map[string]string `yaml:"role_mapping" desc:"A mapping of ocis role names to PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM claim values. This setting can only be configured in the configuration file and not via environment variables."`
|
RolesMap []RoleMapping `yaml:"role_mapping" desc:"A list of mappings of ocis role names to PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM claim values. This setting can only be configured in the configuration file and not via environment variables."`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoleMapping defines which ocis role matches a specific claim value
|
||||||
|
type RoleMapping struct {
|
||||||
|
RoleName string `yaml:"role_name" desc:"The name of an ocis role that this mapping should apply for."`
|
||||||
|
ClaimValue string `yaml:"claim_value" desc:"The value of the 'PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM' that matches the role defined in 'role_name'."`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PolicySelector is the toplevel-configuration for different selectors
|
// PolicySelector is the toplevel-configuration for different selectors
|
||||||
|
|||||||
@@ -60,11 +60,11 @@ func DefaultConfig() *config.Config {
|
|||||||
// this default is only relevant when Driver is set to "oidc"
|
// this default is only relevant when Driver is set to "oidc"
|
||||||
OIDCRoleMapper: config.OIDCRoleMapper{
|
OIDCRoleMapper: config.OIDCRoleMapper{
|
||||||
RoleClaim: "roles",
|
RoleClaim: "roles",
|
||||||
RoleMapping: map[string]string{
|
RolesMap: []config.RoleMapping{
|
||||||
"admin": "ocisAdmin",
|
config.RoleMapping{RoleName: "admin", ClaimValue: "ocisAdmin"},
|
||||||
"spaceadmin": "ocisSpaceAdmin",
|
config.RoleMapping{RoleName: "spaceadmin", ClaimValue: "ocisSpaceAdmin"},
|
||||||
"user": "ocisUser",
|
config.RoleMapping{RoleName: "user", ClaimValue: "ocisUser"},
|
||||||
"guest": "ocisGuest",
|
config.RoleMapping{RoleName: "guest", ClaimValue: "ocisGuest"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,47 +34,55 @@ func NewOIDCRoleAssigner(opts ...Option) UserRoleAssigner {
|
|||||||
// already has a different role assigned.
|
// already has a different role assigned.
|
||||||
func (ra oidcRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error) {
|
func (ra oidcRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error) {
|
||||||
logger := ra.logger.SubloggerWithRequestID(ctx).With().Str("userid", user.GetId().GetOpaqueId()).Logger()
|
logger := ra.logger.SubloggerWithRequestID(ctx).With().Str("userid", user.GetId().GetOpaqueId()).Logger()
|
||||||
claimValueToRoleID, err := ra.oidcClaimvaluesToRoleIDs()
|
roleNamesToRoleIDs, err := ra.roleNamesToRoleIDs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Error mapping claims to roles ids")
|
logger.Error().Err(err).Msg("Error mapping role names to role ids")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
roleIDsFromClaim := make([]string, 0, 1)
|
claimRolesRaw, ok := claims[ra.rolesClaim].([]interface{})
|
||||||
claimRoles, ok := claims[ra.rolesClaim].([]interface{})
|
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Error().Err(err).Str("rolesClaim", ra.rolesClaim).Msg("No roles in user claims")
|
logger.Error().Str("rolesClaim", ra.rolesClaim).Msg("No roles in user claims")
|
||||||
return nil, err
|
return nil, errors.New("no roles in user claims")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug().Str("rolesClaim", ra.rolesClaim).Interface("rolesInClaim", claims[ra.rolesClaim]).Msg("got roles in claim")
|
logger.Debug().Str("rolesClaim", ra.rolesClaim).Interface("rolesInClaim", claims[ra.rolesClaim]).Msg("got roles in claim")
|
||||||
for _, cri := range claimRoles {
|
claimRoles := map[string]struct{}{}
|
||||||
|
for _, cri := range claimRolesRaw {
|
||||||
cr, ok := cri.(string)
|
cr, ok := cri.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
err := errors.New("invalid role in claims")
|
err := errors.New("invalid role in claims")
|
||||||
logger.Error().Err(err).Interface("claimValue", cri).Msg("Is not a valid string.")
|
logger.Error().Err(err).Interface("claimValue", cri).Msg("Is not a valid string.")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
id, ok := claimValueToRoleID[cr]
|
|
||||||
if !ok {
|
|
||||||
logger.Debug().Str("role", cr).Msg("No mapping for claim role. Skipped.")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
roleIDsFromClaim = append(roleIDsFromClaim, id)
|
|
||||||
}
|
|
||||||
logger.Debug().Interface("roleIDs", roleIDsFromClaim).Msg("Mapped claim roles to roleids")
|
|
||||||
|
|
||||||
switch len(roleIDsFromClaim) {
|
claimRoles[cr] = struct{}{}
|
||||||
default:
|
}
|
||||||
err := errors.New("too many roles found in claims")
|
|
||||||
logger.Error().Err(err).Msg("Only one role per user is allowed.")
|
if len(claimRoles) == 0 {
|
||||||
return nil, err
|
err := errors.New("no roles set in claim")
|
||||||
case 0:
|
|
||||||
err := errors.New("no role in claim, maps to a ocis role")
|
|
||||||
logger.Error().Err(err).Msg("")
|
logger.Error().Err(err).Msg("")
|
||||||
return nil, err
|
return nil, err
|
||||||
case 1:
|
|
||||||
// exactly one mapping. This is right
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the roleMapping config is supposed to have the role mappings ordered from the highest privileged role
|
||||||
|
// down to the lowest privileged role. Since ocis currently only can handle a single role assignment we
|
||||||
|
// pick the highest privileged role that matches a value from the claims
|
||||||
|
roleIDFromClaim := ""
|
||||||
|
for _, mapping := range ra.Options.roleMapping {
|
||||||
|
if _, ok := claimRoles[mapping.ClaimValue]; ok {
|
||||||
|
logger.Debug().Str("ocisRole", mapping.RoleName).Str("role id", roleNamesToRoleIDs[mapping.RoleName]).Msg("first matching role")
|
||||||
|
roleIDFromClaim = roleNamesToRoleIDs[mapping.RoleName]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if roleIDFromClaim == "" {
|
||||||
|
err := errors.New("no role in claim maps to an ocis role")
|
||||||
|
logger.Error().Err(err).Msg("")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
assignedRoles, err := loadRolesIDs(ctx, user.GetId().GetOpaqueId(), ra.roleService)
|
assignedRoles, err := loadRolesIDs(ctx, user.GetId().GetOpaqueId(), ra.roleService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Could not load roles")
|
logger.Error().Err(err).Msg("Could not load roles")
|
||||||
@@ -86,8 +94,9 @@ func (ra oidcRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *c
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
logger.Debug().Interface("assignedRoleIds", assignedRoles).Msg("Currently assigned roles")
|
logger.Debug().Interface("assignedRoleIds", assignedRoles).Msg("Currently assigned roles")
|
||||||
if len(assignedRoles) == 0 || (assignedRoles[0] != roleIDsFromClaim[0]) {
|
|
||||||
logger.Debug().Interface("assignedRoleIds", assignedRoles).Interface("newRoleIds", roleIDsFromClaim).Msg("Updating role assignment for user")
|
if len(assignedRoles) == 0 || (assignedRoles[0] != roleIDFromClaim) {
|
||||||
|
logger.Debug().Interface("assignedRoleIds", assignedRoles).Interface("newRoleId", roleIDFromClaim).Msg("Updating role assignment for user")
|
||||||
newctx, err := ra.prepareAdminContext()
|
newctx, err := ra.prepareAdminContext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Error creating admin context")
|
logger.Error().Err(err).Msg("Error creating admin context")
|
||||||
@@ -95,14 +104,14 @@ func (ra oidcRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *c
|
|||||||
}
|
}
|
||||||
if _, err = ra.roleService.AssignRoleToUser(newctx, &settingssvc.AssignRoleToUserRequest{
|
if _, err = ra.roleService.AssignRoleToUser(newctx, &settingssvc.AssignRoleToUserRequest{
|
||||||
AccountUuid: user.GetId().GetOpaqueId(),
|
AccountUuid: user.GetId().GetOpaqueId(),
|
||||||
RoleId: roleIDsFromClaim[0],
|
RoleId: roleIDFromClaim,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.Error().Err(err).Msg("Role assignment failed")
|
logger.Error().Err(err).Msg("Role assignment failed")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDsFromClaim)
|
user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", []string{roleIDFromClaim})
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,32 +145,32 @@ func (ra oidcRoleAssigner) prepareAdminContext() (context.Context, error) {
|
|||||||
return newctx, nil
|
return newctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type roleClaimToIDCache struct {
|
type roleNameToIDCache struct {
|
||||||
roleClaimToID map[string]string
|
roleNameToID map[string]string
|
||||||
lastRead time.Time
|
lastRead time.Time
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var roleClaimToID roleClaimToIDCache
|
var roleNameToID roleNameToIDCache
|
||||||
|
|
||||||
func (ra oidcRoleAssigner) oidcClaimvaluesToRoleIDs() (map[string]string, error) {
|
func (ra oidcRoleAssigner) roleNamesToRoleIDs() (map[string]string, error) {
|
||||||
cacheTTL := 5 * time.Minute
|
cacheTTL := 5 * time.Minute
|
||||||
roleClaimToID.lock.RLock()
|
roleNameToID.lock.RLock()
|
||||||
|
|
||||||
if !roleClaimToID.lastRead.IsZero() && time.Since(roleClaimToID.lastRead) < cacheTTL {
|
if !roleNameToID.lastRead.IsZero() && time.Since(roleNameToID.lastRead) < cacheTTL {
|
||||||
defer roleClaimToID.lock.RUnlock()
|
defer roleNameToID.lock.RUnlock()
|
||||||
return roleClaimToID.roleClaimToID, nil
|
return roleNameToID.roleNameToID, nil
|
||||||
}
|
}
|
||||||
ra.logger.Debug().Msg("refreshing roles ids")
|
ra.logger.Debug().Msg("refreshing roles ids")
|
||||||
|
|
||||||
// cache needs Refresh get a write lock
|
// cache needs Refresh get a write lock
|
||||||
roleClaimToID.lock.RUnlock()
|
roleNameToID.lock.RUnlock()
|
||||||
roleClaimToID.lock.Lock()
|
roleNameToID.lock.Lock()
|
||||||
defer roleClaimToID.lock.Unlock()
|
defer roleNameToID.lock.Unlock()
|
||||||
|
|
||||||
// check again, another goroutine might have updated while we "upgraded" the lock
|
// check again, another goroutine might have updated while we "upgraded" the lock
|
||||||
if !roleClaimToID.lastRead.IsZero() && time.Since(roleClaimToID.lastRead) < cacheTTL {
|
if !roleNameToID.lastRead.IsZero() && time.Since(roleNameToID.lastRead) < cacheTTL {
|
||||||
return roleClaimToID.roleClaimToID, nil
|
return roleNameToID.roleNameToID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all roles to find the role IDs.
|
// Get all roles to find the role IDs.
|
||||||
@@ -183,16 +192,10 @@ func (ra oidcRoleAssigner) oidcClaimvaluesToRoleIDs() (map[string]string, error)
|
|||||||
newIDs := map[string]string{}
|
newIDs := map[string]string{}
|
||||||
for _, role := range res.Bundles {
|
for _, role := range res.Bundles {
|
||||||
ra.logger.Debug().Str("role", role.Name).Str("id", role.Id).Msg("Got Role")
|
ra.logger.Debug().Str("role", role.Name).Str("id", role.Id).Msg("Got Role")
|
||||||
roleClaim, ok := ra.roleMapping[role.Name]
|
newIDs[role.Name] = role.Id
|
||||||
if !ok {
|
|
||||||
err := errors.New("Incomplete role mapping")
|
|
||||||
ra.logger.Error().Err(err).Str("role", role.Name).Msg("Role not mapped to a claim value")
|
|
||||||
return map[string]string{}, err
|
|
||||||
}
|
|
||||||
newIDs[roleClaim] = role.Id
|
|
||||||
}
|
}
|
||||||
ra.logger.Debug().Interface("roleMap", newIDs).Msg("Claim Role to role ID map")
|
ra.logger.Debug().Interface("roleMap", newIDs).Msg("Role Name to role ID map")
|
||||||
roleClaimToID.roleClaimToID = newIDs
|
roleNameToID.roleNameToID = newIDs
|
||||||
roleClaimToID.lastRead = time.Now()
|
roleNameToID.lastRead = time.Now()
|
||||||
return roleClaimToID.roleClaimToID, nil
|
return roleNameToID.roleNameToID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||||
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
||||||
"github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision"
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision"
|
||||||
|
"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate mockery --name=UserRoleAssigner
|
//go:generate mockery --name=UserRoleAssigner
|
||||||
@@ -26,7 +27,7 @@ type UserRoleAssigner interface {
|
|||||||
type Options struct {
|
type Options struct {
|
||||||
roleService settingssvc.RoleService
|
roleService settingssvc.RoleService
|
||||||
rolesClaim string
|
rolesClaim string
|
||||||
roleMapping map[string]string
|
roleMapping []config.RoleMapping
|
||||||
autoProvsionCreator autoprovision.Creator
|
autoProvsionCreator autoprovision.Creator
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,7 @@ func WithRolesClaim(claim string) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithRoleMapping configures the map of ocis role names to claims values
|
// WithRoleMapping configures the map of ocis role names to claims values
|
||||||
func WithRoleMapping(roleMap map[string]string) Option {
|
func WithRoleMapping(roleMap []config.RoleMapping) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.roleMapping = roleMap
|
o.roleMapping = roleMap
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user