refactor proxy
Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
|
||||
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/auth/scope"
|
||||
"github.com/cs3org/reva/v2/pkg/token"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/oidc"
|
||||
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
|
||||
)
|
||||
|
||||
// NewAccountsServiceUserBackend creates a user-provider which fetches users from the ocis accounts-service
|
||||
func NewAccountsServiceUserBackend(ac accountssvc.AccountsService, rs settingssvc.RoleService, oidcISS string, tokenManager token.Manager, logger log.Logger) UserBackend {
|
||||
return &accountsServiceBackend{
|
||||
accountsClient: ac,
|
||||
settingsRoleService: rs,
|
||||
OIDCIss: oidcISS,
|
||||
tokenManager: tokenManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type accountsServiceBackend struct {
|
||||
accountsClient accountssvc.AccountsService
|
||||
settingsRoleService settingssvc.RoleService
|
||||
OIDCIss string
|
||||
logger log.Logger
|
||||
tokenManager token.Manager
|
||||
}
|
||||
|
||||
func (a accountsServiceBackend) GetUserByClaims(ctx context.Context, claim, value string, withRoles bool) (*cs3.User, string, error) {
|
||||
var account *accountsmsg.Account
|
||||
var status int
|
||||
var query string
|
||||
|
||||
switch claim {
|
||||
case "mail":
|
||||
query = fmt.Sprintf("mail eq '%s'", strings.ReplaceAll(value, "'", "''"))
|
||||
case "username":
|
||||
query = fmt.Sprintf("preferred_name eq '%s'", strings.ReplaceAll(value, "'", "''"))
|
||||
case "id":
|
||||
query = fmt.Sprintf("id eq '%s'", strings.ReplaceAll(value, "'", "''"))
|
||||
default:
|
||||
return nil, "", fmt.Errorf("invalid user by claim lookup must be 'mail', 'username' or 'id")
|
||||
}
|
||||
|
||||
account, status = a.getAccount(ctx, query)
|
||||
if status == http.StatusNotFound {
|
||||
return nil, "", ErrAccountNotFound
|
||||
}
|
||||
|
||||
if status != 0 || account == nil {
|
||||
return nil, "", fmt.Errorf("could not get account, got status: %d", status)
|
||||
}
|
||||
|
||||
if !account.AccountEnabled {
|
||||
return nil, "", ErrAccountDisabled
|
||||
}
|
||||
|
||||
user := a.accountToUser(account)
|
||||
|
||||
if withRoles {
|
||||
if err := injectRoles(ctx, user, a.settingsRoleService); err != nil {
|
||||
a.logger.Warn().Err(err).Msgf("Could not load roles... continuing without")
|
||||
}
|
||||
}
|
||||
|
||||
token, err := a.generateToken(ctx, user)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return user, token, nil
|
||||
|
||||
}
|
||||
|
||||
// Authenticate authenticates against the accounts services and returns the user on success
|
||||
func (a *accountsServiceBackend) Authenticate(ctx context.Context, username string, password string) (*cs3.User, string, error) {
|
||||
query := fmt.Sprintf(
|
||||
"login eq '%s' and password eq '%s'",
|
||||
strings.ReplaceAll(username, "'", "''"),
|
||||
strings.ReplaceAll(password, "'", "''"),
|
||||
)
|
||||
account, status := a.getAccount(ctx, query)
|
||||
|
||||
if status != 0 {
|
||||
return nil, "", fmt.Errorf("could not authenticate with username, password for user %s. Status: %d", username, status)
|
||||
}
|
||||
|
||||
user := a.accountToUser(account)
|
||||
|
||||
token, err := a.generateToken(ctx, user)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if err := injectRoles(ctx, user, a.settingsRoleService); err != nil {
|
||||
a.logger.Warn().Err(err).Msgf("Could not load roles... continuing without")
|
||||
}
|
||||
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func (a accountsServiceBackend) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error) {
|
||||
req := &accountssvc.CreateAccountRequest{
|
||||
Account: &accountsmsg.Account{
|
||||
CreationType: "LocalAccount",
|
||||
AccountEnabled: true,
|
||||
},
|
||||
}
|
||||
var ok bool
|
||||
if req.Account.DisplayName, ok = claims[oidc.Name].(string); !ok {
|
||||
a.logger.Debug().Msg("Missing name claim, trying displayname")
|
||||
if req.Account.DisplayName, ok = claims["displayname"].(string); !ok {
|
||||
a.logger.Debug().Msg("Missing displayname claim")
|
||||
}
|
||||
}
|
||||
if req.Account.PreferredName, ok = claims[oidc.PreferredUsername].(string); !ok {
|
||||
a.logger.Warn().Msg("Missing preferred_username claim, falling back to email")
|
||||
if req.Account.PreferredName, ok = claims[oidc.Email].(string); !ok {
|
||||
a.logger.Debug().Msg("Missing email claim as well")
|
||||
}
|
||||
}
|
||||
if req.Account.PreferredName != "" {
|
||||
// also use as on premises samaccount name
|
||||
req.Account.OnPremisesSamAccountName = req.Account.PreferredName
|
||||
}
|
||||
if req.Account.Mail, ok = claims[oidc.Email].(string); !ok {
|
||||
a.logger.Warn().Msg("Missing email claim")
|
||||
}
|
||||
created, err := a.accountsClient.CreateAccount(context.Background(), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := a.accountToUser(created)
|
||||
|
||||
if err := injectRoles(ctx, user, a.settingsRoleService); err != nil {
|
||||
a.logger.Warn().Err(err).Msg("Could not load roles... continuing without")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a accountsServiceBackend) GetUserGroups(ctx context.Context, userID string) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
// accountToUser converts an owncloud account struct to a reva user struct. In the proxy
|
||||
// we work with the reva struct as a token can be minted from it.
|
||||
func (a *accountsServiceBackend) accountToUser(account *accountsmsg.Account) *cs3.User {
|
||||
user := &cs3.User{
|
||||
Id: &cs3.UserId{
|
||||
OpaqueId: account.Id,
|
||||
Idp: a.OIDCIss,
|
||||
Type: cs3.UserType_USER_TYPE_PRIMARY, // TODO: once we have support for other user types, this needs to be inferred
|
||||
},
|
||||
Username: account.OnPremisesSamAccountName,
|
||||
DisplayName: account.DisplayName,
|
||||
Mail: account.Mail,
|
||||
MailVerified: account.ExternalUserState == "" || account.ExternalUserState == "Accepted",
|
||||
Groups: expandGroups(account),
|
||||
UidNumber: account.UidNumber,
|
||||
GidNumber: account.GidNumber,
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (a *accountsServiceBackend) getAccount(ctx context.Context, query string) (account *accountsmsg.Account, status int) {
|
||||
resp, err := a.accountsClient.ListAccounts(ctx, &accountssvc.ListAccountsRequest{
|
||||
Query: query,
|
||||
PageSize: 2,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
a.logger.Error().Err(err).Str("query", query).Msgf("error fetching from accounts-service")
|
||||
status = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Accounts) <= 0 {
|
||||
a.logger.Error().Str("query", query).Msgf("account not found")
|
||||
status = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Accounts) > 1 {
|
||||
a.logger.Error().Str("query", query).Msgf("more than one account found, aborting")
|
||||
status = http.StatusForbidden
|
||||
return
|
||||
}
|
||||
|
||||
account = resp.Accounts[0]
|
||||
return
|
||||
}
|
||||
|
||||
func (a *accountsServiceBackend) generateToken(ctx context.Context, u *cs3.User) (string, error) {
|
||||
s, err := scope.AddOwnerScope(nil)
|
||||
if err != nil {
|
||||
a.logger.Error().Err(err).Msg("could not get owner scope")
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := a.tokenManager.MintToken(ctx, u, s)
|
||||
if err != nil {
|
||||
a.logger.Error().Err(err).Msg("could not mint token")
|
||||
return "", err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func expandGroups(account *accountsmsg.Account) []string {
|
||||
groups := make([]string, len(account.MemberOf))
|
||||
for i := range account.MemberOf {
|
||||
// reva needs the unix group name
|
||||
groups[i] = account.MemberOf[i].OnPremisesSamAccountName
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// injectRoles adds roles from the roles-service to the user-struct by mutating an existing struct
|
||||
func injectRoles(ctx context.Context, u *cs3.User, ss settingssvc.RoleService) error {
|
||||
roleIDs, err := loadRolesIDs(ctx, u.Id.OpaqueId, ss)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(roleIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
enc, err := encodeRoleIDs(roleIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.Opaque == nil {
|
||||
u.Opaque = &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{
|
||||
"roles": enc,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
u.Opaque.Map["roles"] = enc
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
|
||||
"github.com/owncloud/ocis/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/ocis-pkg/oidc"
|
||||
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
|
||||
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
|
||||
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
|
||||
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go-micro.dev/v4/client"
|
||||
)
|
||||
|
||||
var mockAccResp = []*accountsmsg.Account{
|
||||
{
|
||||
Id: "1234",
|
||||
AccountEnabled: true,
|
||||
DisplayName: "foo",
|
||||
PreferredName: "prefname",
|
||||
UidNumber: 1,
|
||||
GidNumber: 2,
|
||||
Mail: "foo@example.org",
|
||||
OnPremisesSamAccountName: "samaccount",
|
||||
MemberOf: []*accountsmsg.Group{
|
||||
{OnPremisesSamAccountName: "g1"},
|
||||
{OnPremisesSamAccountName: "g2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var expectedRoles = []*settingsmsg.UserRoleAssignment{
|
||||
{Id: "abc", AccountUuid: "1234", RoleId: "a"},
|
||||
{Id: "def", AccountUuid: "1234", RoleId: "b"},
|
||||
}
|
||||
|
||||
func TestGetUserByClaimsFound(t *testing.T) {
|
||||
type testCase struct {
|
||||
id, claim, value string
|
||||
}
|
||||
|
||||
var tests = []testCase{
|
||||
{id: "Mail", claim: "mail", value: "foo@example.org"},
|
||||
{id: "Username", claim: "username", value: "prefname"},
|
||||
{id: "ID", claim: "id", value: "1234"},
|
||||
}
|
||||
|
||||
accBackend := newAccountsBackend(mockAccResp, expectedRoles)
|
||||
|
||||
for k := range tests {
|
||||
t.Run(tests[k].id, func(t *testing.T) {
|
||||
u, _, err := accBackend.GetUserByClaims(context.Background(), tests[k].claim, tests[k].value, true)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, u)
|
||||
assertUserMatchesAccount(t, mockAccResp[0], u)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserByClaimsNotFound(t *testing.T) {
|
||||
accBackend := newAccountsBackend([]*accountsmsg.Account{}, expectedRoles)
|
||||
|
||||
u, _, err := accBackend.GetUserByClaims(context.Background(), "mail", "foo@example.com", true)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, u)
|
||||
assert.Equal(t, ErrAccountNotFound, err)
|
||||
}
|
||||
|
||||
func TestGetUserByClaimsInvalidClaim(t *testing.T) {
|
||||
accBackend := newAccountsBackend([]*accountsmsg.Account{}, expectedRoles)
|
||||
u, _, err := accBackend.GetUserByClaims(context.Background(), "invalidClaimName", "efwfwfwfe", true)
|
||||
|
||||
assert.Nil(t, u)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetUserByClaimsDisabledAccount(t *testing.T) {
|
||||
accBackend := newAccountsBackend([]*accountsmsg.Account{{AccountEnabled: false}}, expectedRoles)
|
||||
u, _, err := accBackend.GetUserByClaims(context.Background(), "mail", "foo@example.com", true)
|
||||
|
||||
assert.Nil(t, u)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrAccountDisabled, err)
|
||||
}
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
accBackend := newAccountsBackend(mockAccResp, expectedRoles)
|
||||
u, _, err := accBackend.Authenticate(context.Background(), "foo", "secret")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, u)
|
||||
assertUserMatchesAccount(t, mockAccResp[0], u)
|
||||
}
|
||||
|
||||
func TestAuthenticateFailed(t *testing.T) {
|
||||
accBackend := newAccountsBackend([]*accountsmsg.Account{}, expectedRoles)
|
||||
u, _, err := accBackend.Authenticate(context.Background(), "foo", "secret")
|
||||
|
||||
assert.Nil(t, u)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCreateUserFromClaims(t *testing.T) {
|
||||
exp := mockAccResp[0]
|
||||
accBackend := newAccountsBackend([]*accountsmsg.Account{}, expectedRoles)
|
||||
act, _ := accBackend.CreateUserFromClaims(context.Background(), map[string]interface{}{
|
||||
oidc.Name: mockAccResp[0].DisplayName,
|
||||
oidc.PreferredUsername: mockAccResp[0].OnPremisesSamAccountName,
|
||||
oidc.Email: mockAccResp[0].Mail,
|
||||
oidc.UIDNumber: "1",
|
||||
oidc.GIDNumber: "2",
|
||||
oidc.Groups: []string{"g1", "g2"},
|
||||
})
|
||||
|
||||
assert.NotNil(t, act.Id)
|
||||
assert.Equal(t, exp.Id, act.Id.OpaqueId)
|
||||
assert.Equal(t, exp.Mail, act.Mail)
|
||||
assert.Equal(t, exp.DisplayName, act.DisplayName)
|
||||
assert.Equal(t, exp.OnPremisesSamAccountName, act.Username)
|
||||
}
|
||||
|
||||
func TestGetUserGroupsUnimplemented(t *testing.T) {
|
||||
accBackend := newAccountsBackend([]*accountsmsg.Account{}, expectedRoles)
|
||||
assert.Panics(t, func() { accBackend.GetUserGroups(context.Background(), "foo") })
|
||||
}
|
||||
|
||||
func assertUserMatchesAccount(t *testing.T, exp *accountsmsg.Account, act *userv1beta1.User) {
|
||||
// User
|
||||
assert.NotNil(t, act.Id)
|
||||
assert.Equal(t, exp.Id, act.Id.OpaqueId)
|
||||
assert.Equal(t, exp.Mail, act.Mail)
|
||||
assert.Equal(t, exp.DisplayName, act.DisplayName)
|
||||
assert.Equal(t, exp.OnPremisesSamAccountName, act.Username)
|
||||
|
||||
// Groups
|
||||
assert.ElementsMatch(t, []string{"g1", "g2"}, act.Groups)
|
||||
|
||||
// Roles
|
||||
assert.NotNil(t, act.Opaque.Map["roles"])
|
||||
assert.Equal(t, `["a","b"]`, string(act.Opaque.Map["roles"].GetValue()))
|
||||
|
||||
// UID/GID
|
||||
assert.Equal(t, int64(1), act.UidNumber)
|
||||
assert.Equal(t, int64(2), act.GidNumber)
|
||||
}
|
||||
|
||||
func newAccountsBackend(mockAccounts []*accountsmsg.Account, mockRoles []*settingsmsg.UserRoleAssignment) UserBackend {
|
||||
accSvc, roleSvc := getAccountService(mockAccounts, nil), getRoleService(mockRoles, nil)
|
||||
tokenManager, _ := jwt.New(map[string]interface{}{
|
||||
"secret": "change-me",
|
||||
"expires": int64(24 * 60 * 60),
|
||||
})
|
||||
accBackend := NewAccountsServiceUserBackend(accSvc, roleSvc, "https://idp.example.org", tokenManager, log.NewLogger())
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
return accBackend
|
||||
}
|
||||
|
||||
func getAccountService(expectedResponse []*accountsmsg.Account, err error) *accountssvc.MockAccountsService {
|
||||
return &accountssvc.MockAccountsService{
|
||||
ListFunc: func(ctx context.Context, in *accountssvc.ListAccountsRequest, opts ...client.CallOption) (*accountssvc.ListAccountsResponse, error) {
|
||||
return &accountssvc.ListAccountsResponse{Accounts: expectedResponse}, err
|
||||
},
|
||||
CreateFunc: func(ctx context.Context, in *accountssvc.CreateAccountRequest, opts ...client.CallOption) (*accountsmsg.Account, error) {
|
||||
a := in.Account
|
||||
a.Id = "1234"
|
||||
return a, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getRoleService(expectedResponse []*settingsmsg.UserRoleAssignment, err error) *settingssvc.MockRoleService {
|
||||
return &settingssvc.MockRoleService{
|
||||
ListRoleAssignmentsFunc: func(ctx context.Context, req *settingssvc.ListRoleAssignmentsRequest, opts ...client.CallOption) (*settingssvc.ListRoleAssignmentsResponse, error) {
|
||||
return &settingssvc.ListRoleAssignmentsResponse{Assignments: expectedResponse}, err
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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/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
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
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/owncloud/ocis/ocis-pkg/log"
|
||||
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
|
||||
settingsService "github.com/owncloud/ocis/settings/pkg/service/v0"
|
||||
)
|
||||
|
||||
type cs3backend struct {
|
||||
settingsRoleService settingssvc.RoleService
|
||||
authProvider RevaAuthenticator
|
||||
machineAuthAPIKey string
|
||||
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 {
|
||||
return &cs3backend{
|
||||
settingsRoleService: rs,
|
||||
authProvider: ap,
|
||||
machineAuthAPIKey: machineAuthAPIKey,
|
||||
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 {
|
||||
c.logger.Error().Err(err).Msgf("Could not load roles")
|
||||
}
|
||||
}
|
||||
|
||||
// if roles are empty, assume we haven't seen the user before and assign a
|
||||
// default user role. At least until proper roles are provided. See
|
||||
// https://github.com/owncloud/ocis/issues/1825 for more context.
|
||||
if len(roleIDs) == 0 {
|
||||
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")
|
||||
}
|
||||
roleIDs = append(roleIDs, settingsService.BundleUUIDRoleUser)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func (c cs3backend) GetUserGroups(ctx context.Context, userID string) {
|
||||
panic("implement me")
|
||||
}
|
||||
@@ -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/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
|
||||
}
|
||||
Reference in New Issue
Block a user