feat(proxy): Update selected attributes of autoprovisioned users

When autoprovisioning is enabled, we now update autoprovisioned users when their
display name or email address claims change.

Closes: #8955
This commit is contained in:
Ralf Haferkamp
2024-05-14 15:26:09 +02:00
committed by Ralf Haferkamp
parent cd7b15b250
commit 7ca8391ce2
5 changed files with 120 additions and 0 deletions

View File

@@ -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 {

View File

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

View File

@@ -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")

View File

@@ -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 {