From 0a6b2ab449560f7aa6105f782248cb029a34bc2d Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Fri, 20 May 2022 13:06:16 +0200 Subject: [PATCH 1/8] fix env separator in config struct annotation --- extensions/idp/pkg/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/idp/pkg/config/config.go b/extensions/idp/pkg/config/config.go index 15a8b67ab4c..ed1fb00124f 100644 --- a/extensions/idp/pkg/config/config.go +++ b/extensions/idp/pkg/config/config.go @@ -37,7 +37,7 @@ type Ldap struct { BindDN string `yaml:"bind_dn" env:"LDAP_BIND_DN;IDP_LDAP_BIND_DN"` BindPassword string `yaml:"bind_password" env:"LDAP_BIND_PASSWORD;IDP_LDAP_BIND_PASSWORD"` - BaseDN string `yaml:"base_dn" env:"LDAP_USER_BASE_DN,IDP_LDAP_BASE_DN"` + BaseDN string `yaml:"base_dn" env:"LDAP_USER_BASE_DN;IDP_LDAP_BASE_DN"` Scope string `yaml:"scope" env:"LDAP_USER_SCOPE;IDP_LDAP_SCOPE"` LoginAttribute string `yaml:"login_attribute" env:"IDP_LDAP_LOGIN_ATTRIBUTE"` From 3bc18b17522f8f372afedee4ae7e865053577d64 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Fri, 20 May 2022 13:09:34 +0200 Subject: [PATCH 2/8] Let graph auth middleware add the roleids to the context They were also added by the ExtractAccountUUID for the /drives endpoint. We'll need some on other endpoints as well (for automatic user provisioning). --- extensions/graph/pkg/middleware/auth.go | 10 ++++++++++ extensions/graph/pkg/service/v0/service.go | 22 +++++++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/extensions/graph/pkg/middleware/auth.go b/extensions/graph/pkg/middleware/auth.go index a460a49823c..037aa0bc742 100644 --- a/extensions/graph/pkg/middleware/auth.go +++ b/extensions/graph/pkg/middleware/auth.go @@ -8,6 +8,8 @@ import ( "github.com/cs3org/reva/v2/pkg/token/manager/jwt" "github.com/owncloud/ocis/v2/extensions/graph/pkg/service/v0/errorcode" "github.com/owncloud/ocis/v2/ocis-pkg/account" + opkgm "github.com/owncloud/ocis/v2/ocis-pkg/middleware" + gmmetadata "go-micro.dev/v4/metadata" "google.golang.org/grpc/metadata" ) @@ -25,6 +27,8 @@ func authOptions(opts ...account.Option) account.Options { // Auth provides a middleware to authenticate requests using the x-access-token header value // and write it to the context. If there is no x-access-token the middleware prevents access and renders a json document. func Auth(opts ...account.Option) func(http.Handler) http.Handler { + // Note: This largely duplicates was ocis-pkg/middleware/account.go already does (apart from a slightly different error + // handling). Ideally we should merge both middlewares. opt := authOptions(opts...) tokenManager, err := jwt.New(map[string]interface{}{ "secret": opt.JWTSecret, @@ -69,6 +73,12 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler { ctx = revactx.ContextSetToken(ctx, t) ctx = revactx.ContextSetUser(ctx, u) + ctx = gmmetadata.Set(ctx, opkgm.AccountID, u.Id.OpaqueId) + if u.Opaque != nil { + if roles, ok := u.Opaque.Map["roles"]; ok { + ctx = gmmetadata.Set(ctx, opkgm.RoleIDs, string(roles.Value)) + } + } ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, t) next.ServeHTTP(w, r.WithContext(ctx)) diff --git a/extensions/graph/pkg/service/v0/service.go b/extensions/graph/pkg/service/v0/service.go index cf328ce0503..ca880b1bb62 100644 --- a/extensions/graph/pkg/service/v0/service.go +++ b/extensions/graph/pkg/service/v0/service.go @@ -14,8 +14,6 @@ import ( "github.com/owncloud/ocis/v2/extensions/graph/pkg/identity" "github.com/owncloud/ocis/v2/extensions/graph/pkg/identity/ldap" graphm "github.com/owncloud/ocis/v2/extensions/graph/pkg/middleware" - "github.com/owncloud/ocis/v2/ocis-pkg/account" - opkgm "github.com/owncloud/ocis/v2/ocis-pkg/middleware" "github.com/owncloud/ocis/v2/ocis-pkg/roles" "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" @@ -171,19 +169,13 @@ func NewService(opts ...Option) Service { }) }) }) - r.Group(func(r chi.Router) { - r.Use(opkgm.ExtractAccountUUID( - account.Logger(options.Logger), - account.JWTSecret(options.Config.TokenManager.JWTSecret)), - ) - r.Route("/drives", func(r chi.Router) { - r.Get("/", svc.GetAllDrives) - r.Post("/", svc.CreateDrive) - r.Route("/{driveID}", func(r chi.Router) { - r.Patch("/", svc.UpdateDrive) - r.Get("/", svc.GetSingleDrive) - r.Delete("/", svc.DeleteDrive) - }) + r.Route("/drives", func(r chi.Router) { + r.Get("/", svc.GetAllDrives) + r.Post("/", svc.CreateDrive) + r.Route("/{driveID}", func(r chi.Router) { + r.Patch("/", svc.UpdateDrive) + r.Get("/", svc.GetSingleDrive) + r.Delete("/", svc.DeleteDrive) }) }) }) From 211c7457eb99ad19ce33e583d3efdbfea02f9ff7 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 24 May 2022 08:51:07 +0200 Subject: [PATCH 3/8] Return "nameAlreadyExists" error properly When trying to create a user that already exist return a proper error, that clients can check for. --- extensions/graph/pkg/identity/ldap.go | 7 +++++++ extensions/graph/pkg/service/v0/users.go | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/extensions/graph/pkg/identity/ldap.go b/extensions/graph/pkg/identity/ldap.go index 25419455933..c9083061f42 100644 --- a/extensions/graph/pkg/identity/ldap.go +++ b/extensions/graph/pkg/identity/ldap.go @@ -161,6 +161,13 @@ func (i *LDAP) CreateUser(ctx context.Context, user libregraph.User) (*libregrap ar.Attribute("sn", []string{sn}) if err := i.conn.Add(&ar); err != nil { + var lerr *ldap.Error + i.logger.Debug().Err(err).Msg("error adding user") + if errors.As(err, &lerr) { + if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists { + err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error()) + } + } return nil, err } diff --git a/extensions/graph/pkg/service/v0/users.go b/extensions/graph/pkg/service/v0/users.go index 42a6909d717..e272bdf4226 100644 --- a/extensions/graph/pkg/service/v0/users.go +++ b/extensions/graph/pkg/service/v0/users.go @@ -117,7 +117,12 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) { } if u, err = g.identityBackend.CreateUser(r.Context(), *u); err != nil { - errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + var ecErr errorcode.Error + if errors.As(err, &ecErr) { + ecErr.Render(w, r) + } else { + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } return } From 9bdb03900d5d4e6950c91403e6b3cd1665fdb3ad Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 24 May 2022 13:15:08 +0200 Subject: [PATCH 4/8] Add user autoprovisioning via libreGraph When removing the accounts service we lost the user autoprovision feature. This re-introduces it. When autoprovisioning is enabled (via PROXY_AUTOPROVISION_ACCOUNTS, as in the past) accounts that are not resolvable via cs3 will be provsioned via the libregraph API. Closes: #3540 --- extensions/ocs/pkg/service/v0/service.go | 2 +- extensions/proxy/pkg/command/server.go | 14 +- extensions/proxy/pkg/config/config.go | 18 +- .../proxy/pkg/middleware/account_resolver.go | 5 +- extensions/proxy/pkg/user/backend/cs3.go | 206 +++++++++++++++++- 5 files changed, 227 insertions(+), 18 deletions(-) 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..683aa7e8ef3 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) } @@ -113,10 +132,191 @@ func (c *cs3backend) Authenticate(ctx context.Context, username string, password 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) { - 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 +} + +// 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 +} From 6a67be0cb85e06a785c4e600f5748487ac9f5e91 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 24 May 2022 15:34:25 +0200 Subject: [PATCH 5/8] Fix typo Co-authored-by: David Christofas --- extensions/graph/pkg/middleware/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/graph/pkg/middleware/auth.go b/extensions/graph/pkg/middleware/auth.go index 037aa0bc742..8d64b70f2ad 100644 --- a/extensions/graph/pkg/middleware/auth.go +++ b/extensions/graph/pkg/middleware/auth.go @@ -27,7 +27,7 @@ func authOptions(opts ...account.Option) account.Options { // Auth provides a middleware to authenticate requests using the x-access-token header value // and write it to the context. If there is no x-access-token the middleware prevents access and renders a json document. func Auth(opts ...account.Option) func(http.Handler) http.Handler { - // Note: This largely duplicates was ocis-pkg/middleware/account.go already does (apart from a slightly different error + // Note: This largely duplicates what ocis-pkg/middleware/account.go already does (apart from a slightly different error // handling). Ideally we should merge both middlewares. opt := authOptions(opts...) tokenManager, err := jwt.New(map[string]interface{}{ From 0eea228c7655d1e0d2e009df2eab52b7f4a1a1cf Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 24 May 2022 15:44:00 +0200 Subject: [PATCH 6/8] Add changelog for autoprovisioning --- changelog/unreleased/enhancement-user-autoprovision.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/unreleased/enhancement-user-autoprovision.md diff --git a/changelog/unreleased/enhancement-user-autoprovision.md b/changelog/unreleased/enhancement-user-autoprovision.md new file mode 100644 index 00000000000..d4beaade862 --- /dev/null +++ b/changelog/unreleased/enhancement-user-autoprovision.md @@ -0,0 +1,7 @@ +Enhancement: reintroduce user autoprovisioning in proxy + +With the removal of the accounts service autoprovisioning of users upon first login +was no longer possible. We added this feature back for the cs3 user backend in the proxy. +Leveraging the libregraph users API for creating the users. + +https://github.com/owncloud/ocis/pull/3860 From 40b484aaf133b25c073656336588fb882437ed64 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 24 May 2022 16:09:17 +0200 Subject: [PATCH 7/8] Use utlis function to void nil pointer panics Co-authored-by: kobergj --- extensions/graph/pkg/middleware/auth.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/extensions/graph/pkg/middleware/auth.go b/extensions/graph/pkg/middleware/auth.go index 8d64b70f2ad..328899dbb1e 100644 --- a/extensions/graph/pkg/middleware/auth.go +++ b/extensions/graph/pkg/middleware/auth.go @@ -6,6 +6,7 @@ import ( "github.com/cs3org/reva/v2/pkg/auth/scope" revactx "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/token/manager/jwt" + "github.com/cs3org/reva/v2/pkg/utils" "github.com/owncloud/ocis/v2/extensions/graph/pkg/service/v0/errorcode" "github.com/owncloud/ocis/v2/ocis-pkg/account" opkgm "github.com/owncloud/ocis/v2/ocis-pkg/middleware" @@ -74,10 +75,8 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler { ctx = revactx.ContextSetToken(ctx, t) ctx = revactx.ContextSetUser(ctx, u) ctx = gmmetadata.Set(ctx, opkgm.AccountID, u.Id.OpaqueId) - if u.Opaque != nil { - if roles, ok := u.Opaque.Map["roles"]; ok { - ctx = gmmetadata.Set(ctx, opkgm.RoleIDs, string(roles.Value)) - } + if role := utils.ReadPlainFromOpaque(u.Opaque, "roles"); role != "" { + ctx = gmmetadata.Set(ctx, opkgm.RoleIDs, role) } ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, t) From 1e6fa5b08e2ed5382a0b50c8a0315d5354fb87fc Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Tue, 24 May 2022 16:58:03 +0200 Subject: [PATCH 8/8] Fix autoprovisioning (keycload) deployment example --- .../config/ocis/entrypoint-override.sh | 29 ------------------- .../examples/ocis_keycloak/docker-compose.yml | 7 +++-- 2 files changed, 4 insertions(+), 32 deletions(-) delete mode 100644 deployments/examples/ocis_keycloak/config/ocis/entrypoint-override.sh diff --git a/deployments/examples/ocis_keycloak/config/ocis/entrypoint-override.sh b/deployments/examples/ocis_keycloak/config/ocis/entrypoint-override.sh deleted file mode 100644 index ba7fbe47e89..00000000000 --- a/deployments/examples/ocis_keycloak/config/ocis/entrypoint-override.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh - -set -e - -ocis server& -sleep 10 - -# stop builtin IDP since we use Keycloak as a replacement -#ocis kill idp - -echo "##################################################" -echo "change default secrets:" - -ocis accounts update --password $STORAGE_LDAP_BIND_PASSWORD bc596f3c-c955-4328-80a0-60d018b4ad57 # REVA - -echo "##################################################" - -echo "##################################################" -echo "delete demo users" # users are provided by keycloak - -set +e # accounts can only delete once, so it will fail the second time -# only admin, IDP and REVA user will be created because of ACCOUNTS_DEMO_USERS_AND_GROUPS=false -ocis accounts remove 820ba2a1-3f54-4538-80a4-2d73007e30bf # IDP user -ocis accounts remove ddc2004c-0977-11eb-9d3f-a793888cd0f8 # admin -set -e - -echo "##################################################" - -wait # wait for oCIS to exit diff --git a/deployments/examples/ocis_keycloak/docker-compose.yml b/deployments/examples/ocis_keycloak/docker-compose.yml index c847db80160..f6bbd466f81 100644 --- a/deployments/examples/ocis_keycloak/docker-compose.yml +++ b/deployments/examples/ocis_keycloak/docker-compose.yml @@ -49,7 +49,10 @@ services: ocis-net: entrypoint: - /bin/sh - - /entrypoint-override.sh + # run ocis init to initialize a configuration file with random secrets + # it will fail on subsequent runs, because the config file already exists + # therefore we ignore the error and then start the ocis server + command: ["-c", "ocis init || true; ocis server"] environment: # Keycloak IDP specific configuration PROXY_AUTOPROVISION_ACCOUNTS: "true" @@ -64,7 +67,6 @@ services: OCIS_LOG_LEVEL: ${OCIS_LOG_LEVEL:-error} # make oCIS less verbose PROXY_TLS: "false" # do not use SSL between Traefik and oCIS # demo users - ACCOUNTS_DEMO_USERS_AND_GROUPS: "${DEMO_USERS:-false}" # deprecated, remove after switching to LibreIDM IDM_CREATE_DEMO_USERS: "${DEMO_USERS:-false}" # change default secrets IDP_LDAP_BIND_PASSWORD: ${IDP_LDAP_BIND_PASSWORD:-idp} @@ -75,7 +77,6 @@ services: # INSECURE: needed if oCIS / Traefik is using self generated certificates OCIS_INSECURE: "${INSECURE:-false}" volumes: - - ./config/ocis/entrypoint-override.sh:/entrypoint-override.sh - ocis-data:/var/lib/ocis labels: - "traefik.enable=true"