diff --git a/changelog/unreleased/autoprovision-update-user.md b/changelog/unreleased/autoprovision-update-user.md new file mode 100644 index 000000000..fd8ef76fd --- /dev/null +++ b/changelog/unreleased/autoprovision-update-user.md @@ -0,0 +1,7 @@ +Enhancement: Update selected attributes of autoprovisioned users + +When autoprovisioning is enabled, we now update autoprovisioned users when +their display name or email address claims change. + +https://github.com/owncloud/ocis/pull/9166 +https://github.com/owncloud/ocis/issues/8955 diff --git a/services/proxy/pkg/middleware/account_resolver.go b/services/proxy/pkg/middleware/account_resolver.go index 79fd1ff8b..868e13288 100644 --- a/services/proxy/pkg/middleware/account_resolver.go +++ b/services/proxy/pkg/middleware/account_resolver.go @@ -134,6 +134,14 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } + if m.autoProvisionAccounts { + if err = m.userProvider.UpdateUserIfNeeded(req.Context(), user, claims); err != nil { + m.logger.Error().Err(err).Str("userid", user.GetId().GetOpaqueId()).Interface("claims", claims).Msg("Failed to update autoprovisioned user") + w.WriteHeader(http.StatusInternalServerError) + return + } + } + // resolve the user's roles user, err = m.userRoleAssigner.UpdateUserRoleAssignment(ctx, user, claims) if err != nil { diff --git a/services/proxy/pkg/user/backend/backend.go b/services/proxy/pkg/user/backend/backend.go index cc9c86a8a..229ef7676 100644 --- a/services/proxy/pkg/user/backend/backend.go +++ b/services/proxy/pkg/user/backend/backend.go @@ -21,4 +21,5 @@ type UserBackend interface { GetUserByClaims(ctx context.Context, claim, value string) (*cs3.User, string, error) Authenticate(ctx context.Context, username string, password string) (*cs3.User, string, error) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error) + UpdateUserIfNeeded(ctx context.Context, user *cs3.User, claims map[string]interface{}) error } diff --git a/services/proxy/pkg/user/backend/cs3.go b/services/proxy/pkg/user/backend/cs3.go index 2646c7d35..38c3b76cc 100644 --- a/services/proxy/pkg/user/backend/cs3.go +++ b/services/proxy/pkg/user/backend/cs3.go @@ -224,6 +224,62 @@ func (c *cs3backend) CreateUserFromClaims(ctx context.Context, claims map[string return &cs3UserCreated, nil } +func (c cs3backend) UpdateUserIfNeeded(ctx context.Context, user *cs3.User, claims map[string]interface{}) error { + newUser, err := c.libregraphUserFromClaims(claims) + if err != nil { + c.logger.Error().Err(err).Interface("claims", claims).Msg("Error converting claims to user") + return fmt.Errorf("error converting claims to updated user: %w", err) + } + + // Check if the user needs to be updated, only updates of "displayName" and "mail" are supported + // currently. + switch { + case newUser.GetDisplayName() != user.GetDisplayName(): + fallthrough + case newUser.GetMail() != user.GetMail(): + return c.updateLibregraphUser(user.GetId().GetOpaqueId(), newUser) + } + + return nil +} + +func (c cs3backend) updateLibregraphUser(userid string, user libregraph.User) error { + gatewayClient, err := c.gatewaySelector.Next() + if err != nil { + c.logger.Error().Err(err).Msg("could not select next gateway client") + return err + } + newctx := context.Background() + authRes, err := gatewayClient.Authenticate(newctx, &gateway.AuthenticateRequest{ + Type: "serviceaccounts", + ClientId: c.serviceAccount.ServiceAccountID, + ClientSecret: c.serviceAccount.ServiceAccountSecret, + }) + if err != nil { + return err + } + if authRes.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK { + return fmt.Errorf("error authenticating service user: %s", authRes.GetStatus().GetMessage()) + } + + lgClient, err := c.setupLibregraphClient(newctx, authRes.GetToken()) + if err != nil { + c.logger.Error().Err(err).Msg("Error setting up libregraph client") + return err + } + + req := lgClient.UserApi.UpdateUser(newctx, userid).User(user) + + _, resp, err := req.Execute() + defer resp.Body.Close() + if err != nil { + c.logger.Error().Err(err).Msg("Failed to update user via libregraph") + return err + } + + return nil +} + func (c cs3backend) setupLibregraphClient(ctx context.Context, cs3token string) (*libregraph.APIClient, error) { // Use micro registry to resolve next graph service endpoint next, err := c.graphSelector.Select("com.owncloud.graph.graph") diff --git a/services/proxy/pkg/user/backend/mocks/user_backend.go b/services/proxy/pkg/user/backend/mocks/user_backend.go index 2d57b3401..a44198f61 100644 --- a/services/proxy/pkg/user/backend/mocks/user_backend.go +++ b/services/proxy/pkg/user/backend/mocks/user_backend.go @@ -215,6 +215,54 @@ func (_c *UserBackend_GetUserByClaims_Call) RunAndReturn(run func(context.Contex return _c } +// UpdateUserIfNeeded provides a mock function with given fields: ctx, user, claims +func (_m *UserBackend) UpdateUserIfNeeded(ctx context.Context, user *userv1beta1.User, claims map[string]interface{}) error { + ret := _m.Called(ctx, user, claims) + + if len(ret) == 0 { + panic("no return value specified for UpdateUserIfNeeded") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *userv1beta1.User, map[string]interface{}) error); ok { + r0 = rf(ctx, user, claims) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UserBackend_UpdateUserIfNeeded_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUserIfNeeded' +type UserBackend_UpdateUserIfNeeded_Call struct { + *mock.Call +} + +// UpdateUserIfNeeded is a helper method to define mock.On call +// - ctx context.Context +// - user *userv1beta1.User +// - claims map[string]interface{} +func (_e *UserBackend_Expecter) UpdateUserIfNeeded(ctx interface{}, user interface{}, claims interface{}) *UserBackend_UpdateUserIfNeeded_Call { + return &UserBackend_UpdateUserIfNeeded_Call{Call: _e.mock.On("UpdateUserIfNeeded", ctx, user, claims)} +} + +func (_c *UserBackend_UpdateUserIfNeeded_Call) Run(run func(ctx context.Context, user *userv1beta1.User, claims map[string]interface{})) *UserBackend_UpdateUserIfNeeded_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*userv1beta1.User), args[2].(map[string]interface{})) + }) + return _c +} + +func (_c *UserBackend_UpdateUserIfNeeded_Call) Return(_a0 error) *UserBackend_UpdateUserIfNeeded_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UserBackend_UpdateUserIfNeeded_Call) RunAndReturn(run func(context.Context, *userv1beta1.User, map[string]interface{}) error) *UserBackend_UpdateUserIfNeeded_Call { + _c.Call.Return(run) + return _c +} + // NewUserBackend creates a new instance of UserBackend. 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 NewUserBackend(t interface {