diff --git a/accounts/pkg/config/defaults/defaultconfig.go b/accounts/pkg/config/defaults/defaultconfig.go index bf62637ece3..8e8a82266f3 100644 --- a/accounts/pkg/config/defaults/defaultconfig.go +++ b/accounts/pkg/config/defaults/defaultconfig.go @@ -71,7 +71,7 @@ func DefaultConfig() *config.Config { }, ServiceUser: config.ServiceUser{ UUID: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad", - Username: "", + Username: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad", UID: 0, GID: 0, }, diff --git a/accounts/pkg/service/v0/accounts.go b/accounts/pkg/service/v0/accounts.go index bd28223106e..7fab6a7d187 100644 --- a/accounts/pkg/service/v0/accounts.go +++ b/accounts/pkg/service/v0/accounts.go @@ -99,33 +99,6 @@ func (s Service) hasSelfManagementPermissions(ctx context.Context) bool { return s.RoleManager.FindPermissionByID(ctx, roleIDs, SelfManagementPermissionID) != nil } -// serviceUserToIndex temporarily adds a service user to the index, which is supposed to be removed before the lock on the handler function is released -func (s Service) serviceUserToIndex() (teardownServiceUser func()) { - if s.Config.ServiceUser.Username != "" && s.Config.ServiceUser.UUID != "" { - _, err := s.index.Add(s.getInMemoryServiceUser()) - if err != nil { - s.log.Logger.Err(err).Msg("service user was configured but failed to be added to the index") - } else { - return func() { - _ = s.index.Delete(s.getInMemoryServiceUser()) - } - } - } - return func() {} -} - -func (s Service) getInMemoryServiceUser() accountsmsg.Account { - return accountsmsg.Account{ - AccountEnabled: true, - Id: s.Config.ServiceUser.UUID, - PreferredName: s.Config.ServiceUser.Username, - OnPremisesSamAccountName: s.Config.ServiceUser.Username, - DisplayName: s.Config.ServiceUser.Username, - UidNumber: s.Config.ServiceUser.UID, - GidNumber: s.Config.ServiceUser.GID, - } -} - // ListAccounts implements the AccountsServiceHandler interface // the query contains account properties func (s Service) ListAccounts(ctx context.Context, in *accountssvc.ListAccountsRequest, out *accountssvc.ListAccountsResponse) (err error) { @@ -145,8 +118,6 @@ func (s Service) ListAccounts(ctx context.Context, in *accountssvc.ListAccountsR } onlySelf := hasSelf && !hasManagement - teardownServiceUser := s.serviceUserToIndex() - defer teardownServiceUser() match, authRequest := getAuthQueryMatch(in.Query) if authRequest { password := match[2] diff --git a/accounts/pkg/service/v0/service.go b/accounts/pkg/service/v0/service.go index f709b70f20b..7c19e26618e 100644 --- a/accounts/pkg/service/v0/service.go +++ b/accounts/pkg/service/v0/service.go @@ -86,9 +86,33 @@ func New(opts ...Option) (s *Service, err error) { if err = s.createDefaultGroups(cfg.DemoUsersAndGroups); err != nil { return nil, err } + + s.serviceUserToIndex() return } +// serviceUserToIndex temporarily adds a service user to the index, which is supposed to be removed before the lock on the handler function is released +func (s Service) serviceUserToIndex() { + if s.Config.ServiceUser.Username != "" && s.Config.ServiceUser.UUID != "" { + _, err := s.index.Add(s.getInMemoryServiceUser()) + if err != nil { + s.log.Logger.Err(err).Msg("service user was configured but failed to be added to the index") + } + } +} + +func (s Service) getInMemoryServiceUser() accountsmsg.Account { + return accountsmsg.Account{ + AccountEnabled: true, + Id: s.Config.ServiceUser.UUID, + PreferredName: s.Config.ServiceUser.Username, + OnPremisesSamAccountName: s.Config.ServiceUser.Username, + DisplayName: s.Config.ServiceUser.Username, + UidNumber: s.Config.ServiceUser.UID, + GidNumber: s.Config.ServiceUser.GID, + } +} + func (s Service) buildIndex() (*indexer.Indexer, error) { var indexcfg *idxcfg.Config diff --git a/changelog/unreleased/store-settings-in-metadata-service.md b/changelog/unreleased/store-settings-in-metadata-service.md new file mode 100644 index 00000000000..16fd2340821 --- /dev/null +++ b/changelog/unreleased/store-settings-in-metadata-service.md @@ -0,0 +1,5 @@ +Change: settings service now stores its data via metadata service + +Instead of writing files to disk it will use metadata service to do so + +https://github.com/owncloud/ocis/pull/3232 diff --git a/ocis-pkg/roles/manager.go b/ocis-pkg/roles/manager.go index 1878db0200a..6c58239ed75 100644 --- a/ocis-pkg/roles/manager.go +++ b/ocis-pkg/roles/manager.go @@ -46,6 +46,7 @@ func (m *Manager) List(ctx context.Context, roleIDs []string) []*settingsmsg.Bun res, err := m.roleService.ListRoles(ctx, request) if err != nil { m.logger.Debug().Err(err).Msg("failed to fetch roles by roleIDs") + return nil } for _, role := range res.Bundles { m.cache.set(role.Id, role) diff --git a/settings/pkg/config/config.go b/settings/pkg/config/config.go index ef0e3d31a9d..4ee5d895d3a 100644 --- a/settings/pkg/config/config.go +++ b/settings/pkg/config/config.go @@ -19,7 +19,10 @@ type Config struct { HTTP HTTP `ocisConfig:"http"` GRPC GRPC `ocisConfig:"grpc"` - DataPath string `ocisConfig:"data_path" env:"SETTINGS_DATA_PATH"` + StoreType string `ocisConfig:"store_type" env:"SETTINGS_STORE_TYPE"` + DataPath string `ocisConfig:"data_path" env:"SETTINGS_DATA_PATH"` + Metadata Metadata `ocisConfig:"metadata_config"` + Asset Asset `ocisConfig:"asset"` TokenManager TokenManager `ocisConfig:"token_manager"` @@ -30,3 +33,13 @@ type Config struct { type Asset struct { Path string `ocisConfig:"path" env:"SETTINGS_ASSET_PATH"` } + +// Metadata configures the metadata store to use +type Metadata struct { + GatewayAddress string `ocisConfig:"gateway_addr" env:"STORAGE_GATEWAY_GRPC_ADDR"` + StorageAddress string `ocisConfig:"storage_addr" env:"STORAGE_GRPC_ADDR"` + + ServiceUserID string `ocisConfig:"service_user_id" env:"METADATA_SERVICE_USER_UUID"` + ServiceUserIDP string `ocisConfig:"service_user_idp" env:"OCIS_URL;METADATA_SERVICE_USER_IDP"` + MachineAuthAPIKey string `ocisConfig:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY"` +} diff --git a/settings/pkg/config/defaults/defaultconfig.go b/settings/pkg/config/defaults/defaultconfig.go index e86f5b6b2bf..f7b9ead4c4c 100644 --- a/settings/pkg/config/defaults/defaultconfig.go +++ b/settings/pkg/config/defaults/defaultconfig.go @@ -17,6 +17,7 @@ func FullDefaultConfig() *config.Config { return cfg } +// DefaultConfig returns the default config func DefaultConfig() *config.Config { return &config.Config{ Service: config.Service{ @@ -44,13 +45,22 @@ func DefaultConfig() *config.Config { Addr: "127.0.0.1:9191", Namespace: "com.owncloud.api", }, - DataPath: path.Join(defaults.BaseDataPath(), "settings"), + StoreType: "metadata", // use metadata or filesystem + DataPath: path.Join(defaults.BaseDataPath(), "settings"), Asset: config.Asset{ Path: "", }, TokenManager: config.TokenManager{ JWTSecret: "Pive-Fumkiu4", }, + + Metadata: config.Metadata{ + GatewayAddress: "127.0.0.1:9142", + StorageAddress: "127.0.0.1:9215", + ServiceUserID: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad", + ServiceUserIDP: "https://localhost:9200", + MachineAuthAPIKey: "change-me-please", + }, } } diff --git a/settings/pkg/service/v0/service.go b/settings/pkg/service/v0/service.go index 2cdb42ec4d4..3d15a226dd2 100644 --- a/settings/pkg/service/v0/service.go +++ b/settings/pkg/service/v0/service.go @@ -15,7 +15,8 @@ import ( settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/settings/pkg/config" "github.com/owncloud/ocis/settings/pkg/settings" - store "github.com/owncloud/ocis/settings/pkg/store/filesystem" + filestore "github.com/owncloud/ocis/settings/pkg/store/filesystem" + metastore "github.com/owncloud/ocis/settings/pkg/store/metadata" merrors "go-micro.dev/v4/errors" "go-micro.dev/v4/metadata" "google.golang.org/protobuf/types/known/emptypb" @@ -32,12 +33,21 @@ type Service struct { // NewService returns a service implementation for Service. func NewService(cfg *config.Config, logger log.Logger) Service { service := Service{ - id: "ocis-settings", - config: cfg, - logger: logger, - manager: store.New(cfg), + id: "ocis-settings", + config: cfg, + logger: logger, + } + + switch cfg.StoreType { + default: + fallthrough + case "metadata": + service.manager = metastore.New(cfg) + case "filesystem": + service.manager = filestore.New(cfg) + // TODO: if we want to further support filesystem store it should use default permissions from store/defaults/defaults.go instead using this duplicate + service.RegisterDefaultRoles() } - service.RegisterDefaultRoles() return service } diff --git a/settings/pkg/store/defaults/defaults.go b/settings/pkg/store/defaults/defaults.go new file mode 100644 index 00000000000..c4a209b7b18 --- /dev/null +++ b/settings/pkg/store/defaults/defaults.go @@ -0,0 +1,456 @@ +package defaults + +import ( + settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0" +) + +const ( + // BundleUUIDRoleAdmin represents the admin role + BundleUUIDRoleAdmin = "71881883-1768-46bd-a24d-a356a2afdf7f" + + // BundleUUIDRoleUser represents the user role. + BundleUUIDRoleUser = "d7beeea8-8ff4-406b-8fb6-ab2dd81e6b11" + + // BundleUUIDRoleGuest represents the guest role. + BundleUUIDRoleGuest = "38071a68-456a-4553-846a-fa67bf5596cc" + + // BundleUUIDRoleMetadata represents the metadata user role + BundleUUIDRoleMetadata = "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad" + + // RoleManagementPermissionID is the hardcoded setting UUID for the role management permission + RoleManagementPermissionID string = "a53e601e-571f-4f86-8fec-d4576ef49c62" + // RoleManagementPermissionName is the hardcoded setting name for the role management permission + RoleManagementPermissionName string = "role-management" + + // SettingsManagementPermissionID is the hardcoded setting UUID for the settings management permission + SettingsManagementPermissionID string = "79e13b30-3e22-11eb-bc51-0b9f0bad9a58" + // SettingsManagementPermissionName is the hardcoded setting name for the settings management permission + SettingsManagementPermissionName string = "settings-management" + + // SetSpaceQuotaPermissionID is the hardcoded setting UUID for the set space quota permission + SetSpaceQuotaPermissionID string = "4e6f9709-f9e7-44f1-95d4-b762d27b7896" + // SetSpaceQuotaPermissionName is the hardcoded setting name for the set space quota permission + SetSpaceQuotaPermissionName string = "set-space-quota" + + // ListAllSpacesPermissionID is the hardcoded setting UUID for the list all spaces permission + ListAllSpacesPermissionID string = "016f6ddd-9501-4a0a-8ebe-64a20ee8ec82" + // ListAllSpacesPermissionName is the hardcoded setting name for the list all spaces permission + ListAllSpacesPermissionName string = "list-all-spaces" + + // CreateSpacePermissionID is the hardcoded setting UUID for the create space permission + CreateSpacePermissionID string = "79e13b30-3e22-11eb-bc51-0b9f0bad9a58" + // CreateSpacePermissionName is the hardcoded setting name for the create space permission + CreateSpacePermissionName string = "create-space" + + settingUUIDProfileLanguage = "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f" + + // AccountManagementPermissionID is the hardcoded setting UUID for the account management permission + AccountManagementPermissionID string = "8e587774-d929-4215-910b-a317b1e80f73" + // AccountManagementPermissionName is the hardcoded setting name for the account management permission + AccountManagementPermissionName string = "account-management" + // GroupManagementPermissionID is the hardcoded setting UUID for the group management permission + GroupManagementPermissionID string = "522adfbe-5908-45b4-b135-41979de73245" + // GroupManagementPermissionName is the hardcoded setting name for the group management permission + GroupManagementPermissionName string = "group-management" + // SelfManagementPermissionID is the hardcoded setting UUID for the self management permission + SelfManagementPermissionID string = "e03070e9-4362-4cc6-a872-1c7cb2eb2b8e" + // SelfManagementPermissionName is the hardcoded setting name for the self management permission + SelfManagementPermissionName string = "self-management" +) + +// GenerateBundlesDefaultRoles bootstraps the default roles. +func GenerateBundlesDefaultRoles() []*settingsmsg.Bundle { + return []*settingsmsg.Bundle{ + generateBundleAdminRole(), + generateBundleUserRole(), + generateBundleGuestRole(), + generateBundleProfileRequest(), + generateBundleMetadataRole(), + } +} + +func generateBundleAdminRole() *settingsmsg.Bundle { + return &settingsmsg.Bundle{ + Id: BundleUUIDRoleAdmin, + Name: "admin", + Type: settingsmsg.Bundle_TYPE_ROLE, + Extension: "ocis-roles", + DisplayName: "Admin", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, + }, + Settings: []*settingsmsg.Setting{ + { + Id: RoleManagementPermissionID, + Name: RoleManagementPermissionName, + DisplayName: "Role Management", + Description: "This permission gives full access to everything that is related to role management.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_USER, + Id: "all", + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: settingsmsg.Permission_CONSTRAINT_ALL, + }, + }, + }, + { + Id: SettingsManagementPermissionID, + Name: SettingsManagementPermissionName, + DisplayName: "Settings Management", + Description: "This permission gives full access to everything that is related to settings management.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_USER, + Id: "all", + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: settingsmsg.Permission_CONSTRAINT_ALL, + }, + }, + }, + { + Id: "7d81f103-0488-4853-bce5-98dcce36d649", + Name: "language-readwrite", + DisplayName: "Permission to read and set the language (anyone)", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SETTING, + Id: settingUUIDProfileLanguage, + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: settingsmsg.Permission_CONSTRAINT_ALL, + }, + }, + }, + { + Id: AccountManagementPermissionID, + Name: AccountManagementPermissionName, + DisplayName: "Account Management", + Description: "This permission gives full access to everything that is related to account management.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_USER, + Id: "all", + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: settingsmsg.Permission_CONSTRAINT_ALL, + }, + }, + }, + { + Id: GroupManagementPermissionID, + Name: GroupManagementPermissionName, + DisplayName: "Group Management", + Description: "This permission gives full access to everything that is related to group management.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_GROUP, + Id: "all", + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: settingsmsg.Permission_CONSTRAINT_ALL, + }, + }, + }, + { + Id: SetSpaceQuotaPermissionID, + Name: SetSpaceQuotaPermissionName, + DisplayName: "Set Space Quota", + Description: "This permission allows to manage space quotas.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: settingsmsg.Permission_CONSTRAINT_ALL, + }, + }, + }, + { + Id: CreateSpacePermissionID, + Name: CreateSpacePermissionName, + DisplayName: "Create Space", + Description: "This permission allows to create new spaces.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: settingsmsg.Permission_CONSTRAINT_ALL, + }, + }, + }, + { + Id: ListAllSpacesPermissionID, + Name: ListAllSpacesPermissionName, + DisplayName: "List All Spaces", + Description: "This permission allows list all spaces.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READ, + Constraint: settingsmsg.Permission_CONSTRAINT_ALL, + }, + }, + }, + }, + } +} + +func generateBundleUserRole() *settingsmsg.Bundle { + return &settingsmsg.Bundle{ + Id: BundleUUIDRoleUser, + Name: "user", + Type: settingsmsg.Bundle_TYPE_ROLE, + Extension: "ocis-roles", + DisplayName: "User", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, + }, + Settings: []*settingsmsg.Setting{ + { + Id: "640e00d2-4df8-41bd-b1c2-9f30a01e0e99", + Name: "language-readwrite", + DisplayName: "Permission to read and set the language (self)", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SETTING, + Id: settingUUIDProfileLanguage, + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: settingsmsg.Permission_CONSTRAINT_OWN, + }, + }, + }, + { + Id: SelfManagementPermissionID, + Name: SelfManagementPermissionName, + DisplayName: "Self Management", + Description: "This permission gives access to self management.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_USER, + Id: "me", + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: settingsmsg.Permission_CONSTRAINT_OWN, + }, + }, + }, + { + Id: CreateSpacePermissionID, + Name: CreateSpacePermissionName, + DisplayName: "Create own Space", + Description: "This permission allows to create a space owned by the current user.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, // TODO resource type space? self? me? own? + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_CREATE, + Constraint: settingsmsg.Permission_CONSTRAINT_OWN, + }, + }, + }, + }, + } +} + +func generateBundleGuestRole() *settingsmsg.Bundle { + return &settingsmsg.Bundle{ + Id: BundleUUIDRoleGuest, + Name: "guest", + Type: settingsmsg.Bundle_TYPE_ROLE, + Extension: "ocis-roles", + DisplayName: "Guest", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, + }, + Settings: []*settingsmsg.Setting{ + { + Id: "ca878636-8b1a-4fae-8282-8617a4c13597", + Name: "language-readwrite", + DisplayName: "Permission to read and set the language (self)", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SETTING, + Id: settingUUIDProfileLanguage, + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: settingsmsg.Permission_CONSTRAINT_OWN, + }, + }, + }, + }, + } +} + +func generateBundleProfileRequest() *settingsmsg.Bundle { + return &settingsmsg.Bundle{ + Id: "2a506de7-99bd-4f0d-994e-c38e72c28fd9", + Name: "profile", + Extension: "ocis-accounts", + Type: settingsmsg.Bundle_TYPE_DEFAULT, + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, + }, + DisplayName: "Profile", + Settings: []*settingsmsg.Setting{ + { + Id: settingUUIDProfileLanguage, + Name: "language", + DisplayName: "Language", + Description: "User language", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_USER, + }, + Value: &languageSetting, + }, + }, + } +} + +func generateBundleMetadataRole() *settingsmsg.Bundle { + return &settingsmsg.Bundle{ + Id: BundleUUIDRoleMetadata, + Name: "metadata", + Type: settingsmsg.Bundle_TYPE_ROLE, + Extension: "ocis-roles", + DisplayName: "Metadata", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, + }, + Settings: []*settingsmsg.Setting{ + { + Id: CreateSpacePermissionID, + Name: CreateSpacePermissionName, + DisplayName: "Create own Space", + Description: "This permission allows to create a space owned by the current user.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, // TODO resource type space? self? me? own? + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_CREATE, + Constraint: settingsmsg.Permission_CONSTRAINT_OWN, + }, + }, + }, + }, + } +} + +// TODO: languageSetting needed? +var languageSetting = settingsmsg.Setting_SingleChoiceValue{ + SingleChoiceValue: &settingsmsg.SingleChoiceList{ + Options: []*settingsmsg.ListOption{ + { + Value: &settingsmsg.ListOptionValue{ + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "cs", + }, + }, + DisplayValue: "Czech", + }, + { + Value: &settingsmsg.ListOptionValue{ + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "de", + }, + }, + DisplayValue: "Deutsch", + }, + { + Value: &settingsmsg.ListOptionValue{ + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "en", + }, + }, + DisplayValue: "English", + Default: true, + }, + { + Value: &settingsmsg.ListOptionValue{ + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "es", + }, + }, + DisplayValue: "Español", + }, + { + Value: &settingsmsg.ListOptionValue{ + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "fr", + }, + }, + DisplayValue: "Français", + }, + { + Value: &settingsmsg.ListOptionValue{ + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "gl", + }, + }, + DisplayValue: "Galego", + }, + { + Value: &settingsmsg.ListOptionValue{ + Option: &settingsmsg.ListOptionValue_StringValue{ + StringValue: "it", + }, + }, + DisplayValue: "Italiano", + }, + }, + }, +} + +// DefaultRoleAssignments returns (as one might guess) the default role assignments +func DefaultRoleAssignments() []*settingsmsg.UserRoleAssignment { + return []*settingsmsg.UserRoleAssignment{ + // accounts service user for the metadata user is allowed to create spaces + { + AccountUuid: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad", + RoleId: BundleUUIDRoleAdmin, + }, + // default admin users + { + AccountUuid: "058bff95-6708-4fe5-91e4-9ea3d377588b", + RoleId: BundleUUIDRoleAdmin, + }, { + AccountUuid: "ddc2004c-0977-11eb-9d3f-a793888cd0f8", + RoleId: BundleUUIDRoleAdmin, + }, { + AccountUuid: "820ba2a1-3f54-4538-80a4-2d73007e30bf", + RoleId: BundleUUIDRoleAdmin, + }, { + AccountUuid: "bc596f3c-c955-4328-80a0-60d018b4ad57", + RoleId: BundleUUIDRoleAdmin, + }, + // default users with role "user" + { + AccountUuid: "4c510ada-c86b-4815-8820-42cdf82c3d51", + RoleId: BundleUUIDRoleUser, + }, { + AccountUuid: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + RoleId: BundleUUIDRoleUser, + }, { + AccountUuid: "932b4540-8d16-481e-8ef4-588e4b6b151c", + RoleId: BundleUUIDRoleUser, + }, + } +} diff --git a/settings/pkg/store/metadata/assignments.go b/settings/pkg/store/metadata/assignments.go new file mode 100644 index 00000000000..9f71ce0440c --- /dev/null +++ b/settings/pkg/store/metadata/assignments.go @@ -0,0 +1,111 @@ +// Package store implements the go-micro store interface +package store + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/gofrs/uuid" + settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0" + "github.com/owncloud/ocis/settings/pkg/store/defaults" +) + +// ListRoleAssignments loads and returns all role assignments matching the given assignment identifier. +func (s *Store) ListRoleAssignments(accountUUID string) ([]*settingsmsg.UserRoleAssignment, error) { + if s.mdc == nil || accountUUID == "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad" { + return defaultRoleAssignments(accountUUID), nil + } + s.Init() + ctx := context.TODO() + assIDs, err := s.mdc.ReadDir(ctx, accountPath(accountUUID)) + if err != nil { + return nil, err + } + + ass := make([]*settingsmsg.UserRoleAssignment, 0, len(assIDs)) + for _, assID := range assIDs { + b, err := s.mdc.SimpleDownload(ctx, assignmentPath(accountUUID, assID)) + if err != nil { + return nil, err + } + + a := &settingsmsg.UserRoleAssignment{} + err = json.Unmarshal(b, a) + if err != nil { + return nil, err + } + + ass = append(ass, a) + } + return ass, 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" + _ = s.mdc.Delete(ctx, accountPath(accountUUID)) + // TODO: How to differentiate between 'not found' and other errors? + + err := s.mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID)) + if err != nil { + return nil, err + } + + ass := &settingsmsg.UserRoleAssignment{ + Id: uuid.Must(uuid.NewV4()).String(), + 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) + if err != nil { + 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 { + return s.mdc.Delete(ctx, assignmentPath(accID, assID)) + } + } + } + return fmt.Errorf("assignmentID '%s' not found", assignmentID) +} + +func defaultRoleAssignments(accID string) []*settingsmsg.UserRoleAssignment { + var assmnts []*settingsmsg.UserRoleAssignment + for _, r := range defaults.DefaultRoleAssignments() { + if r.AccountUuid == accID { + assmnts = append(assmnts, r) + } + } + return assmnts +} + +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) +} diff --git a/settings/pkg/store/metadata/assignments_test.go b/settings/pkg/store/metadata/assignments_test.go new file mode 100644 index 00000000000..d4902f1e829 --- /dev/null +++ b/settings/pkg/store/metadata/assignments_test.go @@ -0,0 +1,189 @@ +package store + +import ( + "log" + "sync" + "testing" + + olog "github.com/owncloud/ocis/ocis-pkg/log" + settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0" + "github.com/owncloud/ocis/settings/pkg/config/defaults" + "github.com/stretchr/testify/require" +) + +var ( + einstein = "a4d07560-a670-4be9-8d60-9b547751a208" + //marie = "3c054db3-eec1-4ca4-b985-bc56dcf560cb" + + s = &Store{ + Logger: logger, + l: &sync.Mutex{}, + cfg: defaults.DefaultConfig(), + } + + 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: "ocis-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: "ocis-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 init() { + _ = NewMDC(s) + setupRoles() +} + +func setupRoles() { + 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", + einstein, + "f36db5e6-a03c-40df-8413-711c67e40b47", + "44f1a664-0a7f-461a-b0be-5b59e46bbc7a", + }, + } + + for _, scenario := range scenarios { + scenario := scenario + t.Run(scenario.name, func(t *testing.T) { + 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 TestDeleteAssignment(t *testing.T) { + var scenarios = []struct { + name string + userID string + firstRole string + secondRole string + }{ + { + "roles assignments", + einstein, + "f36db5e6-a03c-40df-8413-711c67e40b47", + "44f1a664-0a7f-461a-b0be-5b59e46bbc7a", + }, + } + + for _, scenario := range scenarios { + scenario := scenario + t.Run(scenario.name, func(t *testing.T) { + 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? + }) + } +} diff --git a/settings/pkg/store/metadata/bundles.go b/settings/pkg/store/metadata/bundles.go new file mode 100644 index 00000000000..766d6cf2625 --- /dev/null +++ b/settings/pkg/store/metadata/bundles.go @@ -0,0 +1,146 @@ +// Package store implements the go-micro store interface +package store + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/gofrs/uuid" + settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0" + "github.com/owncloud/ocis/settings/pkg/store/defaults" +) + +// 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) { + // TODO: this is needed for initialization - we need to find a better way to fix this + if s.mdc == nil && len(bundleIDs) == 1 { + return defaultBundle(bundleType, bundleIDs[0]), nil + } + s.Init() + ctx := context.TODO() + + if len(bundleIDs) == 0 { + bIDs, err := s.mdc.ReadDir(ctx, bundleFolderLocation) + if err != nil { + return nil, err + } + + bundleIDs = bIDs + } + var bundles []*settingsmsg.Bundle + for _, id := range bundleIDs { + b, err := s.mdc.SimpleDownload(ctx, bundlePath(id)) + if err != nil { + 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) { + if s.mdc == nil { + return defaultBundle(settingsmsg.Bundle_TYPE_ROLE, bundleID)[0], nil + } + s.Init() + ctx := context.TODO() + b, err := s.mdc.SimpleDownload(ctx, bundlePath(bundleID)) + if err != nil { + 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) + if err != nil { + return nil, err + } + + // TODO: avoid spamming metadata service + for _, id := range ids { + b, err := s.ReadBundle(id) + if err != nil { + return nil, err + } + + for _, setting := range b.Settings { + if setting.Id == settingID { + return setting, nil + } + } + + } + return nil, fmt.Errorf("setting '%s' not found", settingID) +} + +// 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 { + // TODO: How to differentiate 'not found'? + b = new(settingsmsg.Bundle) + b.Id = bundleID + b.Type = settingsmsg.Bundle_TYPE_DEFAULT + } + + if setting.Id == "" { + setting.Id = uuid.Must(uuid.NewV4()).String() + } + + 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) +} + +func defaultBundle(bundleType settingsmsg.Bundle_Type, bundleID string) []*settingsmsg.Bundle { + var bundles []*settingsmsg.Bundle + for _, b := range defaults.GenerateBundlesDefaultRoles() { + if b.Type == bundleType && b.Id == bundleID { + bundles = append(bundles, b) + } + } + return bundles +} diff --git a/settings/pkg/store/metadata/bundles_test.go b/settings/pkg/store/metadata/bundles_test.go new file mode 100644 index 00000000000..0aadbd3b72e --- /dev/null +++ b/settings/pkg/store/metadata/bundles_test.go @@ -0,0 +1,208 @@ +package store + +import ( + "testing" + + "github.com/gofrs/uuid" + settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/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.Must(uuid.NewV4()).String() + + 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) { + 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) { + //mdc := NewMDC() + //s := Store{ + //Logger: olog.NewLogger( + //olog.Color(true), + //olog.Pretty(true), + //olog.Level("info"), + //), + + //l: &sync.Mutex{}, + //mdc: mdc, + //} + + // 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) + +} diff --git a/settings/pkg/store/metadata/cache.go b/settings/pkg/store/metadata/cache.go new file mode 100644 index 00000000000..2b00496202b --- /dev/null +++ b/settings/pkg/store/metadata/cache.go @@ -0,0 +1,123 @@ +package store + +import ( + "context" + "path" + "strings" + "time" + + "github.com/ReneKroon/ttlcache/v2" +) + +var ( + cachettl = 0 + // these need to be global instances for now as the `Service` (and therefore the `Store`) are instantiated twice (for grpc and http) + // therefore caches need to cover both instances + dircache = initCache(cachettl) + filescache = initCache(cachettl) +) + +// CachedMDC is cache for the metadataclient +type CachedMDC struct { + next MetadataClient + + files *ttlcache.Cache + dirs *ttlcache.Cache +} + +// 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.files.Get(id); err == nil { + return b.([]byte), nil + } + b, err := c.next.SimpleDownload(ctx, id) + if err != nil { + return nil, err + } + + _ = c.files.Set(id, b) + 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.files.Get(id) + if err == nil && string(b.([]byte)) == string(content) { + // no need to bug mdc + return nil + } + + err = c.next.SimpleUpload(ctx, id, content) + if err != nil { + return err + } + + // invalidate caches + _ = c.dirs.Remove(path.Dir(id)) + _ = c.files.Set(id, content) + 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 + _ = removePrefix(c.files, id) + _ = removePrefix(c.dirs, 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.dirs.Get(id) + if err == nil { + return i.([]string), nil + } + + s, err := c.next.ReadDir(ctx, id) + if err != nil { + return nil, err + } + + return s, c.dirs.Set(id, s) +} + +// 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 + _ = c.dirs.Remove(path.Dir(id)) + return nil +} + +// Init instantiates the caches +func (c *CachedMDC) Init(ctx context.Context, id string) error { + c.dirs = dircache + c.files = filescache + return c.next.Init(ctx, id) +} + +func initCache(ttlSeconds int) *ttlcache.Cache { + cache := ttlcache.NewCache() + _ = cache.SetTTL(time.Duration(ttlSeconds) * time.Second) + cache.SkipTTLExtensionOnHit(true) + return cache +} + +func removePrefix(cache *ttlcache.Cache, prefix string) error { + for _, k := range cache.GetKeys() { + if strings.HasPrefix(k, prefix) { + if err := cache.Remove(k); err != nil { + return err + } + } + } + return nil +} diff --git a/settings/pkg/store/metadata/permissions.go b/settings/pkg/store/metadata/permissions.go new file mode 100644 index 00000000000..50ebe12a532 --- /dev/null +++ b/settings/pkg/store/metadata/permissions.go @@ -0,0 +1,72 @@ +package store + +import ( + settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0" + "github.com/owncloud/ocis/settings/pkg/settings" + "github.com/owncloud/ocis/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.ErrPermissionNotFound +} + +// 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 +} diff --git a/settings/pkg/store/metadata/permissions_test.go b/settings/pkg/store/metadata/permissions_test.go new file mode 100644 index 00000000000..db3283cb461 --- /dev/null +++ b/settings/pkg/store/metadata/permissions_test.go @@ -0,0 +1,27 @@ +package store + +import ( + "testing" + + settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0" + "github.com/stretchr/testify/require" +) + +func TestPermission(t *testing.T) { + // 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) + +} diff --git a/settings/pkg/store/metadata/store.go b/settings/pkg/store/metadata/store.go new file mode 100644 index 00000000000..db0abdcb552 --- /dev/null +++ b/settings/pkg/store/metadata/store.go @@ -0,0 +1,155 @@ +// Package store implements the go-micro store interface +package store + +import ( + "context" + "encoding/json" + "log" + "sync" + + "github.com/cs3org/reva/v2/pkg/storage/utils/metadata" + "github.com/gofrs/uuid" + olog "github.com/owncloud/ocis/ocis-pkg/log" + settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0" + "github.com/owncloud/ocis/settings/pkg/config" + "github.com/owncloud/ocis/settings/pkg/settings" + "github.com/owncloud/ocis/settings/pkg/store/defaults" +) + +var ( + // Name is the default name for the settings store + Name = "ocis-settings" + managerName = "metadata" + settingsSpaceID = "f1bdd61a-da7c-49fc-8203-0558109d1b4f" // uuid.Must(uuid.NewV4()).String() + 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)} + 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.NewLogger( + olog.Color(cfg.Log.Color), + olog.Pretty(cfg.Log.Pretty), + olog.Level(cfg.Log.Level), + olog.File(cfg.Log.File), + ), + 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.ServiceUserID, cfg.ServiceUserIDP, cfg.MachineAuthAPIKey) + 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 defaults.GenerateBundlesDefaultRoles() { + 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() { + accountUUID := p.AccountUuid + roleID := p.RoleId + err = mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID)) + if err != nil { + return err + } + + ass := &settingsmsg.UserRoleAssignment{ + Id: uuid.Must(uuid.NewV4()).String(), + 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 init() { + settings.Registry[managerName] = New +} diff --git a/settings/pkg/store/metadata/store_test.go b/settings/pkg/store/metadata/store_test.go new file mode 100644 index 00000000000..10570b62b18 --- /dev/null +++ b/settings/pkg/store/metadata/store_test.go @@ -0,0 +1,110 @@ +package store + +import ( + "context" + "strings" + + "github.com/owncloud/ocis/settings/pkg/config/defaults" +) + +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 ocis 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 nil if not found +func (m *MockedMetadataClient) SimpleDownload(_ context.Context, id string) ([]byte, error) { + return m.data[id], nil +} + +// 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) +} diff --git a/settings/pkg/store/metadata/values.go b/settings/pkg/store/metadata/values.go new file mode 100644 index 00000000000..9dd73b16d1b --- /dev/null +++ b/settings/pkg/store/metadata/values.go @@ -0,0 +1,94 @@ +// Package store implements the go-micro store interface +package store + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/gofrs/uuid" + settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0" +) + +// 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) + if err != nil { + 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)) + if err != nil { + 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)) + if err != nil { + 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) { + fmt.Println("ReadValueByUniqueIdentifiers not implemented") + return nil, errors.New("not implemented") +} + +// 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.Must(uuid.NewV4()).String() + } + 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) +} diff --git a/settings/pkg/store/metadata/values_test.go b/settings/pkg/store/metadata/values_test.go new file mode 100644 index 00000000000..62c71867e0d --- /dev/null +++ b/settings/pkg/store/metadata/values_test.go @@ -0,0 +1,100 @@ +package store + +import ( + "testing" + + settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/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) { + 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) { + 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) + +} diff --git a/settings/pkg/store/registry.go b/settings/pkg/store/registry.go index 29186466e34..2359e0d392d 100644 --- a/settings/pkg/store/registry.go +++ b/settings/pkg/store/registry.go @@ -3,4 +3,5 @@ package store import ( // init filesystem store _ "github.com/owncloud/ocis/settings/pkg/store/filesystem" + _ "github.com/owncloud/ocis/settings/pkg/store/metadata" )