Initial QSfera import
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
// Package store implements the go-micro store interface
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/settings"
|
||||
"github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
)
|
||||
|
||||
// ListRoleAssignments loads and returns all role assignments matching the given assignment identifier.
|
||||
func (s *Store) ListRoleAssignments(accountUUID string) ([]*settingsmsg.UserRoleAssignment, error) {
|
||||
// shortcut for service accounts
|
||||
for _, serviceAccountID := range s.cfg.ServiceAccountIDs {
|
||||
if accountUUID == serviceAccountID {
|
||||
return []*settingsmsg.UserRoleAssignment{
|
||||
{
|
||||
Id: uuid.NewString(), // should we hardcode this id too?
|
||||
AccountUuid: accountUUID,
|
||||
RoleId: defaults.BundleUUIDServiceAccount,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
assIDs, err := s.mdc.ReadDir(ctx, accountPath(accountUUID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return make([]*settingsmsg.UserRoleAssignment, 0), nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ass := make([]*settingsmsg.UserRoleAssignment, 0, len(assIDs))
|
||||
for _, assID := range assIDs {
|
||||
b, err := s.mdc.SimpleDownload(ctx, assignmentPath(accountUUID, assID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
continue
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := &settingsmsg.UserRoleAssignment{}
|
||||
err = json.Unmarshal(b, a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ass = append(ass, a)
|
||||
}
|
||||
return ass, nil
|
||||
}
|
||||
|
||||
// ListRoleAssignmentsByRole returns all role assignmentes matching the give roleID
|
||||
func (s *Store) ListRoleAssignmentsByRole(roleID string) ([]*settingsmsg.UserRoleAssignment, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
accountIDs, err := s.mdc.ReadDir(ctx, accountsFolderLocation)
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return make([]*settingsmsg.UserRoleAssignment, 0), nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
assignments := make([]*settingsmsg.UserRoleAssignment, 0, len(accountIDs))
|
||||
|
||||
// This is very inefficient, with the current layout we need to iterated through all
|
||||
// account folders and read each assignment file in there to check if that contains
|
||||
// the give role ID.
|
||||
for _, account := range accountIDs {
|
||||
assignmentIDs, err := s.mdc.ReadDir(ctx, accountPath(account))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return make([]*settingsmsg.UserRoleAssignment, 0), nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, assignmentID := range assignmentIDs {
|
||||
b, err := s.mdc.SimpleDownload(ctx, assignmentPath(account, assignmentID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
continue
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := &settingsmsg.UserRoleAssignment{}
|
||||
err = json.Unmarshal(b, a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if a.GetRoleId() == roleID {
|
||||
assignments = append(assignments, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
return assignments, nil
|
||||
}
|
||||
|
||||
// WriteRoleAssignment appends the given role assignment to the existing assignments of the respective account.
|
||||
func (s *Store) WriteRoleAssignment(accountUUID, roleID string) (*settingsmsg.UserRoleAssignment, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
// as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role"
|
||||
err := s.mdc.Delete(ctx, accountPath(accountUUID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
// already gone, continue
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ass := &settingsmsg.UserRoleAssignment{
|
||||
Id: uuid.NewString(),
|
||||
AccountUuid: accountUUID,
|
||||
RoleId: roleID,
|
||||
}
|
||||
b, err := json.Marshal(ass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ass, s.mdc.SimpleUpload(ctx, assignmentPath(accountUUID, ass.Id), b)
|
||||
}
|
||||
|
||||
// RemoveRoleAssignment deletes the given role assignment from the existing assignments of the respective account.
|
||||
func (s *Store) RemoveRoleAssignment(assignmentID string) error {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
accounts, err := s.mdc.ReadDir(ctx, accountsFolderLocation)
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return fmt.Errorf("assignmentID '%s' %w", assignmentID, settings.ErrNotFound)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: use indexer to avoid spamming Metadata service
|
||||
for _, accID := range accounts {
|
||||
assIDs, err := s.mdc.ReadDir(ctx, accountPath(accID))
|
||||
if err != nil {
|
||||
// TODO: error?
|
||||
continue
|
||||
}
|
||||
|
||||
for _, assID := range assIDs {
|
||||
if assID == assignmentID {
|
||||
// as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role"
|
||||
// we also have to delete the cached dir listing
|
||||
return s.mdc.Delete(ctx, accountPath(accID))
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("assignmentID '%s' %w", assignmentID, settings.ErrNotFound)
|
||||
}
|
||||
|
||||
func accountPath(accountUUID string) string {
|
||||
return fmt.Sprintf("%s/%s", accountsFolderLocation, accountUUID)
|
||||
}
|
||||
|
||||
func assignmentPath(accountUUID string, assignmentID string) string {
|
||||
return fmt.Sprintf("%s/%s/%s", accountsFolderLocation, accountUUID, assignmentID)
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
olog "github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/shared"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/config/defaults"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
alan = "00000000-0000-0000-0000-000000000001"
|
||||
mary = "00000000-0000-0000-0000-000000000002"
|
||||
dennis = "00000000-0000-0000-0000-000000000003"
|
||||
|
||||
role1 = "11111111-1111-1111-1111-111111111111"
|
||||
role2 = "22222222-2222-2222-2222-222222222222"
|
||||
|
||||
logger = olog.NewLogger(
|
||||
olog.Color(true),
|
||||
olog.Pretty(true),
|
||||
olog.Level("info"),
|
||||
)
|
||||
|
||||
bundles = []*settingsmsg.Bundle{
|
||||
{
|
||||
Id: "f36db5e6-a03c-40df-8413-711c67e40b47",
|
||||
Type: settingsmsg.Bundle_TYPE_ROLE,
|
||||
DisplayName: "test role - reads | update",
|
||||
Name: "TEST_ROLE",
|
||||
Extension: "qsfera-settings",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_BUNDLE,
|
||||
},
|
||||
Settings: []*settingsmsg.Setting{
|
||||
{
|
||||
Id: "updateID",
|
||||
Name: "update",
|
||||
Value: &settingsmsg.Setting_PermissionValue{
|
||||
PermissionValue: &settingsmsg.Permission{
|
||||
Operation: settingsmsg.Permission_OPERATION_UPDATE,
|
||||
},
|
||||
},
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SETTING,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "readID",
|
||||
Name: "read",
|
||||
Value: &settingsmsg.Setting_PermissionValue{
|
||||
PermissionValue: &settingsmsg.Permission{
|
||||
Operation: settingsmsg.Permission_OPERATION_READ,
|
||||
},
|
||||
},
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_BUNDLE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
|
||||
Type: settingsmsg.Bundle_TYPE_ROLE,
|
||||
DisplayName: "another",
|
||||
Name: "ANOTHER_TEST_ROLE",
|
||||
Extension: "qsfera-settings",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_BUNDLE,
|
||||
},
|
||||
Settings: []*settingsmsg.Setting{
|
||||
{
|
||||
Id: "readID",
|
||||
Name: "read",
|
||||
Value: &settingsmsg.Setting_PermissionValue{
|
||||
PermissionValue: &settingsmsg.Permission{
|
||||
Operation: settingsmsg.Permission_OPERATION_READ,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func initStore() *Store {
|
||||
s := &Store{
|
||||
Logger: logger,
|
||||
l: &sync.Mutex{},
|
||||
cfg: defaults.DefaultConfig(),
|
||||
}
|
||||
s.cfg.Commons = &shared.Commons{
|
||||
AdminUserID: uuid.NewString(),
|
||||
}
|
||||
|
||||
_ = NewMDC(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func setupRoles(s *Store) {
|
||||
for i := range bundles {
|
||||
if _, err := s.WriteBundle(bundles[i]); err != nil {
|
||||
log.Fatal("error initializing ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignmentUniqueness(t *testing.T) {
|
||||
var scenarios = []struct {
|
||||
name string
|
||||
userID string
|
||||
firstRole string
|
||||
secondRole string
|
||||
}{
|
||||
{
|
||||
"roles assignments",
|
||||
alan,
|
||||
"f36db5e6-a03c-40df-8413-711c67e40b47",
|
||||
"44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
s := initStore()
|
||||
setupRoles(s)
|
||||
firstAssignment, err := s.WriteRoleAssignment(scenario.userID, scenario.firstRole)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, firstAssignment.RoleId, scenario.firstRole)
|
||||
// TODO: check entry exists
|
||||
|
||||
list, err := s.ListRoleAssignments(scenario.userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(list))
|
||||
require.Equal(t, list[0].RoleId, scenario.firstRole)
|
||||
|
||||
// creating another assignment shouldn't add another entry, as we support max one role per user.
|
||||
// assigning the second role should remove the old
|
||||
secondAssignment, err := s.WriteRoleAssignment(scenario.userID, scenario.secondRole)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, secondAssignment.RoleId, scenario.secondRole)
|
||||
|
||||
list, err = s.ListRoleAssignments(scenario.userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(list))
|
||||
require.Equal(t, list[0].RoleId, scenario.secondRole)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoleAssignmentByRole(t *testing.T) {
|
||||
type assignment struct {
|
||||
userID string
|
||||
roleID string
|
||||
}
|
||||
|
||||
var scenarios = []struct {
|
||||
name string
|
||||
assignments []assignment
|
||||
queryRole string
|
||||
numResults int
|
||||
}{
|
||||
{
|
||||
name: "just 2 assignments",
|
||||
assignments: []assignment{
|
||||
{
|
||||
userID: alan,
|
||||
roleID: role1,
|
||||
}, {
|
||||
userID: mary,
|
||||
roleID: role1,
|
||||
},
|
||||
},
|
||||
queryRole: role1,
|
||||
numResults: 2,
|
||||
},
|
||||
{
|
||||
name: "no assignments match",
|
||||
assignments: []assignment{
|
||||
{
|
||||
userID: alan,
|
||||
roleID: role1,
|
||||
}, {
|
||||
userID: mary,
|
||||
roleID: role1,
|
||||
},
|
||||
},
|
||||
queryRole: role2,
|
||||
numResults: 0,
|
||||
},
|
||||
{
|
||||
name: "only one assignment matches",
|
||||
assignments: []assignment{
|
||||
{
|
||||
userID: alan,
|
||||
roleID: role1,
|
||||
}, {
|
||||
userID: mary,
|
||||
roleID: role1,
|
||||
}, {
|
||||
userID: dennis,
|
||||
roleID: role2,
|
||||
},
|
||||
},
|
||||
queryRole: role2,
|
||||
numResults: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
s := initStore()
|
||||
setupRoles(s)
|
||||
for _, a := range scenario.assignments {
|
||||
ass, err := s.WriteRoleAssignment(a.userID, a.roleID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ass.RoleId, a.roleID)
|
||||
}
|
||||
|
||||
list, err := s.ListRoleAssignmentsByRole(scenario.queryRole)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, scenario.numResults, len(list))
|
||||
for _, ass := range list {
|
||||
require.Equal(t, ass.RoleId, scenario.queryRole)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAssignment(t *testing.T) {
|
||||
var scenarios = []struct {
|
||||
name string
|
||||
userID string
|
||||
firstRole string
|
||||
secondRole string
|
||||
}{
|
||||
{
|
||||
"roles assignments",
|
||||
alan,
|
||||
"f36db5e6-a03c-40df-8413-711c67e40b47",
|
||||
"44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
s := initStore()
|
||||
setupRoles(s)
|
||||
assignment, err := s.WriteRoleAssignment(scenario.userID, scenario.firstRole)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, assignment.RoleId, scenario.firstRole)
|
||||
// TODO: uncomment
|
||||
// require.True(t, mdc.IDExists(assignment.RoleId))
|
||||
|
||||
list, err := s.ListRoleAssignments(scenario.userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(list))
|
||||
require.Equal(t, assignment.Id, list[0].Id)
|
||||
|
||||
err = s.RemoveRoleAssignment(assignment.Id)
|
||||
require.NoError(t, err)
|
||||
// TODO: uncomment
|
||||
// require.False(t, mdc.IDExists(assignment.RoleId))
|
||||
|
||||
list, err = s.ListRoleAssignments(scenario.userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(list))
|
||||
|
||||
err = s.RemoveRoleAssignment(assignment.Id)
|
||||
require.Error(t, err)
|
||||
// TODO: do we want a custom error message?
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Package store implements the go-micro store interface
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/settings"
|
||||
"github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
)
|
||||
|
||||
// ListBundles returns all bundles in the dataPath folder that match the given type.
|
||||
func (s *Store) ListBundles(bundleType settingsmsg.Bundle_Type, bundleIDs []string) ([]*settingsmsg.Bundle, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
if len(bundleIDs) == 0 {
|
||||
bIDs, err := s.mdc.ReadDir(ctx, bundleFolderLocation)
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return make([]*settingsmsg.Bundle, 0), nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bundleIDs = bIDs
|
||||
}
|
||||
var bundles []*settingsmsg.Bundle
|
||||
for _, id := range bundleIDs {
|
||||
if id == defaults.BundleUUIDServiceAccount {
|
||||
bundles = append(bundles, defaults.ServiceAccountBundle())
|
||||
continue
|
||||
}
|
||||
b, err := s.mdc.SimpleDownload(ctx, bundlePath(id))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
continue
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bundle := &settingsmsg.Bundle{}
|
||||
err = json.Unmarshal(b, bundle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bundle.Type == bundleType {
|
||||
bundles = append(bundles, bundle)
|
||||
}
|
||||
|
||||
}
|
||||
return bundles, nil
|
||||
}
|
||||
|
||||
// ReadBundle tries to find a bundle by the given id from the metadata service
|
||||
func (s *Store) ReadBundle(bundleID string) (*settingsmsg.Bundle, error) {
|
||||
// shortcut for service accounts
|
||||
if bundleID == defaults.BundleUUIDServiceAccount {
|
||||
return defaults.ServiceAccountBundle(), nil
|
||||
}
|
||||
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
b, err := s.mdc.SimpleDownload(ctx, bundlePath(bundleID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return nil, fmt.Errorf("bundleID '%s' %w", bundleID, settings.ErrNotFound)
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bundle := &settingsmsg.Bundle{}
|
||||
return bundle, json.Unmarshal(b, bundle)
|
||||
}
|
||||
|
||||
// ReadSetting tries to find a setting by the given id from the metadata service
|
||||
func (s *Store) ReadSetting(settingID string) (*settingsmsg.Setting, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
ids, err := s.mdc.ReadDir(ctx, bundleFolderLocation)
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return nil, fmt.Errorf("settingID '%s' %w", settingID, settings.ErrNotFound)
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: avoid spamming metadata service
|
||||
for _, id := range ids {
|
||||
b, err := s.ReadBundle(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, settings.ErrNotFound) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, setting := range b.Settings {
|
||||
if setting.Id == settingID {
|
||||
return setting, nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil, fmt.Errorf("settingID '%s' %w", settingID, settings.ErrNotFound)
|
||||
}
|
||||
|
||||
// WriteBundle sends the givens record to the metadataclient. returns `record` for legacy reasons
|
||||
func (s *Store) WriteBundle(record *settingsmsg.Bundle) (*settingsmsg.Bundle, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
b, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, s.mdc.SimpleUpload(ctx, bundlePath(record.Id), b)
|
||||
}
|
||||
|
||||
// AddSettingToBundle adds the given setting to the bundle with the given bundleID.
|
||||
func (s *Store) AddSettingToBundle(bundleID string, setting *settingsmsg.Setting) (*settingsmsg.Setting, error) {
|
||||
s.Init()
|
||||
b, err := s.ReadBundle(bundleID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, settings.ErrNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
b = new(settingsmsg.Bundle)
|
||||
b.Id = bundleID
|
||||
b.Type = settingsmsg.Bundle_TYPE_DEFAULT
|
||||
}
|
||||
|
||||
if setting.Id == "" {
|
||||
setting.Id = uuid.NewString()
|
||||
}
|
||||
|
||||
b.Settings = append(b.Settings, setting)
|
||||
_, err = s.WriteBundle(b)
|
||||
return setting, err
|
||||
}
|
||||
|
||||
// RemoveSettingFromBundle removes the setting from the bundle with the given ids.
|
||||
func (s *Store) RemoveSettingFromBundle(bundleID string, settingID string) error {
|
||||
fmt.Println("RemoveSettingFromBundle not implemented")
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func bundlePath(id string) string {
|
||||
return fmt.Sprintf("%s/%s", bundleFolderLocation, id)
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var bundleScenarios = []struct {
|
||||
name string
|
||||
bundle *settingsmsg.Bundle
|
||||
}{
|
||||
{
|
||||
name: "generic-test-file-resource",
|
||||
bundle: &settingsmsg.Bundle{
|
||||
Id: bundle1,
|
||||
Type: settingsmsg.Bundle_TYPE_DEFAULT,
|
||||
Extension: extension1,
|
||||
DisplayName: "test1",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_FILE,
|
||||
Id: "beep",
|
||||
},
|
||||
Settings: []*settingsmsg.Setting{
|
||||
{
|
||||
Id: setting1,
|
||||
Description: "test-desc-1",
|
||||
DisplayName: "test-displayname-1",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_FILE,
|
||||
Id: "bleep",
|
||||
},
|
||||
Value: &settingsmsg.Setting_IntValue{
|
||||
IntValue: &settingsmsg.Int{
|
||||
Min: 0,
|
||||
Max: 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "generic-test-system-resource",
|
||||
bundle: &settingsmsg.Bundle{
|
||||
Id: bundle2,
|
||||
Type: settingsmsg.Bundle_TYPE_DEFAULT,
|
||||
Extension: extension2,
|
||||
DisplayName: "test1",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SYSTEM,
|
||||
},
|
||||
Settings: []*settingsmsg.Setting{
|
||||
{
|
||||
Id: setting2,
|
||||
Description: "test-desc-2",
|
||||
DisplayName: "test-displayname-2",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SYSTEM,
|
||||
},
|
||||
Value: &settingsmsg.Setting_IntValue{
|
||||
IntValue: &settingsmsg.Int{
|
||||
Min: 0,
|
||||
Max: 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "generic-test-role-bundle",
|
||||
bundle: &settingsmsg.Bundle{
|
||||
Id: bundle3,
|
||||
Type: settingsmsg.Bundle_TYPE_ROLE,
|
||||
Extension: extension1,
|
||||
DisplayName: "Role1",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SYSTEM,
|
||||
},
|
||||
Settings: []*settingsmsg.Setting{
|
||||
{
|
||||
Id: setting3,
|
||||
Description: "test-desc-3",
|
||||
DisplayName: "test-displayname-3",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SETTING,
|
||||
Id: setting1,
|
||||
},
|
||||
Value: &settingsmsg.Setting_PermissionValue{
|
||||
PermissionValue: &settingsmsg.Permission{
|
||||
Operation: settingsmsg.Permission_OPERATION_READ,
|
||||
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
appendTestBundleID = uuid.NewString()
|
||||
|
||||
appendTestSetting1 = &settingsmsg.Setting{
|
||||
Id: "append-test-setting-1",
|
||||
Description: "test-desc-3",
|
||||
DisplayName: "test-displayname-3",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SETTING,
|
||||
Id: setting1,
|
||||
},
|
||||
Value: &settingsmsg.Setting_PermissionValue{
|
||||
PermissionValue: &settingsmsg.Permission{
|
||||
Operation: settingsmsg.Permission_OPERATION_READ,
|
||||
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
appendTestSetting2 = &settingsmsg.Setting{
|
||||
Id: "append-test-setting-2",
|
||||
Description: "test-desc-3",
|
||||
DisplayName: "test-displayname-3",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SETTING,
|
||||
Id: setting1,
|
||||
},
|
||||
Value: &settingsmsg.Setting_PermissionValue{
|
||||
PermissionValue: &settingsmsg.Permission{
|
||||
Operation: settingsmsg.Permission_OPERATION_READ,
|
||||
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestBundles(t *testing.T) {
|
||||
s := initStore()
|
||||
for i := range bundleScenarios {
|
||||
b := bundleScenarios[i]
|
||||
t.Run(b.name, func(t *testing.T) {
|
||||
_, err := s.WriteBundle(b.bundle)
|
||||
require.NoError(t, err)
|
||||
bundle, err := s.ReadBundle(b.bundle.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, b.bundle, bundle)
|
||||
})
|
||||
}
|
||||
|
||||
// check that ListBundles only returns bundles with type DEFAULT
|
||||
bundles, err := s.ListBundles(settingsmsg.Bundle_TYPE_DEFAULT, []string{})
|
||||
require.NoError(t, err)
|
||||
for i := range bundles {
|
||||
require.Equal(t, settingsmsg.Bundle_TYPE_DEFAULT, bundles[i].Type)
|
||||
}
|
||||
|
||||
// check that ListBundles filtered by an id only returns that bundle
|
||||
filteredBundles, err := s.ListBundles(settingsmsg.Bundle_TYPE_DEFAULT, []string{bundle2})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(filteredBundles))
|
||||
if len(filteredBundles) == 1 {
|
||||
require.Equal(t, bundle2, filteredBundles[0].Id)
|
||||
}
|
||||
|
||||
// check that ListRoles only returns bundles with type ROLE
|
||||
roles, err := s.ListBundles(settingsmsg.Bundle_TYPE_ROLE, []string{})
|
||||
require.NoError(t, err)
|
||||
for i := range roles {
|
||||
require.Equal(t, settingsmsg.Bundle_TYPE_ROLE, roles[i].Type)
|
||||
}
|
||||
|
||||
// check that ReadSetting works
|
||||
setting, err := s.ReadSetting(setting1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test-desc-1", setting.Description) // could be tested better ;)
|
||||
}
|
||||
|
||||
func TestAppendSetting(t *testing.T) {
|
||||
s := initStore()
|
||||
setupRoles(s)
|
||||
|
||||
// appending to non existing bundle creates new
|
||||
_, err := s.AddSettingToBundle(appendTestBundleID, appendTestSetting1)
|
||||
require.NoError(t, err)
|
||||
|
||||
b, err := s.ReadBundle(appendTestBundleID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.Settings, 1)
|
||||
|
||||
_, err = s.AddSettingToBundle(appendTestBundleID, appendTestSetting2)
|
||||
require.NoError(t, err)
|
||||
|
||||
b, err = s.ReadBundle(appendTestBundleID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.Settings, 2)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
|
||||
olog "github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/settings/pkg/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/store"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// CachedMDC is cache for the metadataclient
|
||||
type CachedMDC struct {
|
||||
cfg *config.Config
|
||||
logger olog.Logger
|
||||
next MetadataClient
|
||||
|
||||
filesCache microstore.Store
|
||||
dirsCache microstore.Store
|
||||
}
|
||||
|
||||
// SimpleDownload caches the answer from SimpleDownload or returns the cached one
|
||||
func (c *CachedMDC) SimpleDownload(ctx context.Context, id string) ([]byte, error) {
|
||||
if b, err := c.filesCache.Read(id); err == nil && len(b) == 1 {
|
||||
return b[0].Value, nil
|
||||
}
|
||||
b, err := c.next.SimpleDownload(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.filesCache.Write(µstore.Record{
|
||||
Key: id,
|
||||
Value: b,
|
||||
Expiry: c.cfg.Metadata.Cache.TTL,
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("SimpleDownload: failed to update to files cache")
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// SimpleUpload caches the answer from SimpleUpload and invalidates the cache
|
||||
func (c *CachedMDC) SimpleUpload(ctx context.Context, id string, content []byte) error {
|
||||
b, err := c.filesCache.Read(id)
|
||||
if err == nil && len(b) == 1 && string(b[0].Value) == string(content) {
|
||||
// no need to bug mdc
|
||||
return nil
|
||||
}
|
||||
|
||||
err = c.next.SimpleUpload(ctx, id, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// invalidate caches
|
||||
if err = c.dirsCache.Delete(path.Dir(id)); err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to clear dirs cache")
|
||||
}
|
||||
|
||||
err = c.filesCache.Write(µstore.Record{
|
||||
Key: id,
|
||||
Value: content,
|
||||
Expiry: c.cfg.Metadata.Cache.TTL,
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("SimpleUpload: failed to update to files cache")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete invalidates the cache when operation was successful
|
||||
func (c *CachedMDC) Delete(ctx context.Context, id string) error {
|
||||
if err := c.next.Delete(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// invalidate caches
|
||||
_ = c.removePrefix(c.filesCache, id)
|
||||
_ = c.removePrefix(c.dirsCache, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadDir caches the response from ReadDir or returnes the cached one
|
||||
func (c *CachedMDC) ReadDir(ctx context.Context, id string) ([]string, error) {
|
||||
i, err := c.dirsCache.Read(id)
|
||||
if err == nil && len(i) == 1 {
|
||||
var ret []string
|
||||
if err = msgpack.Unmarshal(i[0].Value, &ret); err == nil {
|
||||
return ret, nil
|
||||
}
|
||||
c.logger.Error().Err(err).Msg("failed to unmarshal entry from dirs cache")
|
||||
}
|
||||
|
||||
s, err := c.next.ReadDir(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var value []byte
|
||||
if value, err = msgpack.Marshal(s); err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to marshal ReadDir result for dirs cache")
|
||||
return s, err
|
||||
}
|
||||
err = c.dirsCache.Write(µstore.Record{
|
||||
Key: id,
|
||||
Value: value,
|
||||
Expiry: c.cfg.Metadata.Cache.TTL,
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("ReadDir: failed to update dirs cache")
|
||||
}
|
||||
|
||||
return s, err
|
||||
}
|
||||
|
||||
// MakeDirIfNotExist invalidates the cache
|
||||
func (c *CachedMDC) MakeDirIfNotExist(ctx context.Context, id string) error {
|
||||
err := c.next.MakeDirIfNotExist(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// invalidate caches
|
||||
if err = c.dirsCache.Delete(path.Dir(id)); err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to clear dirs cache")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init instantiates the caches
|
||||
func (c *CachedMDC) Init(ctx context.Context, id string) error {
|
||||
c.dirsCache = store.Create(
|
||||
store.Store(c.cfg.Metadata.Cache.Store),
|
||||
store.TTL(c.cfg.Metadata.Cache.TTL),
|
||||
microstore.Nodes(c.cfg.Metadata.Cache.Nodes...),
|
||||
microstore.Database(c.cfg.Metadata.Cache.Database),
|
||||
microstore.Table(c.cfg.Metadata.Cache.DirectoryTable),
|
||||
store.DisablePersistence(c.cfg.Metadata.Cache.DisablePersistence),
|
||||
store.Authentication(c.cfg.Metadata.Cache.AuthUsername, c.cfg.Metadata.Cache.AuthPassword),
|
||||
)
|
||||
c.filesCache = store.Create(
|
||||
store.Store(c.cfg.Metadata.Cache.Store),
|
||||
store.TTL(c.cfg.Metadata.Cache.TTL),
|
||||
microstore.Nodes(c.cfg.Metadata.Cache.Nodes...),
|
||||
microstore.Database(c.cfg.Metadata.Cache.Database),
|
||||
microstore.Table(c.cfg.Metadata.Cache.FileTable),
|
||||
store.DisablePersistence(c.cfg.Metadata.Cache.DisablePersistence),
|
||||
store.Authentication(c.cfg.Metadata.Cache.AuthUsername, c.cfg.Metadata.Cache.AuthPassword),
|
||||
)
|
||||
return c.next.Init(ctx, id)
|
||||
}
|
||||
|
||||
func (c *CachedMDC) removePrefix(cache microstore.Store, prefix string) error {
|
||||
c.logger.Debug().Str("prefix", prefix).Msg("removePrefix")
|
||||
keys, err := cache.List(microstore.ListPrefix(prefix))
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to list cache entries")
|
||||
}
|
||||
for _, k := range keys {
|
||||
c.logger.Debug().Str("key", k).Msg("removePrefix")
|
||||
if err := cache.Delete(k); err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to remove prefix from cache")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/settings"
|
||||
"github.com/qsfera/server/services/settings/pkg/util"
|
||||
)
|
||||
|
||||
// ListPermissionsByResource collects all permissions from the provided roleIDs that match the requested resource
|
||||
func (s *Store) ListPermissionsByResource(resource *settingsmsg.Resource, roleIDs []string) ([]*settingsmsg.Permission, error) {
|
||||
records := make([]*settingsmsg.Permission, 0)
|
||||
for _, roleID := range roleIDs {
|
||||
role, err := s.ReadBundle(roleID)
|
||||
if err != nil {
|
||||
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
|
||||
continue
|
||||
}
|
||||
records = append(records, extractPermissionsByResource(resource, role)...)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// ReadPermissionByID finds the permission in the roles, specified by the provided roleIDs
|
||||
func (s *Store) ReadPermissionByID(permissionID string, roleIDs []string) (*settingsmsg.Permission, error) {
|
||||
for _, roleID := range roleIDs {
|
||||
role, err := s.ReadBundle(roleID)
|
||||
if err != nil {
|
||||
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
|
||||
continue
|
||||
}
|
||||
for _, permission := range role.Settings {
|
||||
if permission.Id == permissionID {
|
||||
if value, ok := permission.Value.(*settingsmsg.Setting_PermissionValue); ok {
|
||||
return value.PermissionValue, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ReadPermissionByName finds the permission in the roles, specified by the provided roleIDs
|
||||
func (s *Store) ReadPermissionByName(name string, roleIDs []string) (*settingsmsg.Permission, error) {
|
||||
for _, roleID := range roleIDs {
|
||||
role, err := s.ReadBundle(roleID)
|
||||
if err != nil {
|
||||
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
|
||||
continue
|
||||
}
|
||||
for _, permission := range role.Settings {
|
||||
if permission.Name == name {
|
||||
if value, ok := permission.Value.(*settingsmsg.Setting_PermissionValue); ok {
|
||||
return value.PermissionValue, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, settings.ErrNotFound
|
||||
}
|
||||
|
||||
// extractPermissionsByResource collects all permissions from the provided role that match the requested resource
|
||||
func extractPermissionsByResource(resource *settingsmsg.Resource, role *settingsmsg.Bundle) []*settingsmsg.Permission {
|
||||
permissions := make([]*settingsmsg.Permission, 0)
|
||||
for _, setting := range role.Settings {
|
||||
if value, ok := setting.Value.(*settingsmsg.Setting_PermissionValue); ok {
|
||||
if util.IsResourceMatched(setting.Resource, resource) {
|
||||
permissions = append(permissions, value.PermissionValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return permissions
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPermission(t *testing.T) {
|
||||
s := initStore()
|
||||
setupRoles(s)
|
||||
// bunldes are initialized within init func
|
||||
p, err := s.ReadPermissionByID("readID", []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, settingsmsg.Permission_OPERATION_READ, p.Operation)
|
||||
|
||||
p, err = s.ReadPermissionByName("read", []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, settingsmsg.Permission_OPERATION_READ, p.Operation)
|
||||
|
||||
pms, err := s.ListPermissionsByResource(&settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_BUNDLE,
|
||||
}, []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pms, 1)
|
||||
require.Equal(t, settingsmsg.Permission_OPERATION_READ, pms[0].Operation)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// Package store implements the go-micro store interface
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
olog "github.com/qsfera/server/pkg/log"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/config"
|
||||
"github.com/qsfera/server/services/settings/pkg/settings"
|
||||
"github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
// Name is the default name for the settings store
|
||||
Name = "qsfera-settings"
|
||||
managerName = "metadata"
|
||||
settingsSpaceID = "f1bdd61a-da7c-49fc-8203-0558109d1b4f" // uuid.NewString()
|
||||
rootFolderLocation = "settings"
|
||||
bundleFolderLocation = "settings/bundles"
|
||||
accountsFolderLocation = "settings/accounts"
|
||||
valuesFolderLocation = "settings/values"
|
||||
)
|
||||
|
||||
// MetadataClient is the interface to talk to metadata service
|
||||
type MetadataClient interface {
|
||||
SimpleDownload(ctx context.Context, id string) ([]byte, error)
|
||||
SimpleUpload(ctx context.Context, id string, content []byte) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
ReadDir(ctx context.Context, id string) ([]string, error)
|
||||
MakeDirIfNotExist(ctx context.Context, id string) error
|
||||
Init(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// Store interacts with the filesystem to manage settings information
|
||||
type Store struct {
|
||||
Logger olog.Logger
|
||||
|
||||
mdc MetadataClient
|
||||
cfg *config.Config
|
||||
|
||||
l *sync.Mutex
|
||||
}
|
||||
|
||||
// Init initialize the store once, later calls are noops
|
||||
func (s *Store) Init() {
|
||||
if s.mdc != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
|
||||
if s.mdc != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mdc := &CachedMDC{
|
||||
next: NewMetadataClient(s.cfg.Metadata),
|
||||
cfg: s.cfg,
|
||||
logger: s.Logger,
|
||||
}
|
||||
if err := s.initMetadataClient(mdc); err != nil {
|
||||
s.Logger.Error().Err(err).Msg("error initializing metadata client")
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new store
|
||||
func New(cfg *config.Config) settings.Manager {
|
||||
s := Store{
|
||||
Logger: olog.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel),
|
||||
cfg: cfg,
|
||||
l: &sync.Mutex{},
|
||||
}
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
// NewMetadataClient returns the MetadataClient
|
||||
func NewMetadataClient(cfg config.Metadata) MetadataClient {
|
||||
mdc, err := metadata.NewCS3Storage(cfg.GatewayAddress, cfg.StorageAddress, cfg.SystemUserID, cfg.SystemUserIDP, cfg.SystemUserAPIKey)
|
||||
if err != nil {
|
||||
log.Fatal("error connecting to mdc:", err)
|
||||
}
|
||||
return mdc
|
||||
|
||||
}
|
||||
|
||||
// we need to lazy initialize the MetadataClient because metadata service might not be ready
|
||||
func (s *Store) initMetadataClient(mdc MetadataClient) error {
|
||||
ctx := context.TODO()
|
||||
err := mdc.Init(ctx, settingsSpaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range []string{
|
||||
rootFolderLocation,
|
||||
accountsFolderLocation,
|
||||
bundleFolderLocation,
|
||||
valuesFolderLocation,
|
||||
} {
|
||||
err = mdc.MakeDirIfNotExist(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range s.cfg.Bundles {
|
||||
b, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = mdc.SimpleUpload(ctx, bundlePath(p.Id), b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range defaults.DefaultRoleAssignments(s.cfg) {
|
||||
accountUUID := p.AccountUuid
|
||||
roleID := p.RoleId
|
||||
err = mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assIDs, err := mdc.ReadDir(ctx, accountPath(accountUUID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adminUserID := accountUUID == s.cfg.AdminUserID
|
||||
if len(assIDs) > 0 && !adminUserID {
|
||||
// There is already a role assignment for this ID, skip to the next
|
||||
continue
|
||||
}
|
||||
// for the adminUserID we need to check if the user has the admin role every time
|
||||
if adminUserID {
|
||||
err = s.userMustHaveAdminRole(accountUUID, assIDs, mdc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ass := &settingsmsg.UserRoleAssignment{
|
||||
Id: uuid.NewString(),
|
||||
AccountUuid: accountUUID,
|
||||
RoleId: roleID,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(ass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = mdc.SimpleUpload(ctx, assignmentPath(accountUUID, ass.Id), b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.mdc = mdc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) userMustHaveAdminRole(accountUUID string, assIDs []string, mdc MetadataClient) error {
|
||||
ctx := context.TODO()
|
||||
var hasAdminRole bool
|
||||
|
||||
// load the assignments from the store and check if the admin role is already assigned
|
||||
for _, assID := range assIDs {
|
||||
b, err := mdc.SimpleDownload(ctx, assignmentPath(accountUUID, assID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
a := &settingsmsg.UserRoleAssignment{}
|
||||
err = json.Unmarshal(b, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.RoleId == defaults.BundleUUIDRoleAdmin {
|
||||
hasAdminRole = true
|
||||
}
|
||||
}
|
||||
|
||||
// delete old role assignment and set admin role
|
||||
if !hasAdminRole {
|
||||
err := mdc.Delete(ctx, accountPath(accountUUID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
// already gone, continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
err = mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ass := &settingsmsg.UserRoleAssignment{
|
||||
Id: uuid.NewString(),
|
||||
AccountUuid: accountUUID,
|
||||
RoleId: defaults.BundleUUIDRoleAdmin,
|
||||
}
|
||||
b, err := json.Marshal(ass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mdc.SimpleUpload(ctx, assignmentPath(accountUUID, ass.Id), b)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
settings.Registry[managerName] = New
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/config/defaults"
|
||||
rdefaults "github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
)
|
||||
|
||||
const (
|
||||
// account UUIDs
|
||||
accountUUID1 = "c4572da7-6142-4383-8fc6-efde3d463036"
|
||||
//accountUUID2 = "e11f9769-416a-427d-9441-41a0e51391d7"
|
||||
//accountUUID3 = "633ecd77-1980-412a-8721-bf598a330bb4"
|
||||
|
||||
// extension names
|
||||
extension1 = "test-extension-1"
|
||||
extension2 = "test-extension-2"
|
||||
|
||||
// bundle ids
|
||||
bundle1 = "2f06addf-4fd2-49d5-8f71-00fbd3a3ec47"
|
||||
bundle2 = "2d745744-749c-4286-8e92-74a24d8331c5"
|
||||
bundle3 = "d8fd27d1-c00b-4794-a658-416b756a72ff"
|
||||
|
||||
// setting ids
|
||||
setting1 = "c7ebbc8b-d15a-4f2e-9d7d-d6a4cf858d1a"
|
||||
setting2 = "3fd9a3d9-20b7-40d4-9294-b22bb5868c10"
|
||||
setting3 = "24bb9535-3df4-42f1-a622-7c0562bec99f"
|
||||
|
||||
// value ids
|
||||
value1 = "fd3b6221-dc13-4a22-824d-2480495f1cdb"
|
||||
value2 = "2a0bd9b0-ca1d-491a-8c56-d2ddfd68ded8"
|
||||
value3 = "b42702d2-5e4d-4d73-b133-e1f9e285355e"
|
||||
)
|
||||
|
||||
// use "unit" or "integration" do define test type. You need a running КуСфера instance for integration tests
|
||||
var testtype = "unit"
|
||||
|
||||
// MockedMetadataClient mocks the metadataservice inmemory
|
||||
type MockedMetadataClient struct {
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
// NewMDC instantiates a mocked MetadataClient
|
||||
func NewMDC(s *Store) error {
|
||||
var mdc MetadataClient
|
||||
switch testtype {
|
||||
case "unit":
|
||||
mdc = &MockedMetadataClient{data: make(map[string][]byte)}
|
||||
case "integration":
|
||||
mdc = NewMetadataClient(defaults.DefaultConfig().Metadata)
|
||||
}
|
||||
return s.initMetadataClient(mdc)
|
||||
}
|
||||
|
||||
// SimpleDownload returns errtypes.NotFound if not found
|
||||
func (m *MockedMetadataClient) SimpleDownload(_ context.Context, id string) ([]byte, error) {
|
||||
if data, ok := m.data[id]; ok {
|
||||
return data, nil
|
||||
}
|
||||
return nil, errtypes.NotFound("not found")
|
||||
}
|
||||
|
||||
// SimpleUpload can't error
|
||||
func (m *MockedMetadataClient) SimpleUpload(_ context.Context, id string, content []byte) error {
|
||||
m.data[id] = content
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete can't error either
|
||||
func (m *MockedMetadataClient) Delete(_ context.Context, id string) error {
|
||||
for k := range m.data {
|
||||
if strings.HasPrefix(k, id) {
|
||||
delete(m.data, k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadDir returns nil, nil if not found
|
||||
func (m *MockedMetadataClient) ReadDir(_ context.Context, id string) ([]string, error) {
|
||||
var out []string
|
||||
for k := range m.data {
|
||||
if strings.HasPrefix(k, id) {
|
||||
dir := strings.TrimPrefix(k, id+"/")
|
||||
// filter subfolders the lame way
|
||||
s := strings.Trim(strings.SplitAfter(dir, "/")[0], "/")
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MakeDirIfNotExist does nothing
|
||||
func (*MockedMetadataClient) MakeDirIfNotExist(_ context.Context, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init does nothing
|
||||
func (*MockedMetadataClient) Init(_ context.Context, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IDExists is a helper to check if an id exists
|
||||
func (m *MockedMetadataClient) IDExists(id string) bool {
|
||||
_, ok := m.data[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
// IDHasContent returns true if the value stored under id has the given content (converted to string)
|
||||
func (m *MockedMetadataClient) IDHasContent(id string, content []byte) bool {
|
||||
return string(m.data[id]) == string(content)
|
||||
}
|
||||
|
||||
// TestAdminUserIDInit test the happy path during initialization
|
||||
func TestAdminUserIDInit(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
s := &Store{
|
||||
cfg: defaults.DefaultConfig(),
|
||||
}
|
||||
s.cfg.Bundles = rdefaults.GenerateBundlesDefaultRoles()
|
||||
s.cfg.AdminUserID = "admin"
|
||||
|
||||
// the first assignment is always happening during the initialisation of the metadata client
|
||||
err := NewMDC(s)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
assID, err := s.mdc.ReadDir(context.TODO(), accountPath(s.cfg.AdminUserID))
|
||||
Expect(len(assID)).To(Equal(1))
|
||||
ass, err := s.mdc.SimpleDownload(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]))
|
||||
Expect(ass).ToNot(BeNil())
|
||||
|
||||
assignment := &settingsmsg.UserRoleAssignment{}
|
||||
err = json.Unmarshal(ass, assignment)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(assignment.RoleId).To(Equal(rdefaults.BundleUUIDRoleAdmin))
|
||||
}
|
||||
|
||||
// TestAdminUserIDUpdate test the update on following initialisations
|
||||
func TestAdminUserIDUpdate(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
s := &Store{
|
||||
cfg: defaults.DefaultConfig(),
|
||||
}
|
||||
s.cfg.Bundles = rdefaults.GenerateBundlesDefaultRoles()
|
||||
s.cfg.AdminUserID = "admin"
|
||||
|
||||
// the first assignment is always happening during the initialisation of the metadata client
|
||||
err := NewMDC(s)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// read assignment
|
||||
assID, err := s.mdc.ReadDir(context.TODO(), accountPath(s.cfg.AdminUserID))
|
||||
Expect(len(assID)).To(Equal(1))
|
||||
|
||||
// set assignment to user role
|
||||
userRoleAssignment := &settingsmsg.UserRoleAssignment{
|
||||
AccountUuid: s.cfg.AdminUserID,
|
||||
RoleId: rdefaults.BundleUUIDRoleUser,
|
||||
}
|
||||
b, err := json.Marshal(userRoleAssignment)
|
||||
err = s.mdc.Delete(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]))
|
||||
Expect(err).To(BeNil())
|
||||
err = s.mdc.SimpleUpload(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]), b)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// this happens on every Read / Write on the store
|
||||
// the actual init is only done if the metadata client has not been initialized before
|
||||
// this normally needs a restart of the service
|
||||
err = s.initMetadataClient(s.mdc)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// read assignment id, changes every time the assignment is written
|
||||
assID, err = s.mdc.ReadDir(context.TODO(), accountPath(s.cfg.AdminUserID))
|
||||
Expect(len(assID)).To(Equal(1))
|
||||
|
||||
// check if the assignment is the admin role again
|
||||
ass, err := s.mdc.SimpleDownload(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]))
|
||||
Expect(ass).ToNot(BeNil())
|
||||
assignment := &settingsmsg.UserRoleAssignment{}
|
||||
err = json.Unmarshal(ass, assignment)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(assignment.RoleId).To(Equal(rdefaults.BundleUUIDRoleAdmin))
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Package store implements the go-micro store interface
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/settings"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
)
|
||||
|
||||
// ListValues reads all values that match the given bundleId and accountUUID.
|
||||
// If the bundleId is empty, it's ignored for filtering.
|
||||
// If the accountUUID is empty, only values with empty accountUUID are returned.
|
||||
// If the accountUUID is not empty, values with an empty or with a matching accountUUID are returned.
|
||||
func (s *Store) ListValues(bundleID, accountUUID string) ([]*settingsmsg.Value, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
vIDs, err := s.mdc.ReadDir(ctx, valuesFolderLocation)
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return make([]*settingsmsg.Value, 0), nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: refine logic not to spam metadata service
|
||||
var values []*settingsmsg.Value
|
||||
for _, vid := range vIDs {
|
||||
b, err := s.mdc.SimpleDownload(ctx, valuePath(vid))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
continue
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := &settingsmsg.Value{}
|
||||
err = json.Unmarshal(b, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bundleID != "" && v.BundleId != bundleID {
|
||||
continue
|
||||
}
|
||||
|
||||
if v.AccountUuid == "" {
|
||||
values = append(values, v)
|
||||
continue
|
||||
}
|
||||
|
||||
if v.AccountUuid == accountUUID {
|
||||
values = append(values, v)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// ReadValue tries to find a value by the given valueId within the dataPath
|
||||
func (s *Store) ReadValue(valueID string) (*settingsmsg.Value, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
b, err := s.mdc.SimpleDownload(ctx, valuePath(valueID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return nil, fmt.Errorf("valueID '%s' %w", valueID, settings.ErrNotFound)
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
val := &settingsmsg.Value{}
|
||||
return val, json.Unmarshal(b, val)
|
||||
}
|
||||
|
||||
// ReadValueByUniqueIdentifiers tries to find a value given a set of unique identifiers
|
||||
func (s *Store) ReadValueByUniqueIdentifiers(accountUUID, settingID string) (*settingsmsg.Value, error) {
|
||||
if settingID == "" {
|
||||
return nil, fmt.Errorf("settingID can not be empty %w", settings.ErrNotFound)
|
||||
}
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
vIDs, err := s.mdc.ReadDir(ctx, valuesFolderLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, vid := range vIDs {
|
||||
b, err := s.mdc.SimpleDownload(ctx, valuePath(vid))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
continue
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := &settingsmsg.Value{}
|
||||
err = json.Unmarshal(b, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v.AccountUuid == accountUUID && v.SettingId == settingID {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return nil, settings.ErrNotFound
|
||||
}
|
||||
|
||||
// WriteValue writes the given value into a file within the dataPath
|
||||
func (s *Store) WriteValue(value *settingsmsg.Value) (*settingsmsg.Value, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
if value.Id == "" {
|
||||
value.Id = uuid.NewString()
|
||||
}
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return value, s.mdc.SimpleUpload(ctx, valuePath(value.Id), b)
|
||||
}
|
||||
|
||||
func valuePath(id string) string {
|
||||
return fmt.Sprintf("%s/%s", valuesFolderLocation, id)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var valueScenarios = []struct {
|
||||
name string
|
||||
value *settingsmsg.Value
|
||||
}{
|
||||
{
|
||||
name: "generic-test-with-system-resource",
|
||||
value: &settingsmsg.Value{
|
||||
Id: value1,
|
||||
BundleId: bundle1,
|
||||
SettingId: setting1,
|
||||
AccountUuid: accountUUID1,
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SYSTEM,
|
||||
},
|
||||
Value: &settingsmsg.Value_StringValue{
|
||||
StringValue: "lalala",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "generic-test-with-file-resource",
|
||||
value: &settingsmsg.Value{
|
||||
Id: value2,
|
||||
BundleId: bundle2,
|
||||
SettingId: setting2,
|
||||
AccountUuid: accountUUID1,
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_FILE,
|
||||
Id: "adfba82d-919a-41c3-9cd1-5a3f83b2bf76",
|
||||
},
|
||||
Value: &settingsmsg.Value_StringValue{
|
||||
StringValue: "tralala",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "value without accountUUID",
|
||||
value: &settingsmsg.Value{
|
||||
Id: value3,
|
||||
BundleId: bundle3,
|
||||
SettingId: setting2,
|
||||
AccountUuid: "",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_FILE,
|
||||
Id: "adfba82d-919a-41c3-9cd1-5a3f83b2bf76",
|
||||
},
|
||||
Value: &settingsmsg.Value_StringValue{
|
||||
StringValue: "tralala",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestValues(t *testing.T) {
|
||||
for i := range valueScenarios {
|
||||
index := i
|
||||
t.Run(valueScenarios[index].name, func(t *testing.T) {
|
||||
s := initStore()
|
||||
setupRoles(s)
|
||||
value := valueScenarios[index].value
|
||||
v, err := s.WriteValue(value)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, value, v)
|
||||
|
||||
v, err = s.ReadValue(value.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, value, v)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListValues(t *testing.T) {
|
||||
s := initStore()
|
||||
setupRoles(s)
|
||||
for _, v := range valueScenarios {
|
||||
_, err := s.WriteValue(v.value)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// empty accountid returns only values with empty accountud
|
||||
vs, err := s.ListValues("", "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, vs, 1)
|
||||
|
||||
// filled accountid returns matching and empty accountUUID values
|
||||
vs, err = s.ListValues("", accountUUID1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, vs, 3)
|
||||
|
||||
// filled bundleid only returns matching values
|
||||
vs, err = s.ListValues(bundle3, accountUUID1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, vs, 1)
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user