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
This commit is contained in:
Ralf Haferkamp
2022-05-24 13:15:08 +02:00
committed by Ralf Haferkamp
parent d322e50167
commit 38127757e4
5 changed files with 227 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

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