Skip to content

Commit

Permalink
Add user autoprovisioning via libreGraph
Browse files Browse the repository at this point in the history
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: owncloud#3540
  • Loading branch information
rhafer committed May 24, 2022
1 parent 211c745 commit 9bdb039
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 18 deletions.
2 changes: 1 addition & 1 deletion extensions/ocs/pkg/service/v0/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions extensions/proxy/pkg/command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)
Expand Down Expand Up @@ -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)
}
Expand Down
18 changes: 9 additions & 9 deletions extensions/proxy/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
Expand Down Expand Up @@ -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"`
}

Expand Down
5 changes: 3 additions & 2 deletions extensions/proxy/pkg/middleware/account_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
206 changes: 203 additions & 3 deletions extensions/proxy/pkg/user/backend/cs3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}

0 comments on commit 9bdb039

Please sign in to comment.