rename folder extensions -> services

Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
Christian Richter
2022-06-20 16:46:09 +02:00
parent 1aea93d8cd
commit 78064e6bab
926 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
package backend
import (
"context"
"encoding/json"
"errors"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"google.golang.org/grpc"
)
var (
// ErrAccountNotFound account not found
ErrAccountNotFound = errors.New("user not found")
// ErrAccountDisabled account disabled
ErrAccountDisabled = errors.New("account disabled")
// ErrNotSupported operation not supported by user-backend
ErrNotSupported = errors.New("operation not supported")
)
// UserBackend allows the proxy to retrieve users from different user-backends (accounts-service, CS3)
type UserBackend interface {
GetUserByClaims(ctx context.Context, claim, value string, withRoles bool) (*cs3.User, string, error)
Authenticate(ctx context.Context, username string, password string) (*cs3.User, string, error)
CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error)
GetUserGroups(ctx context.Context, userID string)
}
// RevaAuthenticator helper interface to mock auth-method from reva gateway-client.
type RevaAuthenticator interface {
Authenticate(ctx context.Context, in *gateway.AuthenticateRequest, opts ...grpc.CallOption) (*gateway.AuthenticateResponse, error)
}
// loadRolesIDs returns the role-ids assigned to an user
func loadRolesIDs(ctx context.Context, opaqueUserID string, rs settingssvc.RoleService) ([]string, error) {
req := &settingssvc.ListRoleAssignmentsRequest{AccountUuid: opaqueUserID}
assignmentResponse, err := rs.ListRoleAssignments(ctx, req)
if err != nil {
return nil, err
}
roleIDs := make([]string, 0)
for _, assignment := range assignmentResponse.Assignments {
roleIDs = append(roleIDs, assignment.RoleId)
}
return roleIDs, nil
}
// encodeRoleIDs encoded the given role id's in to reva-specific format to be able to mint a token from them
func encodeRoleIDs(roleIDs []string) (*types.OpaqueEntry, error) {
roleIDsJSON, err := json.Marshal(roleIDs)
if err != nil {
return nil, err
}
return &types.OpaqueEntry{
Decoder: "json",
Value: roleIDsJSON,
}, nil
}

View File

@@ -0,0 +1,327 @@
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/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"
merrors "go-micro.dev/v4/errors"
"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 : %w ", claim, value, err)
}
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")
_, 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
}

View File

