Skip to content

Commit

Permalink
wip: Extract role assignments from claims
Browse files Browse the repository at this point in the history
This is still very rough.
  • Loading branch information
rhafer committed Mar 15, 2023
1 parent 125b982 commit 3a59f75
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 26 deletions.
28 changes: 14 additions & 14 deletions services/graph/pkg/service/v0/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/identity"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
settingssvc "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
"golang.org/x/exp/slices"
)

Expand Down Expand Up @@ -317,20 +316,21 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) {
return
}

// TODO make this configurable, role assignments might be coming from the IDP
// assign roles if possible
if g.roleService != nil {
// All users get the user role by default currently.
// to all new users for now, as create Account request does not have any role field
if _, err = g.roleService.AssignRoleToUser(r.Context(), &settings.AssignRoleToUserRequest{
AccountUuid: *u.Id,
RoleId: settingssvc.BundleUUIDRoleUser,
}); err != nil {
// log as error, admin eventually needs to do something
logger.Error().Err(err).Str("id", *u.Id).Str("role", settingssvc.BundleUUIDRoleUser).Msg("could not create user: role assignment failed")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "role assignment failed")
return
}
}
// if g.roleService != nil {
// // All users get the user role by default currently.
// // to all new users for now, as create Account request does not have any role field
// if _, err = g.roleService.AssignRoleToUser(r.Context(), &settings.AssignRoleToUserRequest{
// AccountUuid: *u.Id,
// RoleId: settingssvc.BundleUUIDRoleUser,
// }); err != nil {
// // log as error, admin eventually needs to do something
// logger.Error().Err(err).Str("id", *u.Id).Str("role", settingssvc.BundleUUIDRoleUser).Msg("could not create user: role assignment failed")
// errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "role assignment failed")
// return
// }
// }

e := events.UserCreated{UserID: *u.Id}
if currentUser, ok := revactx.ContextGetUser(r.Context()); ok {
Expand Down
36 changes: 24 additions & 12 deletions services/proxy/pkg/middleware/account_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,20 @@ func AccountResolver(optionSetters ...Option) func(next http.Handler) http.Handl
userOIDCClaim: options.UserOIDCClaim,
userCS3Claim: options.UserCS3Claim,
autoProvisionAccounts: options.AutoprovisionAccounts,
// TODO turn this into configuration
roleAssignmentFromOIDCClaims: true,
}
}
}

type accountResolver struct {
next http.Handler
logger log.Logger
userProvider backend.UserBackend
autoProvisionAccounts bool
userOIDCClaim string
userCS3Claim string
next http.Handler
logger log.Logger
userProvider backend.UserBackend
autoProvisionAccounts bool
roleAssignmentFromOIDCClaims bool
userOIDCClaim string
userCS3Claim string
}

// TODO do not use the context to store values: https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39
Expand Down Expand Up @@ -94,12 +97,21 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}

// resolve the user's roles
user, err = m.userProvider.GetUserRoles(ctx, user)
if err != nil {
m.logger.Error().Err(err).Msg("Could not get user roles")
w.WriteHeader(http.StatusInternalServerError)
return
if m.roleAssignmentFromOIDCClaims {
user, err = m.userProvider.UpdateRoleAssignmentsFromClaims(ctx, user, claims)
if err != nil {
m.logger.Error().Err(err).Msg("Could not get user roles")
w.WriteHeader(http.StatusInternalServerError)
return
}
} else {
// resolve the user's roles
user, err = m.userProvider.GetUserRoles(ctx, user)
if err != nil {
m.logger.Error().Err(err).Msg("Could not get user roles")
w.WriteHeader(http.StatusInternalServerError)
return
}
}

// add user to context for selectors
Expand Down
1 change: 1 addition & 0 deletions services/proxy/pkg/user/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type UserBackend interface {
GetUserRoles(ctx context.Context, user *cs3.User) (*cs3.User, error)
Authenticate(ctx context.Context, username string, password string) (*cs3.User, string, error)
CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error)
UpdateRoleAssignmentsFromClaims(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error)
}

// RevaAuthenticator helper interface to mock auth-method from reva gateway-client.
Expand Down
127 changes: 127 additions & 0 deletions services/proxy/pkg/user/backend/cs3.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package backend
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -185,6 +186,132 @@ func (c *cs3backend) Authenticate(ctx context.Context, username string, password
return res.User, res.Token, nil
}

