Initial QSfera import
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
package userroles
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/qsfera/server/pkg/middleware"
|
||||
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"go-micro.dev/v4/metadata"
|
||||
)
|
||||
|
||||
type defaultRoleAssigner struct {
|
||||
Options
|
||||
}
|
||||
|
||||
// NewDefaultRoleAssigner returns an implementation of the UserRoleAssigner interface
|
||||
func NewDefaultRoleAssigner(opts ...Option) UserRoleAssigner {
|
||||
opt := Options{}
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return defaultRoleAssigner{
|
||||
Options: opt,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateUserRoleAssignment assigns the role "User" to the supplied user. Unless the user
|
||||
// already has a different role assigned.
|
||||
func (d defaultRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]any) (*cs3.User, error) {
|
||||
var roleIDs []string
|
||||
if user.Id.Type != cs3.UserType_USER_TYPE_LIGHTWEIGHT {
|
||||
var err error
|
||||
roleIDs, err = loadRolesIDs(ctx, user.Id.OpaqueId, d.roleService)
|
||||
if err != nil {
|
||||
d.logger.Error().Err(err).Msg("Could not load roles")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(roleIDs) == 0 {
|
||||
// This user doesn't have a role assignment yet. Assign a
|
||||
// default user role. At least until proper roles are provided. See
|
||||
// https://github.com/owncloud/ocis/issues/1825 for more context.
|
||||
if user.Id.Type == cs3.UserType_USER_TYPE_PRIMARY || user.Id.Type == cs3.UserType_USER_TYPE_GUEST {
|
||||
roleId := defaults.BundleUUIDRoleUser
|
||||
if user.Id.Type == cs3.UserType_USER_TYPE_GUEST {
|
||||
roleId = defaults.BundleUUIDRoleGuest
|
||||
}
|
||||
d.logger.Info().Str("userid", user.Id.OpaqueId).Msg("user has no role assigned, assigning default user role")
|
||||
ctx = metadata.Set(ctx, middleware.AccountID, user.Id.OpaqueId)
|
||||
_, err := d.roleService.AssignRoleToUser(ctx, &settingssvc.AssignRoleToUserRequest{
|
||||
AccountUuid: user.Id.OpaqueId,
|
||||
RoleId: roleId,
|
||||
})
|
||||
if err != nil {
|
||||
d.logger.Error().Err(err).Msg("Could not add default role")
|
||||
return nil, err
|
||||
}
|
||||
roleIDs = append(roleIDs, roleId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDs)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ApplyUserRole it looks up the user's role in the settings service and adds it
|
||||
// user's opaque data
|
||||
func (d defaultRoleAssigner) ApplyUserRole(ctx context.Context, user *cs3.User) (*cs3.User, error) {
|
||||
roleIDs, err := loadRolesIDs(ctx, user.Id.OpaqueId, d.roleService)
|
||||
if err != nil {
|
||||
d.logger.Error().Err(err).Msg("Could not load roles")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDs)
|
||||
return user, nil
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewUserRoleAssigner creates a new instance of UserRoleAssigner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewUserRoleAssigner(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *UserRoleAssigner {
|
||||
mock := &UserRoleAssigner{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// UserRoleAssigner is an autogenerated mock type for the UserRoleAssigner type
|
||||
type UserRoleAssigner struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type UserRoleAssigner_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *UserRoleAssigner) EXPECT() *UserRoleAssigner_Expecter {
|
||||
return &UserRoleAssigner_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// ApplyUserRole provides a mock function for the type UserRoleAssigner
|
||||
func (_mock *UserRoleAssigner) ApplyUserRole(ctx context.Context, user *userv1beta1.User) (*userv1beta1.User, error) {
|
||||
ret := _mock.Called(ctx, user)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ApplyUserRole")
|
||||
}
|
||||
|
||||
var r0 *userv1beta1.User
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *userv1beta1.User) (*userv1beta1.User, error)); ok {
|
||||
return returnFunc(ctx, user)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *userv1beta1.User) *userv1beta1.User); ok {
|
||||
r0 = returnFunc(ctx, user)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*userv1beta1.User)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, *userv1beta1.User) error); ok {
|
||||
r1 = returnFunc(ctx, user)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UserRoleAssigner_ApplyUserRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApplyUserRole'
|
||||
type UserRoleAssigner_ApplyUserRole_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ApplyUserRole is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - user *userv1beta1.User
|
||||
func (_e *UserRoleAssigner_Expecter) ApplyUserRole(ctx interface{}, user interface{}) *UserRoleAssigner_ApplyUserRole_Call {
|
||||
return &UserRoleAssigner_ApplyUserRole_Call{Call: _e.mock.On("ApplyUserRole", ctx, user)}
|
||||
}
|
||||
|
||||
func (_c *UserRoleAssigner_ApplyUserRole_Call) Run(run func(ctx context.Context, user *userv1beta1.User)) *UserRoleAssigner_ApplyUserRole_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 *userv1beta1.User
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(*userv1beta1.User)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserRoleAssigner_ApplyUserRole_Call) Return(user1 *userv1beta1.User, err error) *UserRoleAssigner_ApplyUserRole_Call {
|
||||
_c.Call.Return(user1, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserRoleAssigner_ApplyUserRole_Call) RunAndReturn(run func(ctx context.Context, user *userv1beta1.User) (*userv1beta1.User, error)) *UserRoleAssigner_ApplyUserRole_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateUserRoleAssignment provides a mock function for the type UserRoleAssigner
|
||||
func (_mock *UserRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *userv1beta1.User, claims map[string]any) (*userv1beta1.User, error) {
|
||||
ret := _mock.Called(ctx, user, claims)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateUserRoleAssignment")
|
||||
}
|
||||
|
||||
var r0 *userv1beta1.User
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *userv1beta1.User, map[string]any) (*userv1beta1.User, error)); ok {
|
||||
return returnFunc(ctx, user, claims)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *userv1beta1.User, map[string]any) *userv1beta1.User); ok {
|
||||
r0 = returnFunc(ctx, user, claims)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*userv1beta1.User)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, *userv1beta1.User, map[string]any) error); ok {
|
||||
r1 = returnFunc(ctx, user, claims)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UserRoleAssigner_UpdateUserRoleAssignment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUserRoleAssignment'
|
||||
type UserRoleAssigner_UpdateUserRoleAssignment_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateUserRoleAssignment is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - user *userv1beta1.User
|
||||
// - claims map[string]any
|
||||
func (_e *UserRoleAssigner_Expecter) UpdateUserRoleAssignment(ctx interface{}, user interface{}, claims interface{}) *UserRoleAssigner_UpdateUserRoleAssignment_Call {
|
||||
return &UserRoleAssigner_UpdateUserRoleAssignment_Call{Call: _e.mock.On("UpdateUserRoleAssignment", ctx, user, claims)}
|
||||
}
|
||||
|
||||
func (_c *UserRoleAssigner_UpdateUserRoleAssignment_Call) Run(run func(ctx context.Context, user *userv1beta1.User, claims map[string]any)) *UserRoleAssigner_UpdateUserRoleAssignment_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 *userv1beta1.User
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(*userv1beta1.User)
|
||||
}
|
||||
var arg2 map[string]any
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(map[string]any)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserRoleAssigner_UpdateUserRoleAssignment_Call) Return(user1 *userv1beta1.User, err error) *UserRoleAssigner_UpdateUserRoleAssignment_Call {
|
||||
_c.Call.Return(user1, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *UserRoleAssigner_UpdateUserRoleAssignment_Call) RunAndReturn(run func(ctx context.Context, user *userv1beta1.User, claims map[string]any) (*userv1beta1.User, error)) *UserRoleAssigner_UpdateUserRoleAssignment_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package userroles
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/qsfera/server/pkg/middleware"
|
||||
"github.com/qsfera/server/pkg/oidc"
|
||||
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"go-micro.dev/v4/metadata"
|
||||
)
|
||||
|
||||
type oidcRoleAssigner struct {
|
||||
Options
|
||||
}
|
||||
|
||||
// NewOIDCRoleAssigner returns an implementation of the UserRoleAssigner interface
|
||||
func NewOIDCRoleAssigner(opts ...Option) UserRoleAssigner {
|
||||
opt := Options{}
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return oidcRoleAssigner{
|
||||
Options: opt,
|
||||
}
|
||||
}
|
||||
|
||||
func extractRoles(rolesClaim string, claims map[string]any) (map[string]struct{}, error) {
|
||||
|
||||
claimRoles := map[string]struct{}{}
|
||||
// happy path
|
||||
value, _ := claims[rolesClaim].(string)
|
||||
if value != "" {
|
||||
claimRoles[value] = struct{}{}
|
||||
return claimRoles, nil
|
||||
}
|
||||
|
||||
claim, err := oidc.WalkSegments(oidc.SplitWithEscaping(rolesClaim, ".", "\\"), claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch v := claim.(type) {
|
||||
case []string:
|
||||
for _, cr := range v {
|
||||
claimRoles[cr] = struct{}{}
|
||||
}
|
||||
case []any:
|
||||
for _, cri := range v {
|
||||
cr, ok := cri.(string)
|
||||
if !ok {
|
||||
err := errors.New("invalid role in claims")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claimRoles[cr] = struct{}{}
|
||||
}
|
||||
case string:
|
||||
claimRoles[v] = struct{}{}
|
||||
default:
|
||||
return nil, errors.New("no roles in user claims")
|
||||
}
|
||||
|
||||
return claimRoles, nil
|
||||
}
|
||||
|
||||
// UpdateUserRoleAssignment assigns the role "User" to the supplied user. Unless the user
|
||||
// already has a different role assigned.
|
||||
func (ra oidcRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]any) (*cs3.User, error) {
|
||||
logger := ra.logger.SubloggerWithRequestID(ctx).With().Str("userid", user.GetId().GetOpaqueId()).Logger()
|
||||
roleNamesToRoleIDs, err := ra.roleNamesToRoleIDs()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error mapping role names to role ids")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claimRoles, err := extractRoles(ra.rolesClaim, claims)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error mapping role names to role ids")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(claimRoles) == 0 {
|
||||
err := errors.New("no roles set in claim")
|
||||
logger.Error().Err(err).Msg("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// the roleMapping config is supposed to have the role mappings ordered from the highest privileged role
|
||||
// down to the lowest privileged role. Since КуСфера 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("qsferaRole", 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 КуСфера role")
|
||||
logger.Error().Err(err).Msg("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assignedRoles, err := loadRolesIDs(ctx, user.GetId().GetOpaqueId(), ra.roleService)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Could not load roles")
|
||||
return nil, err
|
||||
}
|
||||
if len(assignedRoles) > 1 {
|
||||
logger.Error().Str("userID", user.GetId().GetOpaqueId()).Int("numRoles", len(assignedRoles)).Msg("The user has too many roles assigned")
|
||||
}
|
||||
logger.Debug().Interface("assignedRoleIds", assignedRoles).Msg("Currently assigned roles")
|
||||
|
||||
if len(assignedRoles) != 1 || (assignedRoles[0] != roleIDFromClaim) {
|
||||
logger.Debug().Interface("assignedRoleIds", assignedRoles).Interface("newRoleId", roleIDFromClaim).Msg("Updating role assignment for user")
|
||||
newctx, err := ra.prepareAdminContext()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error creating admin context")
|
||||
return nil, err
|
||||
}
|
||||
if _, err = ra.roleService.AssignRoleToUser(newctx, &settingssvc.AssignRoleToUserRequest{
|
||||
AccountUuid: user.GetId().GetOpaqueId(),
|
||||
RoleId: roleIDFromClaim,
|
||||
}); err != nil {
|
||||
logger.Error().Err(err).Msg("Role assignment failed")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", []string{roleIDFromClaim})
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ApplyUserRole it looks up the user's role in the settings service and adds it
|
||||
// user's opaque data
|
||||
func (ra oidcRoleAssigner) ApplyUserRole(ctx context.Context, user *cs3.User) (*cs3.User, error) {
|
||||
roleIDs, err := loadRolesIDs(ctx, user.Id.OpaqueId, ra.roleService)
|
||||
if err != nil {
|
||||
ra.logger.Error().Err(err).Msg("Could not load roles")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDs)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (ra oidcRoleAssigner) prepareAdminContext() (context.Context, error) {
|
||||
gatewayClient, err := ra.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
ra.logger.Error().Err(err).Msg("could not select next gateway client")
|
||||
return nil, err
|
||||
}
|
||||
newctx, err := utils.GetServiceUserContext(ra.serviceAccount.ServiceAccountID, gatewayClient, ra.serviceAccount.ServiceAccountSecret)
|
||||
if err != nil {
|
||||
ra.logger.Error().Err(err).Msg("Error preparing request context for provisioning role assignments.")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newctx = metadata.Set(newctx, middleware.AccountID, ra.serviceAccount.ServiceAccountID)
|
||||
return newctx, nil
|
||||
}
|
||||
|
||||
type roleNameToIDCache struct {
|
||||
roleNameToID map[string]string
|
||||
lastRead time.Time
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
var roleNameToID roleNameToIDCache
|
||||
|
||||
func (ra oidcRoleAssigner) roleNamesToRoleIDs() (map[string]string, error) {
|
||||
cacheTTL := 5 * time.Minute
|
||||
roleNameToID.lock.RLock()
|
||||
|
||||
if !roleNameToID.lastRead.IsZero() && time.Since(roleNameToID.lastRead) < cacheTTL {
|
||||
defer roleNameToID.lock.RUnlock()
|
||||
return roleNameToID.roleNameToID, nil
|
||||
}
|
||||
ra.logger.Debug().Msg("refreshing roles ids")
|
||||
|
||||
// cache needs Refresh get a write lock
|
||||
roleNameToID.lock.RUnlock()
|
||||
roleNameToID.lock.Lock()
|
||||
defer roleNameToID.lock.Unlock()
|
||||
|
||||
// check again, another goroutine might have updated while we "upgraded" the lock
|
||||
if !roleNameToID.lastRead.IsZero() && time.Since(roleNameToID.lastRead) < cacheTTL {
|
||||
return roleNameToID.roleNameToID, nil
|
||||
}
|
||||
|
||||
// Get all roles to find the role IDs.
|
||||
// To list roles we need some elevated access to the settings service
|
||||
// prepare a new request context for that until we have service accounts
|
||||
ctx, err := ra.prepareAdminContext()
|
||||
if err != nil {
|
||||
ra.logger.Error().Err(err).Msg("Error creating admin context")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &settingssvc.ListBundlesRequest{}
|
||||
res, err := ra.roleService.ListRoles(ctx, req)
|
||||
if err != nil {
|
||||
ra.logger.Error().Err(err).Msg("Failed to list all roles")
|
||||
return map[string]string{}, err
|
||||
}
|
||||
|
||||
newIDs := map[string]string{}
|
||||
for _, role := range res.Bundles {
|
||||
ra.logger.Debug().Str("role", role.Name).Str("id", role.Id).Msg("Got Role")
|
||||
newIDs[role.Name] = role.Id
|
||||
}
|
||||
ra.logger.Debug().Interface("roleMap", newIDs).Msg("Role Name to role ID map")
|
||||
roleNameToID.roleNameToID = newIDs
|
||||
roleNameToID.lastRead = time.Now()
|
||||
return roleNameToID.roleNameToID, nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package userroles
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractRolesArray(t *testing.T) {
|
||||
byt := []byte(`{"roles":["a","b"]}`)
|
||||
|
||||
claims := map[string]any{}
|
||||
err := json.Unmarshal(byt, &claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
roles, err := extractRoles("roles", claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := roles["a"]; !ok {
|
||||
t.Fatal("must contain 'a'")
|
||||
}
|
||||
if _, ok := roles["b"]; !ok {
|
||||
t.Fatal("must contain 'b'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRolesString(t *testing.T) {
|
||||
byt := []byte(`{"roles":"a"}`)
|
||||
|
||||
claims := map[string]any{}
|
||||
err := json.Unmarshal(byt, &claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
roles, err := extractRoles("roles", claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := roles["a"]; !ok {
|
||||
t.Fatal("must contain 'a'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRolesPathArray(t *testing.T) {
|
||||
byt := []byte(`{"sub":{"roles":["a","b"]}}`)
|
||||
|
||||
claims := map[string]any{}
|
||||
err := json.Unmarshal(byt, &claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
roles, err := extractRoles("sub.roles", claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := roles["a"]; !ok {
|
||||
t.Fatal("must contain 'a'")
|
||||
}
|
||||
if _, ok := roles["b"]; !ok {
|
||||
t.Fatal("must contain 'b'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRolesPathString(t *testing.T) {
|
||||
byt := []byte(`{"sub":{"roles":"a"}}`)
|
||||
|
||||
claims := map[string]any{}
|
||||
err := json.Unmarshal(byt, &claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
roles, err := extractRoles("sub.roles", claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := roles["a"]; !ok {
|
||||
t.Fatal("must contain 'a'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEscapedRolesPathString(t *testing.T) {
|
||||
byt := []byte(`{"sub.roles":"a"}`)
|
||||
|
||||
claims := map[string]any{}
|
||||
err := json.Unmarshal(byt, &claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
roles, err := extractRoles("sub\\.roles", claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := roles["a"]; !ok {
|
||||
t.Fatal("must contain 'a'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoRoles(t *testing.T) {
|
||||
byt := []byte(`{"sub":{"foo":"a"}}`)
|
||||
|
||||
claims := map[string]any{}
|
||||
err := json.Unmarshal(byt, &claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
roles, err := extractRoles("sub.roles", claims)
|
||||
if err == nil {
|
||||
t.Fatal("must not find a role")
|
||||
}
|
||||
if len(roles) != 0 {
|
||||
t.Fatal("length of roles mut be 0")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package userroles
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
|
||||
"github.com/qsfera/server/services/proxy/pkg/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
)
|
||||
|
||||
// UserRoleAssigner allows providing different implementations for how users get their default roles
|
||||
// assigned by the proxy during authentication
|
||||
type UserRoleAssigner interface {
|
||||
// UpdateUserRoleAssignment is called by the account resolver middleware. It updates the user's role assignment
|
||||
// based on the user's (OIDC) claims. It adds the user's roles to the opaque data of the cs3.User struct
|
||||
UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]any) (*cs3.User, error)
|
||||
// ApplyUserRole can be called by proxy middlewares, it looks up the user's roles and adds them
|
||||
// the users "roles" key in the user's opaque data
|
||||
ApplyUserRole(ctx context.Context, user *cs3.User) (*cs3.User, error)
|
||||
}
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
roleService settingssvc.RoleService
|
||||
rolesClaim string
|
||||
roleMapping []config.RoleMapping
|
||||
serviceAccount config.ServiceAccount
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// WithLogger configure the logger
|
||||
func WithLogger(l log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// WithRoleService sets the roleservice instance to use
|
||||
func WithRoleService(rs settingssvc.RoleService) Option {
|
||||
return func(o *Options) {
|
||||
o.roleService = rs
|
||||
}
|
||||
}
|
||||
|
||||
// WithRolesClaim sets the OIDC claim for looking up role names
|
||||
func WithRolesClaim(claim string) Option {
|
||||
return func(o *Options) {
|
||||
o.rolesClaim = claim
|
||||
}
|
||||
}
|
||||
|
||||
// WithRoleMapping configures the map of КуСфера role names to claims values
|
||||
func WithRoleMapping(roleMap []config.RoleMapping) Option {
|
||||
return func(o *Options) {
|
||||
o.roleMapping = roleMap
|
||||
}
|
||||
}
|
||||
|
||||
// WithRevaGatewaySelector set the gatewaySelector option
|
||||
func WithRevaGatewaySelector(selectable pool.Selectable[gateway.GatewayAPIClient]) Option {
|
||||
return func(o *Options) {
|
||||
o.gatewaySelector = selectable
|
||||
}
|
||||
}
|
||||
|
||||
// WithServiceAccount configures the service account creator to use
|
||||
func WithServiceAccount(c config.ServiceAccount) Option {
|
||||
return func(o *Options) {
|
||||
o.serviceAccount = c
|
||||
}
|
||||
}
|
||||
|
||||
// loadRolesIDs returns the role-ids assigned to an user
|
||||
func loadRolesIDs(ctx context.Context, opaqueUserID string, rs settingssvc.RoleService) ([]string, error) {
|
||||
req := &settingssvc.ListRoleAssignmentsRequest{AccountUuid: opaqueUserID}
|
||||
assignmentResponse, err := rs.ListRoleAssignments(ctx, req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleIDs := make([]string, 0)
|
||||
|
||||
for _, assignment := range assignmentResponse.Assignments {
|
||||
roleIDs = append(roleIDs, assignment.RoleId)
|
||||
}
|
||||
|
||||
return roleIDs, nil
|
||||
}
|
||||
Reference in New Issue
Block a user