Repository: owncloud/ocis
Pull request #3232 (2022-02-24 14:18:41)

[full-ci] Store settings via metadata
<!--
Thanks for submitting a change to ownCloud!

This is the bug tracker for the oCIS component. Find other components at https://github.com/owncloud/core/blob/master/.github/CONTRIBUTING.md#guidelines

For fixing potential security issues please see https://owncloud.org/security/

To make it possible for us to get your change reviewed and merged please carefully fill out the requested information below.

Please note that any kind of change needs first be submitted to the master branch which holds the next version of oCIS.

Please set the following labels:

- Set label "Status:Needs-Review" for review or "Status:In-Progress" if the PR still has open tasks.
- Assignment: assign to self
- Reviewers: pick at least one
-->

## Description
<!--- Describe your changes in detail -->
Encourages settings service to store its data via the metadata service

## Related Issue
<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->
- Fixes https://jira.owncloud.com/browse/OCIS-2346

## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
- new unit tests
- existing acceptance tests cover functionality

## Screenshots (if appropriate):

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing functionality to change)
- [x] Technical debt
- [ ] Tests only (no source changes)

## Checklist:
<!-- Tick the checkboxes when done. -->
<!-- Raise documentation ticket in owncloud.github.io/ -->
- [x] Code changes
- [x] Unit tests added
- [ ] Acceptance tests added
- [ ] Documentation ticket raised: <link> 



Patch:
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/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/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/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/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/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"
 )


Test patch:
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_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/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_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_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)
+
+}