// UpdateRoleAssignmentsFromClaims updates an authenticated user's role
// assignments in the settings service based on the roles listed in the user's
// claims.
func (c *cs3backend) UpdateRoleAssignmentsFromClaims(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error) {
// TODO turn this into configuration
roleMapping := map[string]string{
"ocisAdmin": "admin",
"ocisSpaceAdmin": "spaceadmin",
"ocisUser": "user",
"ocisGuest": "guest",
}
rolesClaim := "roles"

roleNameToId := map[string]string{}

// To list roles and update assignment we need some elevated access to the settings service
// prepare a new request context for that until we have service accounts
newctx := context.Background()
autoProvisionUser, err := getAutoProvisionUserCreator()
if err != nil {
return nil, err
}
token, err := c.generateAutoProvisionAdminToken(newctx)
if err != nil {
c.logger.Error().Err(err).Msg("Error generating token for provisioning role assignments.")
return nil, err
}
newctx = revactx.ContextSetToken(newctx, token)
newctx = metadata.Set(newctx, middleware.AccountID, autoProvisionUser.Id.OpaqueId)
newctx = metadata.Set(newctx, middleware.RoleIDs, string(autoProvisionUser.Opaque.Map["roles"].Value))

// Get all roles to find the role IDs.
// TODO: we need to cache this. Roles IDs change rarely and this is a pretty expensive call
req := &settingssvc.ListBundlesRequest{}
res, err := c.roleService.ListRoles(newctx, req)
for _, role := range res.Bundles {
c.logger.Error().Str("role", role.Name).Str("id", role.Id).Msg("Got Role")
roleNameToId[role.Name] = role.Id
}

if err != nil {
c.logger.Error().Err(err).Msg("Error listing roles.")
return nil, err
}

var roleIdsFromClaim []string
c.logger.Error().Interface("rolesclaim", claims[rolesClaim]).Msg("Got ClaimRoles")
if claimRoles, ok := claims[rolesClaim].([]interface{}); ok {
for _, cri := range claimRoles {
cr, ok := cri.(string)
if !ok {
err := errors.New("invalid role in claims")
c.logger.Error().Err(err).Interface("claim value", cri).Msg("Is not a valid string.")
return nil, err
}
ocisRole, ok := roleMapping[cr]
if !ok {
c.logger.Error().Str("role", cr).Msg("Skipping unmaped role from claims.")
continue
}
id, ok := roleNameToId[ocisRole]
if !ok {
err := errors.New("ocis role does not exist")
c.logger.Error().Err(err).Str("ocisRole", ocisRole).Msg("Error mapping role to id")
return nil, err
}
roleIdsFromClaim = append(roleIdsFromClaim, id)
}
} else {
c.logger.Error().Err(err).Msg("No roles in user claims.")
return nil, err
}
c.logger.Error().Interface("roleIds", roleIdsFromClaim).Msg("Mapped roles from claim")
switch len(roleIdsFromClaim) {
default:
err := errors.New("too many roles found in claims")
c.logger.Error().Err(err).Msg("Only one role per user is allowed.")
return nil, err
case 0:
err := errors.New("no role in claim, maps to a ocis role")
c.logger.Error().Err(err).Msg("")
return nil, err
case 1:
// exactly one mapping. This is right
break
}

assignedRoles, err := loadRolesIDs(newctx, user.GetId().GetOpaqueId(), c.roleService)
if err != nil {
c.logger.Error().Err(err).Msgf("Could not load roles")
return nil, err
}
if len(assignedRoles) > 1 {
err := errors.New("too many roles assigned")
c.logger.Error().Err(err).Msg("The user has too many roles assigned")
return nil, err
}
c.logger.Error().Interface("assignedRoleIds", assignedRoles).Msg("Currently assigned roles")
if len(assignedRoles) == 0 || (assignedRoles[0] != roleIdsFromClaim[0]) {
if _, err = c.roleService.AssignRoleToUser(newctx, &settingssvc.AssignRoleToUserRequest{
AccountUuid: user.GetId().GetOpaqueId(),
RoleId: roleIdsFromClaim[0],
}); err != nil {
c.logger.Error().Err(err).Msg("Role assignment failed")
return nil, err
}
}
enc, err := encodeRoleIDs(roleIdsFromClaim)
if err != nil {
c.logger.Error().Err(err).Msg("Could not encode loaded roles")
return nil, err
}

if user.Opaque == nil {
user.Opaque = &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"roles": enc,
},
}
} else {
user.Opaque.Map["roles"] = enc
}

return user, nil
}

// CreateUserFromClaims creates a new user via libregraph users API, taking the
// attributes from the provided `claims` map. On success it returns the new
// user. If the user already exist this is not considered an error and the
Expand Down

0 comments on commit 3a59f75

Please sign in to comment.