@@ -0,0 +1,248 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package test
import (
"context"
"sync"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/user/backend"
)
// Ensure, that UserBackendMock does implement UserBackend.
// If this is not the case, regenerate this file with moq.
var _ backend.UserBackend = &UserBackendMock{}
// UserBackendMock is a mock implementation of UserBackend.
//
// func TestSomethingThatUsesUserBackend(t *testing.T) {
//
// // make and configure a mocked UserBackend
// mockedUserBackend := &UserBackendMock{
// AuthenticateFunc: func(ctx context.Context, username string, password string) (*userv1beta1.User, error) {
// panic("mock out the Authenticate method")
// },
// CreateUserFromClaimsFunc: func(ctx context.Context, claims *oidc.StandardClaims) (*userv1beta1.User, error) {
// panic("mock out the CreateUserFromClaims method")
// },
// GetUserByClaimsFunc: func(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, error) {
// panic("mock out the GetUserByClaims method")
// },
// GetUserGroupsFunc: func(ctx context.Context, userID string) {
// panic("mock out the GetUserGroups method")
// },
// }
//
// // use mockedUserBackend in code that requires UserBackend
// // and then make assertions.
//
// }
type UserBackendMock struct {
// AuthenticateFunc mocks the Authenticate method.
AuthenticateFunc func(ctx context.Context, username string, password string) (*userv1beta1.User, string, error)
// CreateUserFromClaimsFunc mocks the CreateUserFromClaims method.
CreateUserFromClaimsFunc func(ctx context.Context, claims map[string]interface{}) (*userv1beta1.User, error)
// GetUserByClaimsFunc mocks the GetUserByClaims method.
GetUserByClaimsFunc func(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, string, error)
// GetUserGroupsFunc mocks the GetUserGroups method.
GetUserGroupsFunc func(ctx context.Context, userID string)
// calls tracks calls to the methods.
calls struct {
// Authenticate holds details about calls to the Authenticate method.
Authenticate []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Username is the username argument value.
Username string
// Password is the password argument value.
Password string
}
// CreateUserFromClaims holds details about calls to the CreateUserFromClaims method.
CreateUserFromClaims []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Claims is the claims argument value.
Claims map[string]interface{}
}
// GetUserByClaims holds details about calls to the GetUserByClaims method.
GetUserByClaims []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Claim is the claim argument value.
Claim string
// Value is the value argument value.
Value string
// WithRoles is the withRoles argument value.
WithRoles bool
}
// GetUserGroups holds details about calls to the GetUserGroups method.
GetUserGroups []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// UserID is the userID argument value.
UserID string
}
}
lockAuthenticate sync.RWMutex
lockCreateUserFromClaims sync.RWMutex
lockGetUserByClaims sync.RWMutex
lockGetUserGroups sync.RWMutex
}
// Authenticate calls AuthenticateFunc.
func (mock *UserBackendMock) Authenticate(ctx context.Context, username string, password string) (*userv1beta1.User, string, error) {
if mock.AuthenticateFunc == nil {
panic("UserBackendMock.AuthenticateFunc: method is nil but UserBackend.Authenticate was just called")
}
callInfo := struct {
Ctx context.Context
Username string
Password string
}{
Ctx: ctx,
Username: username,
Password: password,
}
mock.lockAuthenticate.Lock()
mock.calls.Authenticate = append(mock.calls.Authenticate, callInfo)
mock.lockAuthenticate.Unlock()
return mock.AuthenticateFunc(ctx, username, password)
}
// AuthenticateCalls gets all the calls that were made to Authenticate.
// Check the length with:
// len(mockedUserBackend.AuthenticateCalls())
func (mock *UserBackendMock) AuthenticateCalls() []struct {
Ctx context.Context
Username string
Password string
} {
var calls []struct {
Ctx context.Context
Username string
Password string
}
mock.lockAuthenticate.RLock()
calls = mock.calls.Authenticate
mock.lockAuthenticate.RUnlock()
return calls
}
// CreateUserFromClaims calls CreateUserFromClaimsFunc.
func (mock *UserBackendMock) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*userv1beta1.User, error) {
if mock.CreateUserFromClaimsFunc == nil {
panic("UserBackendMock.CreateUserFromClaimsFunc: method is nil but UserBackend.CreateUserFromClaims was just called")
}
callInfo := struct {
Ctx context.Context
Claims map[string]interface{}
}{
Ctx: ctx,
Claims: claims,
}
mock.lockCreateUserFromClaims.Lock()
mock.calls.CreateUserFromClaims = append(mock.calls.CreateUserFromClaims, callInfo)
mock.lockCreateUserFromClaims.Unlock()
return mock.CreateUserFromClaimsFunc(ctx, claims)
}
// CreateUserFromClaimsCalls gets all the calls that were made to CreateUserFromClaims.
// Check the length with:
// len(mockedUserBackend.CreateUserFromClaimsCalls())
func (mock *UserBackendMock) CreateUserFromClaimsCalls() []struct {
Ctx context.Context
Claims map[string]interface{}
} {
var calls []struct {
Ctx context.Context
Claims map[string]interface{}
}
mock.lockCreateUserFromClaims.RLock()
calls = mock.calls.CreateUserFromClaims
mock.lockCreateUserFromClaims.RUnlock()
return calls
}
// GetUserByClaims calls GetUserByClaimsFunc.
func (mock *UserBackendMock) GetUserByClaims(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, string, error) {
if mock.GetUserByClaimsFunc == nil {
panic("UserBackendMock.GetUserByClaimsFunc: method is nil but UserBackend.GetUserByClaims was just called")
}
callInfo := struct {
Ctx context.Context
Claim string
Value string
WithRoles bool
}{
Ctx: ctx,
Claim: claim,
Value: value,
WithRoles: withRoles,
}
mock.lockGetUserByClaims.Lock()
mock.calls.GetUserByClaims = append(mock.calls.GetUserByClaims, callInfo)
mock.lockGetUserByClaims.Unlock()
return mock.GetUserByClaimsFunc(ctx, claim, value, withRoles)
}
// GetUserByClaimsCalls gets all the calls that were made to GetUserByClaims.
// Check the length with:
// len(mockedUserBackend.GetUserByClaimsCalls())
func (mock *UserBackendMock) GetUserByClaimsCalls() []struct {
Ctx context.Context
Claim string
Value string
WithRoles bool
} {
var calls []struct {
Ctx context.Context
Claim string
Value string
WithRoles bool
}
mock.lockGetUserByClaims.RLock()
calls = mock.calls.GetUserByClaims
mock.lockGetUserByClaims.RUnlock()
return calls
}
// GetUserGroups calls GetUserGroupsFunc.
func (mock *UserBackendMock) GetUserGroups(ctx context.Context, userID string) {
if mock.GetUserGroupsFunc == nil {
panic("UserBackendMock.GetUserGroupsFunc: method is nil but UserBackend.GetUserGroups was just called")
}
callInfo := struct {
Ctx context.Context
UserID string
}{
Ctx: ctx,
UserID: userID,
}
mock.lockGetUserGroups.Lock()
mock.calls.GetUserGroups = append(mock.calls.GetUserGroups, callInfo)
mock.lockGetUserGroups.Unlock()
mock.GetUserGroupsFunc(ctx, userID)
}
// GetUserGroupsCalls gets all the calls that were made to GetUserGroups.
// Check the length with:
// len(mockedUserBackend.GetUserGroupsCalls())
func (mock *UserBackendMock) GetUserGroupsCalls() []struct {
Ctx context.Context
UserID string
} {
var calls []struct {
Ctx context.Context
UserID string
}
mock.lockGetUserGroups.RLock()
calls = mock.calls.GetUserGroups
mock.lockGetUserGroups.RUnlock()
return calls
}