diff --git a/extensions/ocs/pkg/service/v0/service.go b/extensions/ocs/pkg/service/v0/service.go index 4f552b21aff..1abae288d9b 100644 --- a/extensions/ocs/pkg/service/v0/service.go +++ b/extensions/ocs/pkg/service/v0/service.go @@ -118,7 +118,7 @@ func (o Ocs) getCS3Backend() backend.UserBackend { if err != nil { o.logger.Fatal().Msgf("could not get reva client at address %s", o.config.Reva.Address) } - return backend.NewCS3UserBackend(nil, revaClient, o.config.MachineAuthAPIKey, o.logger) + return backend.NewCS3UserBackend(nil, revaClient, o.config.MachineAuthAPIKey, "", nil, o.logger) } // NotImplementedStub returns a not implemented error diff --git a/extensions/proxy/pkg/command/server.go b/extensions/proxy/pkg/command/server.go index 686ce08e205..072a8952bee 100644 --- a/extensions/proxy/pkg/command/server.go +++ b/extensions/proxy/pkg/command/server.go @@ -8,9 +8,8 @@ import ( "os" "time" - storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0" - "github.com/coreos/go-oidc/v3/oidc" + "github.com/cs3org/reva/v2/pkg/token/manager/jwt" chimiddleware "github.com/go-chi/chi/v5/middleware" "github.com/justinas/alice" "github.com/oklog/run" @@ -30,6 +29,7 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" "github.com/owncloud/ocis/v2/ocis-pkg/version" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0" "github.com/urfave/cli/v2" "golang.org/x/oauth2" ) @@ -135,7 +135,15 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) var userProvider backend.UserBackend switch cfg.AccountBackend { case "cs3": - userProvider = backend.NewCS3UserBackend(rolesClient, revaClient, cfg.MachineAuthAPIKey, logger) + tokenManager, err := jwt.New(map[string]interface{}{ + "secret": cfg.TokenManager.JWTSecret, + }) + if err != nil { + logger.Error().Err(err). + Msg("Failed to create token manager") + } + + userProvider = backend.NewCS3UserBackend(rolesClient, revaClient, cfg.MachineAuthAPIKey, cfg.OIDC.Issuer, tokenManager, logger) default: logger.Fatal().Msgf("Invalid accounts backend type '%s'", cfg.AccountBackend) } diff --git a/extensions/proxy/pkg/config/config.go b/extensions/proxy/pkg/config/config.go index 0025b338795..6e4ad687c48 100644 --- a/extensions/proxy/pkg/config/config.go +++ b/extensions/proxy/pkg/config/config.go @@ -25,13 +25,13 @@ type Config struct { TokenManager *TokenManager `yaml:"token_manager"` PolicySelector *PolicySelector `yaml:"policy_selector"` PreSignedURL PreSignedURL `yaml:"pre_signed_url"` - AccountBackend string `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE"` - UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM"` - UserCS3Claim string `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM"` - MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;PROXY_MACHINE_AUTH_API_KEY"` - AutoprovisionAccounts bool `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS"` - EnableBasicAuth bool `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH"` - InsecureBackends bool `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS"` + AccountBackend string `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the proxy should use, currenly only 'cs3' is possible here."` + UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that should be used for resolving users with the account backend. Currently defaults to 'email'."` + UserCS3Claim string `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Currently defaults to 'mail' (other possible values are: 'username', 'displayname')"` + MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;PROXY_MACHINE_AUTH_API_KEY" desc: "Machine auth API key used for accessing the 'auth-machine' service."` + AutoprovisionAccounts bool `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provsion users that do not yet exist in the users service on-demand upon first signin. To use this a write-enabled libregraph user backend needs to be setup an running."` + EnableBasicAuth bool `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH" desc:"Set this to true to enable 'basic' (username/password) authentication. (Default: false)"` + InsecureBackends bool `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS" desc:"Disable TLS certificate validation for all http backend connections. (Default: false)"` AuthMiddleware AuthMiddleware `yaml:"auth_middleware"` Context context.Context `yaml:"-"` @@ -83,8 +83,8 @@ type AuthMiddleware struct { // OIDC is the config for the OpenID-Connect middleware. If set the proxy will try to authenticate every request // with the configured oidc-provider type OIDC struct { - Issuer string `yaml:"issuer" env:"OCIS_URL;OCIS_OIDC_ISSUER;PROXY_OIDC_ISSUER"` - Insecure bool `yaml:"insecure" env:"OCIS_INSECURE;PROXY_OIDC_INSECURE"` + Issuer string `yaml:"issuer" env:"OCIS_URL;OCIS_OIDC_ISSUER;PROXY_OIDC_ISSUER" desc:"URL of the OpenID connect identity provider."` + Insecure bool `yaml:"insecure" env:"OCIS_INSECURE;PROXY_OIDC_INSECURE" desc:"Disable TLS certificate validation for connections to the IDP. (not recommended for production environments."` UserinfoCache UserinfoCache `yaml:"user_info_cache"` } diff --git a/extensions/proxy/pkg/middleware/account_resolver.go b/extensions/proxy/pkg/middleware/account_resolver.go index 8de2c37a289..451eeee7a1a 100644 --- a/extensions/proxy/pkg/middleware/account_resolver.go +++ b/extensions/proxy/pkg/middleware/account_resolver.go @@ -73,8 +73,9 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) { } m.logger.Debug().Interface("claims", claims).Msg("Autoprovisioning user") user, err = m.userProvider.CreateUserFromClaims(req.Context(), claims) - // TODO instead of creating an account create a personal storage via the CS3 admin api? - // see https://cs3org.github.io/cs3apis/#cs3.admin.user.v1beta1.CreateUserRequest + if err != nil { + m.logger.Error().Err(err).Msg("Autoprovisioning user failed") + } } if errors.Is(err, backend.ErrAccountDisabled) { diff --git a/extensions/proxy/pkg/user/backend/cs3.go b/extensions/proxy/pkg/user/backend/cs3.go index 308283cac4b..c4287464085 100644 --- a/extensions/proxy/pkg/user/backend/cs3.go +++ b/extensions/proxy/pkg/user/backend/cs3.go @@ -2,30 +2,49 @@ package backend import ( "context" + "encoding/json" "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/extensions/graph/pkg/service/v0/errorcode" settingsService "github.com/owncloud/ocis/v2/extensions/settings/pkg/service/v0" "github.com/owncloud/ocis/v2/ocis-pkg/log" + "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" + "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, logger log.Logger) 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, } } @@ -72,7 +91,7 @@ func (c *cs3backend) GetUserByClaims(ctx context.Context, claim, value string, w RoleId: settingsService.BundleUUIDRoleUser, }) if err != nil { - c.logger.Error().Err(err).Msg("Could not add default role") + c.logger.Warn().Err(err).Msg("Could not add default role") } roleIDs = append(roleIDs, settingsService.BundleUUIDRoleUser) } @@ -114,9 +133,190 @@ func (c *cs3backend) Authenticate(ctx context.Context, username string, password } func (c *cs3backend) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error) { - return nil, fmt.Errorf("CS3 Backend does not support creating users from claims") + 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 +} + +var autoProvisionUserCreator *cs3.User + +// This returns an hardcoded internal User, that is privileged to create new User via +// the Graph API. This user is needed of autoprovisioning of user from incoming OIDC +// claims. +func getAutoProvisionUserCreator() (*cs3.User, error) { + if autoProvisionUserCreator == nil { + 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 +}