mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-05 19:59:37 -06:00
When using an external user management we need to allow users to self-assign the default role. This adds an explicit check for that to the settings service. This also means we no longer need to fiddle with the account id in the proxy upon first login. Fixes: #5045
331 lines
10 KiB
Go
331 lines
10 KiB
Go
package backend
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
|
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
|
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
|
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
|
"github.com/cs3org/reva/v2/pkg/auth/scope"
|
|
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
|
"github.com/cs3org/reva/v2/pkg/token"
|
|
libregraph "github.com/owncloud/libre-graph-api-go"
|
|
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
|
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
|
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
|
|
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
|
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
|
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
|
|
settingsService "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
|
|
merrors "go-micro.dev/v4/errors"
|
|
"go-micro.dev/v4/metadata"
|
|
"go-micro.dev/v4/selector"
|
|
)
|
|
|
|
type cs3backend struct {
|
|
graphSelector selector.Selector
|
|
settingsRoleService settingssvc.RoleService
|
|
authProvider RevaAuthenticator
|
|
oidcISS string
|
|
machineAuthAPIKey string
|
|
tokenManager token.Manager
|
|
logger log.Logger
|
|
}
|
|
|
|
// NewCS3UserBackend creates a user-provider which fetches users from a CS3 UserBackend
|
|
func NewCS3UserBackend(rs settingssvc.RoleService, ap RevaAuthenticator, machineAuthAPIKey string, oidcISS string, tokenManager token.Manager, logger log.Logger) UserBackend {
|
|
reg := registry.GetRegistry()
|
|
sel := selector.NewSelector(selector.Registry(reg))
|
|
return &cs3backend{
|
|
graphSelector: sel,
|
|
settingsRoleService: rs,
|
|
authProvider: ap,
|
|
oidcISS: oidcISS,
|
|
machineAuthAPIKey: machineAuthAPIKey,
|
|
tokenManager: tokenManager,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func (c *cs3backend) GetUserByClaims(ctx context.Context, claim, value string, withRoles bool) (*cs3.User, string, error) {
|
|
res, err := c.authProvider.Authenticate(ctx, &gateway.AuthenticateRequest{
|
|
Type: "machine",
|
|
ClientId: claim + ":" + value,
|
|
ClientSecret: c.machineAuthAPIKey,
|
|
})
|
|
|
|
switch {
|
|
case err != nil:
|
|
return nil, "", fmt.Errorf("could not get user by claim %v with value %v: %w", claim, value, err)
|
|
case res.Status.Code != rpcv1beta1.Code_CODE_OK:
|
|
if res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND {
|
|
return nil, "", ErrAccountNotFound
|
|
}
|
|
return nil, "", fmt.Errorf("could not get user by claim %v with value %v : %s ", claim, value, res.Status.Message)
|
|
}
|
|
|
|
user := res.User
|
|
|
|
if !withRoles {
|
|
return user, res.Token, nil
|
|
}
|
|
|
|
var roleIDs []string
|
|
if user.Id.Type != cs3.UserType_USER_TYPE_LIGHTWEIGHT {
|
|
roleIDs, err = loadRolesIDs(ctx, user.Id.OpaqueId, c.settingsRoleService)
|
|
if err != nil {
|
|
var merr *merrors.Error
|
|
if errors.As(err, &merr) && merr.Code == http.StatusNotFound {
|
|
// 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/v2/issues/1825 for more context.
|
|
if user.Id.Type == cs3.UserType_USER_TYPE_PRIMARY {
|
|
c.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 := c.settingsRoleService.AssignRoleToUser(ctx, &settingssvc.AssignRoleToUserRequest{
|
|
AccountUuid: user.Id.OpaqueId,
|
|
RoleId: settingsService.BundleUUIDRoleUser,
|
|
})
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Msg("Could not add default role")
|
|
return nil, "", err
|
|
}
|
|
roleIDs = append(roleIDs, settingsService.BundleUUIDRoleUser)
|
|
}
|
|
} else {
|
|
c.logger.Error().Err(err).Msgf("Could not load roles")
|
|
return nil, "", err
|
|
}
|
|
}
|
|
}
|
|
|
|
enc, err := encodeRoleIDs(roleIDs)
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Msg("Could not encode loaded roles")
|
|
}
|
|
|
|
if user.Opaque == nil {
|
|
user.Opaque = &types.Opaque{
|
|
Map: map[string]*types.OpaqueEntry{
|
|
"roles": enc,
|
|
},
|
|
}
|
|
} else {
|
|
user.Opaque.Map["roles"] = enc
|
|
}
|
|
|
|
return user, res.Token, nil
|
|
}
|
|
|
|
func (c *cs3backend) Authenticate(ctx context.Context, username string, password string) (*cs3.User, string, error) {
|
|
res, err := c.authProvider.Authenticate(ctx, &gateway.AuthenticateRequest{
|
|
Type: "basic",
|
|
ClientId: username,
|
|
ClientSecret: password,
|
|
})
|
|
|
|
switch {
|
|
case err != nil:
|
|
return nil, "", fmt.Errorf("could not authenticate with username and password user: %s, %w", username, err)
|
|
case res.Status.Code != rpcv1beta1.Code_CODE_OK:
|
|
return nil, "", fmt.Errorf("could not authenticate with username and password user: %s, got code: %d", username, res.Status.Code)
|
|
}
|
|
|
|
return res.User, res.Token, 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
|
|
// function will just return the existing user.
|
|
func (c *cs3backend) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error) {
|
|
newctx := context.Background()
|
|
token, err := c.generateAutoProvisionAdminToken(newctx)
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Msg("Error generating token for autoprovisioning user.")
|
|
return nil, err
|
|
}
|
|
lgClient, err := c.setupLibregraphClient(ctx, token)
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Msg("Error setting up libregraph client.")
|
|
return nil, err
|
|
}
|
|
|
|
newUser, err := c.libregraphUserFromClaims(newctx, claims)
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Interface("claims", claims).Msg("Error creating user from claims")
|
|
return nil, fmt.Errorf("Error creating user from claims: %w", err)
|
|
}
|
|
|
|
req := lgClient.UsersApi.CreateUser(newctx).User(newUser)
|
|
|
|
created, resp, err := req.Execute()
|
|
var reread bool
|
|
if err != nil {
|
|
if resp == nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the user already exists here, some other request did already create it in parallel.
|
|
// So just issue a Debug message and ignore the libregraph error otherwise
|
|
var lerr error
|
|
if reread, lerr = c.isAlreadyExists(resp); lerr != nil {
|
|
c.logger.Error().Err(lerr).Msg("extracting error from ibregraph response body failed.")
|
|
return nil, err
|
|
}
|
|
if !reread {
|
|
c.logger.Error().Err(err).Msg("Error creating user")
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// User has been created meanwhile, re-read it to get the user id
|
|
if reread {
|
|
c.logger.Debug().Msg("User already exist, re-reading via libregraph")
|
|
gureq := lgClient.UserApi.GetUser(newctx, newUser.GetOnPremisesSamAccountName())
|
|
created, resp, err = gureq.Execute()
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Msg("Error trying to re-read user from graphAPI")
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
cs3UserCreated := c.cs3UserFromLibregraph(newctx, created)
|
|
|
|
return &cs3UserCreated, nil
|
|
}
|
|
|
|
func (c cs3backend) GetUserGroups(ctx context.Context, userID string) {
|
|
panic("implement me")
|
|
}
|
|
|
|
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")
|
|
if err != nil {
|
|
c.logger.Debug().Err(err).Msg("setupLibregraphClient: error during Select")
|
|
return nil, err
|
|
}
|
|
node, err := next()
|
|
if err != nil {
|
|
c.logger.Debug().Err(err).Msg("setupLibregraphClient: error getting next Node")
|
|
return nil, err
|
|
}
|
|
lgconf := libregraph.NewConfiguration()
|
|
lgconf.Servers = libregraph.ServerConfigurations{
|
|
{
|
|
URL: fmt.Sprintf("%s://%s/graph/v1.0", node.Metadata["protocol"], node.Address),
|
|
},
|
|
}
|
|
|
|
lgconf.DefaultHeader = map[string]string{revactx.TokenHeader: cs3token}
|
|
return libregraph.NewAPIClient(lgconf), nil
|
|
}
|
|
|
|
func (c cs3backend) isAlreadyExists(resp *http.Response) (bool, error) {
|
|
oDataErr := libregraph.NewOdataErrorWithDefaults()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
c.logger.Debug().Err(err).Msg("Error trying to read libregraph response")
|
|
return false, err
|
|
}
|
|
err = json.Unmarshal(body, oDataErr)
|
|
if err != nil {
|
|
c.logger.Debug().Err(err).Msg("Error unmarshalling libregraph response")
|
|
return false, err
|
|
}
|
|
|
|
if oDataErr.Error.Code == errorcode.NameAlreadyExists.String() {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (c cs3backend) libregraphUserFromClaims(ctx context.Context, claims map[string]interface{}) (libregraph.User, error) {
|
|
var ok bool
|
|
var dn, mail, username string
|
|
user := libregraph.User{}
|
|
if dn, ok = claims[oidc.Name].(string); !ok {
|
|
return user, fmt.Errorf("Missing claim '%s'", oidc.Name)
|
|
}
|
|
if mail, ok = claims[oidc.Email].(string); !ok {
|
|
return user, fmt.Errorf("Missing claim '%s'", oidc.Email)
|
|
}
|
|
if username, ok = claims[oidc.PreferredUsername].(string); !ok {
|
|
c.logger.Warn().Str("claim", oidc.PreferredUsername).Msg("Missing claim for username, falling back to email address")
|
|
username = mail
|
|
}
|
|
user.DisplayName = &dn
|
|
user.OnPremisesSamAccountName = &username
|
|
user.Mail = &mail
|
|
return user, nil
|
|
}
|
|
|
|
func (c cs3backend) cs3UserFromLibregraph(ctx context.Context, lu *libregraph.User) cs3.User {
|
|
cs3id := cs3.UserId{
|
|
Type: cs3.UserType_USER_TYPE_PRIMARY,
|
|
Idp: c.oidcISS,
|
|
}
|
|
|
|
cs3id.OpaqueId = lu.GetId()
|
|
|
|
cs3user := cs3.User{
|
|
Id: &cs3id,
|
|
}
|
|
cs3user.Username = lu.GetOnPremisesSamAccountName()
|
|
cs3user.DisplayName = lu.GetDisplayName()
|
|
cs3user.Mail = lu.GetMail()
|
|
return cs3user
|
|
}
|
|
|
|
// This returns an hardcoded internal User, that is privileged to create new User via
|
|
// the Graph API. This user is needed for autoprovisioning of users from incoming OIDC
|
|
// claims.
|
|
func getAutoProvisionUserCreator() (*cs3.User, error) {
|
|
encRoleID, err := encodeRoleIDs([]string{settingsService.BundleUUIDRoleAdmin})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
autoProvisionUserCreator := &cs3.User{
|
|
DisplayName: "Autoprovision User",
|
|
Username: "autoprovisioner",
|
|
Id: &cs3.UserId{
|
|
Idp: "internal",
|
|
OpaqueId: "autoprov-user-id00-0000-000000000000",
|
|
},
|
|
Opaque: &types.Opaque{
|
|
Map: map[string]*types.OpaqueEntry{
|
|
"roles": encRoleID,
|
|
},
|
|
},
|
|
}
|
|
return autoProvisionUserCreator, nil
|
|
}
|
|
|
|
func (c cs3backend) generateAutoProvisionAdminToken(ctx context.Context) (string, error) {
|
|
userCreator, err := getAutoProvisionUserCreator()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
s, err := scope.AddOwnerScope(nil)
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Msg("could not get owner scope")
|
|
return "", err
|
|
}
|
|
|
|
token, err := c.tokenManager.MintToken(ctx, userCreator, s)
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Msg("could not mint token")
|
|
return "", err
|
|
}
|
|
return token, nil
|
|
}
|