Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# maintain v2 separate mocks dir
dir: "{{.InterfaceDir}}/mocks"
structname: "{{.InterfaceName}}"
filename: "{{.InterfaceName | snakecase }}.go"
pkgname: mocks
template: testify
packages:
github.com/qsfera/server/services/settings/pkg/settings:
interfaces:
Manager: {}
+43
View File
@@ -0,0 +1,43 @@
SHELL := bash
NAME := settings
OUTPUT_DIR = ./pkg/service/v0/l10n
TEMPLATE_FILE = ./pkg/service/v0/l10n/settings.pot
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include ../../.bingo/Variables.mk
endif
include ../../.make/default.mk
include ../../.make/go.mk
include ../../.make/release.mk
include ../../.make/docs.mk
include ../../.make/protobuf.mk
.PHONY: docs-generate
docs-generate: grpc-docs-generate
.PHONY: go-generate
go-generate: $(MOCKERY) protobuf
$(MOCKERY)
.PHONY: protobuf
protobuf: buf-generate $(PROTOC_GO_INJECT_TAG)
cd ../../protogen/gen/ && $(PROTOC_GO_INJECT_TAG) -input="qsfera/messages/settings/v0/*.pb.go"
.PHONY: l10n-pull
l10n-pull:
cd $(OUTPUT_DIR) && tx pull --all --force --skip --minimum-perc=75
.PHONY: l10n-push
l10n-push:
cd $(OUTPUT_DIR) && tx push -s --skip
.PHONY: l10n-read
l10n-read: $(GO_XGETTEXT)
$(GO_XGETTEXT) -o $(OUTPUT_DIR)/settings.pot \
--keyword=l10n.Template --add-comments -s \
pkg/store/defaults/templates.go
.PHONY: l10n-clean
l10n-clean:
rm -f $(TEMPLATE_FILE);
+451
View File
@@ -0,0 +1,451 @@
# Settings
The `settings` service provides functionality for other services to register new settings as well as storing and retrieving the respective settings' values.
## Settings Managed
The settings service is currently used for managing the:
* users' `profile` settings like the language and the email notification settings,
* possible user roles and their respective permissions,
* assignment of roles to users.
As an example, user profile settings that can be changed in the Web UI must be persistent.
The settings service persists the settings data via the `storage-system` service.
<!--- Note: The diagramm is outdate, leaving it here for a future rework
The diagram shows how the settings service integrates into КуСфера:
The diagram shows how the settings service integrates into КуСфера:
```mermaid
graph TD
ows ---|"listSettingsBundles(),<br>saveSettingsValue(value)"| os[qsfera-settings]
owc ---|"listSettingsValues()"| sdk[oC SDK]
sdk --- sdks{qsfera-settings<br>available?}
sdks ---|"yes"| os
sdks ---|"no"| defaults[Use set of<br>default values]
oa[КуСфера services<br>e.g. qsfera-accounts] ---|"saveSettingsBundle(bundle)"| os
```
-->
## Caching
The `settings` service caches the results of queries against the storage backend to provide faster responses. The content of this cache is independent of the cache used in the `storage-system` service as it caches directory listing and settings content stored in files.
The store used for the cache can be configured using the `SETTINGS_CACHE_STORE` environment variable. Possible stores are:
- `memory`: Basic in-memory store and the default.
- `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
- `nats-js-kv`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
- `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
Other store types may work but are not supported currently.
Note: The service can only be scaled if not using `memory` store and the stores are configured identically over all instances!
Note that if you have used one of the deprecated stores, you should reconfigure to one of the supported ones as the deprecated stores will be removed in a later version.
Store specific notes:
- When using `redis-sentinel`, the Redis master to use is configured via e.g. `OC_CACHE_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
- When using `nats-js-kv` it is recommended to set `OC_CACHE_STORE_NODES` to the same value as `OC_EVENTS_ENDPOINT`. That way the cache uses the same nats instance as the event bus.
- When using the `nats-js-kv` store, it is possible to set `OC_CACHE_DISABLE_PERSISTENCE` to instruct nats to not persist cache data on disc.
## Settings Management
КуСфера services can register *settings bundles* with the settings service.
## Settings Usage
Services can set or query КуСфера *setting values* of a user from settings bundles.
## Service Accounts
The settings service needs to know the IDs of service accounts but it doesn't need their secrets. They can be configured using the `SETTINGS_SERVICE_ACCOUNTS_IDS` envvar. When only using one service account `OC_SERVICE_ACCOUNT_ID` can also be used. All configured service accounts will get a hidden 'service-account' role. This role contains all permissions the service account needs but will not appear calls to the list roles endpoint. It is not possible to assign the 'service-account' role to a normal user.
## Translations
The `settings` service has embedded translations sourced via transifex to provide a basic set of translated languages. These embedded translations are available for all deployment scenarios. In addition, the service supports custom translations, though it is currently not possible to just add custom translations to embedded ones. If custom translations are configured, the embedded ones are not used. To configure custom translations, the `SETTINGS_TRANSLATION_PATH` environment variable needs to point to a base folder that will contain the translation files. This path must be available from all instances of the userlog service, a shared storage is recommended. Translation files must be of type [.po](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files) or [.mo](https://www.gnu.org/software/gettext/manual/html_node/Binaries.html). For each language, the filename needs to be `settings.po` (or `settings.mo`) and stored in a folder structure defining the language code. In general the path/name pattern for a translation file needs to be:
```text
{SETTINGS_TRANSLATION_PATH}/{language-code}/LC_MESSAGES/settings.po
```
The language code pattern is composed of `language[_territory]` where `language` is the base language and `_territory` is optional and defines a country.
For example, for the language `de`, one needs to place the corresponding translation files to `{SETTINGS_TRANSLATION_PATH}/de_DE/LC_MESSAGES/settings.po`.
<!-- also see the notifications readme -->
Important: For the time being, the embedded КуСфера Web frontend only supports the main language code but does not handle any territory. When strings are available in the language code `language_territory`, the web frontend does not see it as it only requests `language`. In consequence, any translations made must exist in the requested `language` to avoid a fallback to the default.
### Translation Rules
* If a requested language code is not available, the service tries to fall back to the base language if available. For example, if the requested language-code `de_DE` is not available, the service tries to fall back to translations in the `de` folder.
* If the base language `de` is also not available, the service falls back to the system's default English (`en`),
which is the source of the texts provided by the code.
## Default Language
The default language can be defined via the `OC_DEFAULT_LANGUAGE` environment variable. If this variable is not defined, English will be used as default. The value has the ISO 639-1 format ("de", "en", etc.) and is limited by the list supported languages. This setting can be used to set the default language for notification and invitation emails.
Important developer note: the list of supported languages is at the moment not easy defineable, as it is the minimum intersection of languages shown in the WebUI and languages defined in the КуСфера code for the use of notifications and userlog. Even more, not all languages where there are translations available on transifex, are available in the WebUI respectively for КуСфера notifications, and the translation rate for existing languages is partially not that high. You will see therefore quite often English default strings though a supported language may exist and was selected.
The `OC_DEFAULT_LANGUAGE` setting impacts the `notification` and `userlog` services and the WebUI. Note that translations must exist for all named components to be presented correctly.
* If `OC_DEFAULT_LANGUAGE` **is not set**, the expected behavior is:
* The `notification` and `userlog` services and the WebUI use English by default until a user sets another language in the WebUI via _Account -> Language_.
* If a user sets another language in the WebUI in _Account -> Language_, then the `notification` and `userlog` services and WebUI use the language defined by the user. If no translation is found, it falls back to English.
* If `OC_DEFAULT_LANGUAGE` **is set**, the expected behavior is:
* The `notification` and `userlog` services and the WebUI use `OC_DEFAULT_LANGUAGE` by default until a user sets another language in the WebUI via _Account -> Language_.
* If a user sets another language in the WebUI in _Account -> Language_, the `notification` and `userlog` services and WebUI use the language defined by the user. If no translation is found, it falls back to `OC_DEFAULT_LANGUAGE` and then to English.
## Custom Roles
It is possible to replace the default КуСфера roles (`admin`, `user`) with custom roles that contain custom permissions. One can set `SETTINGS_BUNDLES_PATH` to the path of a `json` file containing the new roles.
Role Example:
```json
[
{
"id": "38071a68-456a-4553-846a-fa67bf5596cc", // ID of the role. Recommendation is to use a random uuidv4. But any unique string will do.
"name": "user-light", // Internal name of the role. This is used by the system to identify the role. Any string will do here, but it should be unique among the other roles.
"type": "TYPE_ROLE", // Always use `TYPE_ROLE`
"extension": "qsfera-roles", // Always use `qsfera-roles`
"displayName": "User Light", // DisplayName of the role used in webui
"settings": [
], // Permissions attached to the role. See Details below.
"resource": {
"type": "TYPE_SYSTEM" // Always use `TYPE_SYSTEM`
}
}
]
```
To create custom roles:
* Copy the role example to a `json` file.
* Change `id`, `name`, and `displayName` to your liking.
* Copy the desired permissions from the `user-all-permissions` example below to the `settings` array of the role.
* Set the `SETTINGS_BUNDLE_PATH` envvar to the path of the json file and start КуСфера
Example File:
```json
[
{
"id": "38071a68-456a-4553-846a-fa67bf5596cc",
"name": "user-1-permission",
"type": "TYPE_ROLE",
"extension": "qsfera-roles",
"displayName": "User with one permission only",
"settings": [
{
"id": "7d81f103-0488-4853-bce5-98dcce36d649",
"name": "Language.ReadWrite",
"displayName": "Permission to read and set the language",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_OWN"
},
"resource": {
"type": "TYPE_SETTING",
"id": "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f"
}
}
],
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "71881883-1768-46bd-a24d-a356a2afdf7f",
"name": "user-all-permissions",
"type": "TYPE_ROLE",
"extension": "qsfera-roles",
"displayName": "User with all available permissions",
"settings": [
{
"id": "8e587774-d929-4215-910b-a317b1e80f73",
"name": "Accounts.ReadWrite",
"displayName": "Account Management",
"description": "This permission gives full access to everything that is related to account management.",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_USER",
"id": "all"
}
},
{
"id": "4e41363c-a058-40a5-aec8-958897511209",
"name": "AutoAcceptShares.ReadWriteDisabled",
"displayName": "enable/disable auto accept shares",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_OWN"
},
"resource": {
"type": "TYPE_SETTING",
"id": "ec3ed4a3-3946-4efc-8f9f-76d38b12d3a9"
}
},
{
"id": "ed83fc10-1f54-4a9e-b5a7-fb517f5f3e01",
"name": "Logo.Write",
"displayName": "Change logo",
"description": "This permission permits to change the system logo.",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "11516bbd-7157-49e1-b6ac-d00c820f980b",
"name": "PublicLink.Write",
"displayName": "Write publiclink",
"description": "This permission allows creating public links.",
"permissionValue": {
"operation": "OPERATION_WRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SHARE"
}
},
{
"id": "069c08b1-e31f-4799-9ed6-194b310e7244",
"name": "Shares.Write",
"displayName": "Write share",
"description": "This permission allows creating shares.",
"permissionValue": {
"operation": "OPERATION_WRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SHARE"
}
},
{
"id": "79e13b30-3e22-11eb-bc51-0b9f0bad9a58",
"name": "Drives.Create",
"displayName": "Create Space",
"description": "This permission allows creating new spaces.",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "5de9fe0a-4bc5-4a47-b758-28f370caf169",
"name": "Drives.DeletePersonal",
"displayName": "Delete All Home Spaces",
"description": "This permission allows deleting home spaces.",
"permissionValue": {
"operation": "OPERATION_DELETE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "fb60b004-c1fa-4f09-bf87-55ce7d46ac61",
"name": "Drives.DeleteProject",
"displayName": "Delete AllSpaces",
"description": "This permission allows deleting all spaces.",
"permissionValue": {
"operation": "OPERATION_DELETE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "e9a697c5-c67b-40fc-982b-bcf628e9916d",
"name": "ReadOnlyPublicLinkPassword.Delete",
"displayName": "Delete Read-Only Public link password",
"description": "This permission permits to opt out of a public link password enforcement.",
"permissionValue": {
"operation": "OPERATION_WRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SHARE"
}
},
{
"id": "ad5bb5e5-dc13-4cd3-9304-09a424564ea8",
"name": "EmailNotifications.ReadWriteDisabled",
"displayName": "Disable Email Notifications",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_OWN"
},
"resource": {
"type": "TYPE_SETTING",
"id": "33ffb5d6-cd07-4dc0-afb0-84f7559ae438"
}
},
{
"id": "522adfbe-5908-45b4-b135-41979de73245",
"name": "Groups.ReadWrite",
"displayName": "Group Management",
"description": "This permission gives full access to everything that is related to group management.",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_GROUP",
"id": "all"
}
},
{
"id": "7d81f103-0488-4853-bce5-98dcce36d649",
"name": "Language.ReadWrite",
"displayName": "Permission to read and set the language",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SETTING",
"id": "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f"
}
},
{
"id": "4ebaa725-bfaa-43c5-9817-78bc9994bde4",
"name": "Favorites.List",
"displayName": "List Favorites",
"description": "This permission allows listing favorites.",
"permissionValue": {
"operation": "OPERATION_READ",
"constraint": "CONSTRAINT_OWN"
},
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "016f6ddd-9501-4a0a-8ebe-64a20ee8ec82",
"name": "Drives.List",
"displayName": "List All Spaces",
"description": "This permission allows listing all spaces.",
"permissionValue": {
"operation": "OPERATION_READ",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "b44b4054-31a2-42b8-bb71-968b15cfbd4f",
"name": "Drives.ReadWrite",
"displayName": "Manage space properties",
"description": "This permission allows managing space properties such as name and description.",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "a53e601e-571f-4f86-8fec-d4576ef49c62",
"name": "Roles.ReadWrite",
"displayName": "Role Management",
"description": "This permission gives full access to everything that is related to role management.",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_USER",
"id": "all"
}
},
{
"id": "4e6f9709-f9e7-44f1-95d4-b762d27b7896",
"name": "Drives.ReadWritePersonalQuota",
"displayName": "Set Personal Space Quota",
"description": "This permission allows managing personal space quotas.",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "977f0ae6-0da2-4856-93f3-22e0a8482489",
"name": "Drives.ReadWriteProjectQuota",
"displayName": "Set Project Space Quota",
"description": "This permission allows managing project space quotas.",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "3d58f441-4a05-42f8-9411-ef5874528ae1",
"name": "Settings.ReadWrite",
"displayName": "Settings Management",
"description": "This permission gives full access to everything that is related to settings management.",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_USER",
"id": "all"
}
},
{
"id": "cf3faa8c-50d9-4f84-9650-ff9faf21aa9d",
"name": "Drives.ReadWriteEnabled",
"displayName": "Space ability",
"description": "This permission allows enabling and disabling spaces.",
"permissionValue": {
"operation": "OPERATION_READWRITE",
"constraint": "CONSTRAINT_ALL"
},
"resource": {
"type": "TYPE_SYSTEM"
}
},
{
"id": "a54778fd-1c45-47f0-892d-655caf5236f2",
"name": "Favorites.Write",
"displayName": "Write Favorites",
"description": "This permission allows marking files as favorites.",
"permissionValue": {
"operation": "OPERATION_WRITE",
"constraint": "CONSTRAINT_OWN"
},
"resource": {
"type": "TYPE_FILE"
}
}
],
"resource": {
"type": "TYPE_SYSTEM"
}
}
]
```
@@ -0,0 +1,54 @@
package command
import (
"fmt"
"net/http"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/qsfera/server/services/settings/pkg/config/parser"
"github.com/spf13/cobra"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "health",
Short: "Check health status",
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if err != nil {
logger.Fatal().
Err(err).
Msg("Failed to request health check")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Fatal().
Int("code", resp.StatusCode).
Msg("Health seems to be in bad state")
}
logger.Debug().
Int("code", resp.StatusCode).
Msg("Health got a good state")
return nil
},
}
}
@@ -0,0 +1,36 @@
package command
import (
"os"
"github.com/qsfera/server/pkg/clihelper"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/spf13/cobra"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) []*cobra.Command {
return []*cobra.Command{
// start this service
Server(cfg),
// interaction with this service
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the qsfera-settings command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cobra.Command{
Use: "settings",
Short: "Provide settings and permissions for КуСфера",
})
app.AddCommand(GetCommands(cfg)...)
app.SetArgs(os.Args[1:])
return app.ExecuteContext(cfg.Context)
}
@@ -0,0 +1,114 @@
package command
import (
"context"
"fmt"
"os/signal"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/runner"
ogrpc "github.com/qsfera/server/pkg/service/grpc"
"github.com/qsfera/server/pkg/tracing"
"github.com/qsfera/server/pkg/version"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/qsfera/server/services/settings/pkg/config/parser"
"github.com/qsfera/server/services/settings/pkg/metrics"
"github.com/qsfera/server/services/settings/pkg/server/debug"
"github.com/qsfera/server/services/settings/pkg/server/grpc"
"github.com/qsfera/server/services/settings/pkg/server/http"
svc "github.com/qsfera/server/services/settings/pkg/service/v0"
"github.com/spf13/cobra"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "server",
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
traceProvider, err := tracing.GetTraceProvider(cmd.Context(), cfg.Commons.TracesExporter, cfg.Service.Name)
if err != nil {
return err
}
cfg.GrpcClient, err = ogrpc.NewClient(
append(ogrpc.GetClientOptions(cfg.GRPCClientTLS), ogrpc.WithTraceProvider(traceProvider))...,
)
if err != nil {
return err
}
var cancel context.CancelFunc
if cfg.Context == nil {
cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...)
defer cancel()
}
ctx := cfg.Context
mtrcs := metrics.New()
mtrcs.BuildInfo.WithLabelValues(version.GetString()).Set(1)
handle := svc.NewDefaultLanguageService(cfg, svc.NewService(cfg, logger))
gr := runner.NewGroup()
// prepare an HTTP server and add it to the group run.
httpServer, err := http.Server(
http.Name(cfg.Service.Name),
http.Logger(logger),
http.Context(ctx),
http.Config(cfg),
http.Metrics(mtrcs),
http.ServiceHandler(handle),
http.TraceProvider(traceProvider),
)
if err != nil {
logger.Error().
Err(err).
Msg("Error initializing http service")
return fmt.Errorf("could not initialize http service: %w", err)
}
gr.Add(runner.NewGoMicroHttpServerRunner(cfg.Service.Name+".http", httpServer))
// prepare a gRPC server and add it to the group run.
grpcServer := grpc.Server(
grpc.Name(cfg.Service.Name),
grpc.Logger(logger),
grpc.Context(ctx),
grpc.Config(cfg),
grpc.Metrics(mtrcs),
grpc.ServiceHandler(handle),
grpc.TraceProvider(traceProvider),
)
gr.Add(runner.NewGoMicroGrpcServerRunner(cfg.Service.Name+".grpc", grpcServer))
// prepare a debug server and add it to the group run.
debugServer, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Error().Err(err).Str("server", "debug").Msg("Failed to initialize server")
return err
}
gr.Add(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", debugServer))
grResults := gr.Run(ctx)
// return the first non-nil error found in the results
for _, grResult := range grResults {
if grResult.RunnerError != nil {
return grResult.RunnerError
}
}
return nil
},
}
}
@@ -0,0 +1,49 @@
package command
import (
"fmt"
"os"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/version"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/spf13/cobra"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "print the version of this binary and the running service instances",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Version: " + version.GetString())
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.GRPC.Namespace + "." + cfg.Service.Name)
if err != nil {
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
return err
}
if len(services) == 0 {
fmt.Println("No running " + cfg.Service.Name + " service found.")
return nil
}
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}
@@ -0,0 +1,67 @@
package config
import (
"context"
"time"
"github.com/qsfera/server/pkg/shared"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"go-micro.dev/v4/client"
)
// Config combines all available configuration parts.
type Config struct {
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
Service Service `yaml:"-"`
LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;SETTINGS_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"`
Debug Debug `yaml:"debug"`
HTTP HTTP `yaml:"http"`
GRPC GRPCConfig `yaml:"grpc"`
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
GrpcClient client.Client `yaml:"-"`
Metadata Metadata `yaml:"metadata_config"`
BundlesPath string `yaml:"bundles_path" env:"SETTINGS_BUNDLES_PATH" desc:"The path to a JSON file with a list of bundles. If not defined, the default bundles will be loaded." introductionVersion:"1.0.0"`
Bundles []*settingsmsg.Bundle `yaml:"-"`
AdminUserID string `yaml:"admin_user_id" env:"OC_ADMIN_USER_ID;SETTINGS_ADMIN_USER_ID" desc:"ID of the user that should receive admin privileges. Consider that the UUID can be encoded in some LDAP deployment configurations like in .ldif files. These need to be decoded beforehand." introductionVersion:"1.0.0"`
TokenManager *TokenManager `yaml:"token_manager"`
SetupDefaultAssignments bool `yaml:"set_default_assignments" env:"IDM_CREATE_DEMO_USERS;SETTINGS_SETUP_DEFAULT_ASSIGNMENTS" desc:"The default role assignments the demo users should be setup." introductionVersion:"1.0.0"`
ServiceAccountIDs []string `yaml:"service_account_ids" env:"SETTINGS_SERVICE_ACCOUNT_IDS;OC_SERVICE_ACCOUNT_ID" desc:"The list of all service account IDs. These will be assigned the hidden 'service-account' role. Note: When using 'OC_SERVICE_ACCOUNT_ID' this will contain only one value while 'SETTINGS_SERVICE_ACCOUNT_IDS' can have multiple. See the 'auth-service' service description for more details about service accounts." introductionVersion:"1.0.0"`
DefaultLanguage string `yaml:"default_language" env:"OC_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details." introductionVersion:"1.0.0"`
TranslationPath string `yaml:"translation_path" env:"OC_TRANSLATION_PATH;SETTINGS_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details." introductionVersion:"1.0.0"`
Context context.Context `yaml:"-"`
}
// Metadata configures the metadata store to use
type Metadata struct {
GatewayAddress string `yaml:"gateway_addr" env:"SETTINGS_STORAGE_GATEWAY_GRPC_ADDR;STORAGE_GATEWAY_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"1.0.0"`
StorageAddress string `yaml:"storage_addr" env:"SETTINGS_STORAGE_GRPC_ADDR;STORAGE_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"1.0.0"`
SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID;SETTINGS_SYSTEM_USER_ID" desc:"ID of the КуСфера STORAGE-SYSTEM system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"1.0.0"`
SystemUserIDP string `yaml:"system_user_idp" env:"OC_SYSTEM_USER_IDP;SETTINGS_SYSTEM_USER_IDP" desc:"IDP of the КуСфера STORAGE-SYSTEM system user." introductionVersion:"1.0.0"`
SystemUserAPIKey string `yaml:"system_user_api_key" env:"OC_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"1.0.0"`
Cache *Cache `yaml:"cache"`
}
// Cache configures the cache of the Metadata store
type Cache struct {
Store string `yaml:"store" env:"OC_CACHE_STORE;SETTINGS_CACHE_STORE" desc:"The type of the cache store. Supported values are: 'memory', 'redis-sentinel', 'nats-js-kv', 'noop'. See the text description for details." introductionVersion:"1.0.0"`
Nodes []string `yaml:"addresses" env:"OC_CACHE_STORE_NODES;SETTINGS_CACHE_STORE_NODES" desc:"A list of nodes to access the configured store. This has no effect when 'memory' store is configured. Note that the behaviour how nodes are used is dependent on the library of the configured store. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
Database string `yaml:"database" env:"OC_CACHE_DATABASE" desc:"The database name the configured store should use." introductionVersion:"1.0.0"`
FileTable string `yaml:"files_table" env:"SETTINGS_FILE_CACHE_TABLE" desc:"The database table the store should use for the file cache." introductionVersion:"1.0.0"`
DirectoryTable string `yaml:"directories_table" env:"SETTINGS_DIRECTORY_CACHE_TABLE" desc:"The database table the store should use for the directory cache." introductionVersion:"1.0.0"`
TTL time.Duration `yaml:"ttl" env:"OC_CACHE_TTL;SETTINGS_CACHE_TTL" desc:"Default time to live for entries in the cache. Only applied when access tokens has no expiration. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
DisablePersistence bool `yaml:"disable_persistence" env:"OC_CACHE_DISABLE_PERSISTENCE;SETTINGS_CACHE_DISABLE_PERSISTENCE" desc:"Disables persistence of the cache. Only applies when store type 'nats-js-kv' is configured. Defaults to false." introductionVersion:"1.0.0"`
AuthUsername string `yaml:"username" env:"OC_CACHE_AUTH_USERNAME;SETTINGS_CACHE_AUTH_USERNAME" desc:"The username to authenticate with the cache. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"1.0.0"`
AuthPassword string `yaml:"password" env:"OC_CACHE_AUTH_PASSWORD;SETTINGS_CACHE_AUTH_PASSWORD" desc:"The password to authenticate with the cache. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,9 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `yaml:"addr" env:"SETTINGS_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"`
Token string `yaml:"token" env:"SETTINGS_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"`
Pprof bool `yaml:"pprof" env:"SETTINGS_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"`
Zpages bool `yaml:"zpages" env:"SETTINGS_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,129 @@
package defaults
import (
"encoding/json"
"os"
"strings"
"time"
"github.com/qsfera/server/pkg/structs"
"github.com/qsfera/server/services/settings/pkg/config"
rdefaults "github.com/qsfera/server/services/settings/pkg/store/defaults"
"github.com/pkg/errors"
)
// FullDefaultConfig returns a fully initialized default configuration
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
// DefaultConfig returns the default config
func DefaultConfig() *config.Config {
return &config.Config{
Service: config.Service{
Name: "settings",
},
Debug: config.Debug{
Addr: "127.0.0.1:9194",
Token: "",
Pprof: false,
Zpages: false,
},
HTTP: config.HTTP{
Addr: "127.0.0.1:9190",
Namespace: "qsfera.web",
Root: "/",
CORS: config.CORS{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id"},
AllowCredentials: true,
},
},
GRPC: config.GRPCConfig{
Addr: "127.0.0.1:9191",
Namespace: "qsfera.api",
},
SetupDefaultAssignments: false,
Metadata: config.Metadata{
GatewayAddress: "qsfera.api.storage-system",
StorageAddress: "qsfera.api.storage-system",
SystemUserIDP: "internal",
Cache: &config.Cache{
Store: "memory",
Nodes: []string{"127.0.0.1:9233"},
Database: "settings-cache",
FileTable: "settings_files",
DirectoryTable: "settings_dirs",
TTL: time.Minute * 10,
},
},
BundlesPath: "",
Bundles: nil,
ServiceAccountIDs: []string{"service-user-id"},
}
}
// EnsureDefaults adds default values to the configuration if they are not set yet
func EnsureDefaults(cfg *config.Config) {
if cfg.LogLevel == "" {
cfg.LogLevel = "error"
}
if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
cfg.TokenManager = &config.TokenManager{
JWTSecret: cfg.Commons.TokenManager.JWTSecret,
}
} else if cfg.TokenManager == nil {
cfg.TokenManager = &config.TokenManager{}
}
if cfg.Metadata.SystemUserAPIKey == "" && cfg.Commons != nil && cfg.Commons.SystemUserAPIKey != "" {
cfg.Metadata.SystemUserAPIKey = cfg.Commons.SystemUserAPIKey
}
if cfg.Metadata.SystemUserID == "" && cfg.Commons != nil && cfg.Commons.SystemUserID != "" {
cfg.Metadata.SystemUserID = cfg.Commons.SystemUserID
}
if cfg.AdminUserID == "" && cfg.Commons != nil {
cfg.AdminUserID = cfg.Commons.AdminUserID
}
if cfg.GRPCClientTLS == nil && cfg.Commons != nil {
cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
}
if cfg.GRPC.TLS == nil && cfg.Commons != nil {
cfg.GRPC.TLS = structs.CopyOrZeroValue(cfg.Commons.GRPCServiceTLS)
}
if cfg.Commons != nil {
cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS
}
}
// Sanitize sanitized the configuration
func Sanitize(cfg *config.Config) {
// sanitize config
if cfg.HTTP.Root != "/" {
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
}
}
// LoadBundles loads setting bundles from a file or from defaults
func LoadBundles(cfg *config.Config) error {
if cfg.BundlesPath != "" {
data, _ := os.ReadFile(cfg.BundlesPath)
err := json.Unmarshal(data, &cfg.Bundles)
if err != nil {
return errors.Wrapf(err, "Could not load bundles from path %s", cfg.BundlesPath)
}
}
if len(cfg.Bundles) == 0 {
cfg.Bundles = rdefaults.GenerateBundlesDefaultRoles()
}
return nil
}
@@ -0,0 +1,10 @@
package config
import "github.com/qsfera/server/pkg/shared"
// GRPCConfig defines the available grpc configuration.
type GRPCConfig struct {
Addr string `yaml:"addr" env:"SETTINGS_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"1.0.0"`
Namespace string `yaml:"-"`
TLS *shared.GRPCServiceTLS `yaml:"tls"`
}
@@ -0,0 +1,20 @@
package config
import "github.com/qsfera/server/pkg/shared"
// HTTP defines the available http configuration.
type HTTP struct {
Addr string `yaml:"addr" env:"SETTINGS_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"`
TLS shared.HTTPServiceTLS `yaml:"tls"`
Namespace string `yaml:"-"`
Root string `yaml:"root" env:"SETTINGS_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"`
CORS CORS `yaml:"cors"`
}
// CORS defines the available cors configuration.
type CORS struct {
AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;SETTINGS_CORS_ALLOW_ORIGINS" desc:"A list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;SETTINGS_CORS_ALLOW_METHODS" desc:"A list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;SETTINGS_CORS_ALLOW_HEADERS" desc:"A list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;SETTINGS_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,57 @@
package parser
import (
"errors"
occfg "github.com/qsfera/server/pkg/config"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/qsfera/server/services/settings/pkg/config/defaults"
"github.com/qsfera/server/pkg/config/envdecode"
)
// ParseConfig loads configuration from known paths.
func ParseConfig(cfg *config.Config) error {
err := occfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
defaults.EnsureDefaults(cfg)
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
defaults.Sanitize(cfg)
if err := defaults.LoadBundles(cfg); err != nil {
return err
}
return Validate(cfg)
}
func Validate(cfg *config.Config) error {
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
if cfg.Metadata.SystemUserAPIKey == "" {
return shared.MissingSystemUserApiKeyError(cfg.Service.Name)
}
if cfg.SetupDefaultAssignments && cfg.AdminUserID == "" {
return shared.MissingAdminUserID(cfg.Service.Name)
}
if len(cfg.ServiceAccountIDs) == 0 {
return shared.MissingServiceAccountID(cfg.Service.Name)
}
return nil
}
@@ -0,0 +1,6 @@
package config
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;SETTINGS_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,6 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
}
@@ -0,0 +1,45 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
// Namespace defines the namespace for the defines metrics.
Namespace = "qsfera"
// Subsystem defines the subsystem for the defines metrics.
Subsystem = "settings"
)
// Metrics defines the available metrics of this service.
type Metrics struct {
// Counter *prometheus.CounterVec
BuildInfo *prometheus.GaugeVec
}
// New initializes the available metrics.
func New() *Metrics {
m := &Metrics{
// Counter: prometheus.NewCounterVec(prometheus.CounterOpts{
// Namespace: Namespace,
// Subsystem: Subsystem,
// Name: "greet_total",
// Help: "How many greeting requests processed",
// }, []string{}),
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "build_info",
Help: "Build information",
}, []string{"version"}),
}
// prometheus.Register(
// m.Counter,
// )
_ = prometheus.Register(
m.BuildInfo,
)
return m
}
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/settings/pkg/config"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Context context.Context
Config *config.Config
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
@@ -0,0 +1,38 @@
package debug
import (
"net/http"
"github.com/qsfera/server/pkg/checks"
"github.com/qsfera/server/pkg/handlers"
"github.com/qsfera/server/pkg/service/debug"
"github.com/qsfera/server/pkg/version"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
checkHandler := handlers.NewCheckHandler(
handlers.NewCheckHandlerConfiguration().
WithLogger(options.Logger).
WithCheck("web reachability", checks.NewHTTPCheck(options.Config.HTTP.Addr)).
WithCheck("grpc reachability", checks.NewGRPCCheck(options.Config.GRPC.Addr)),
)
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.GetString()),
debug.Address(options.Config.Debug.Addr),
debug.Token(options.Config.Debug.Token),
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
debug.Health(checkHandler),
debug.Ready(checkHandler),
debug.CorsAllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
debug.CorsAllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
debug.CorsAllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
debug.CorsAllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
), nil
}
@@ -0,0 +1,85 @@
package grpc
import (
"context"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/qsfera/server/services/settings/pkg/metrics"
"github.com/qsfera/server/services/settings/pkg/settings"
"go.opentelemetry.io/otel/trace"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Name string
Logger log.Logger
Context context.Context
Config *config.Config
Metrics *metrics.Metrics
ServiceHandler settings.ServiceHandler
TraceProvider trace.TracerProvider
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Name provides a name for the service.
func Name(val string) Option {
return func(o *Options) {
o.Name = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Metrics provides a function to set the metrics option.
func Metrics(val *metrics.Metrics) Option {
return func(o *Options) {
o.Metrics = val
}
}
// ServiceHandler provides a function to set the ServiceHandler option
func ServiceHandler(val settings.ServiceHandler) Option {
return func(o *Options) {
o.ServiceHandler = val
}
}
// TraceProvider provides a function to set the TraceProvider option
func TraceProvider(val trace.TracerProvider) Option {
return func(o *Options) {
o.TraceProvider = val
}
}
@@ -0,0 +1,85 @@
package grpc
import (
"context"
permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
"github.com/qsfera/server/pkg/service/grpc"
"github.com/qsfera/server/pkg/version"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"go-micro.dev/v4/api"
"go-micro.dev/v4/server"
)
// Server initializes a new go-micro service ready to run
func Server(opts ...Option) grpc.Service {
options := newOptions(opts...)
service, err := grpc.NewServiceWithClient(
options.Config.GrpcClient,
grpc.TLSEnabled(options.Config.GRPC.TLS.Enabled),
grpc.TLSCert(
options.Config.GRPC.TLS.Cert,
options.Config.GRPC.TLS.Key,
),
grpc.Logger(options.Logger),
grpc.Name(options.Name),
grpc.Version(version.GetString()),
grpc.Address(options.Config.GRPC.Addr),
grpc.Namespace(options.Config.GRPC.Namespace),
grpc.Context(options.Context),
grpc.TraceProvider(options.TraceProvider),
)
if err != nil {
options.Logger.Fatal().Err(err).Msg("Error creating settings service")
}
handle := options.ServiceHandler
if err := settingssvc.RegisterBundleServiceHandler(service.Server(), handle); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register Bundle service handler")
}
if err := settingssvc.RegisterValueServiceHandler(service.Server(), handle); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register Value service handler")
}
if err := settingssvc.RegisterRoleServiceHandler(service.Server(), handle); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register Role service handler")
}
if err := settingssvc.RegisterPermissionServiceHandler(service.Server(), handle); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register Permission service handler")
}
if err := RegisterCS3PermissionsServiceHandler(service.Server(), handle); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register CS3 Permission service handler")
}
return service
}
func RegisterCS3PermissionsServiceHandler(s server.Server, hdlr permissions.PermissionsAPIServer, opts ...server.HandlerOption) error {
type permissionsService interface {
CheckPermission(context.Context, *permissions.CheckPermissionRequest, *permissions.CheckPermissionResponse) error
}
type PermissionsAPI struct {
permissionsService
}
h := &permissionsServiceHandler{hdlr}
opts = append(opts, api.WithEndpoint(&api.Endpoint{
Name: "PermissionsService.Checkpermission",
Path: []string{"/api/v0/permissions/check-permission"},
Method: []string{"POST"},
Handler: "rpc",
}))
return s.Handle(s.NewHandler(&PermissionsAPI{h}, opts...))
}
type permissionsServiceHandler struct {
api permissions.PermissionsAPIServer
}
func (h *permissionsServiceHandler) CheckPermission(ctx context.Context, req *permissions.CheckPermissionRequest, res *permissions.CheckPermissionResponse) error {
r, err := h.api.CheckPermission(ctx, req)
if r != nil {
*res = *r
}
return err
}
@@ -0,0 +1,95 @@
package http
import (
"context"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/qsfera/server/services/settings/pkg/metrics"
"github.com/qsfera/server/services/settings/pkg/settings"
"github.com/spf13/pflag"
"go.opentelemetry.io/otel/trace"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Name string
Logger log.Logger
Context context.Context
Config *config.Config
Metrics *metrics.Metrics
ServiceHandler settings.ServiceHandler
Flags []pflag.Flag
TraceProvider trace.TracerProvider
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Name provides a name for the service.
func Name(val string) Option {
return func(o *Options) {
o.Name = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Metrics provides a function to set the metrics option.
func Metrics(val *metrics.Metrics) Option {
return func(o *Options) {
o.Metrics = val
}
}
// Flags provides a function to set the flags option.
func Flags(flags ...pflag.Flag) Option {
return func(o *Options) {
o.Flags = append(o.Flags, flags...)
}
}
// ServiceHandler provides a function to set the ServiceHandler option
func ServiceHandler(val settings.ServiceHandler) Option {
return func(o *Options) {
o.ServiceHandler = val
}
}
// TraceProvider provides a function to set the TraceProvider option
func TraceProvider(val trace.TracerProvider) Option {
return func(o *Options) {
o.TraceProvider = val
}
}
@@ -0,0 +1,93 @@
package http
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/qsfera/server/pkg/account"
"github.com/qsfera/server/pkg/cors"
"github.com/qsfera/server/pkg/middleware"
ohttp "github.com/qsfera/server/pkg/service/http"
"github.com/qsfera/server/pkg/tracing"
"github.com/qsfera/server/pkg/version"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/riandyrn/otelchi"
"go-micro.dev/v4"
)
// Server initializes the http service and server.
func Server(opts ...Option) (ohttp.Service, error) {
options := newOptions(opts...)
service, err := ohttp.NewService(
ohttp.TLSConfig(options.Config.HTTP.TLS),
ohttp.Logger(options.Logger),
ohttp.Name(options.Name),
ohttp.Version(version.GetString()),
ohttp.Address(options.Config.HTTP.Addr),
ohttp.Namespace(options.Config.HTTP.Namespace),
ohttp.Context(options.Context),
ohttp.Flags(options.Flags...),
)
if err != nil {
options.Logger.Error().
Err(err).
Msg("Error initializing http service")
return ohttp.Service{}, fmt.Errorf("could not initialize http service: %w", err)
}
handle := options.ServiceHandler
mux := chi.NewMux()
mux.Use(chimiddleware.RealIP)
mux.Use(chimiddleware.RequestID)
mux.Use(middleware.NoCache)
mux.Use(middleware.Cors(
cors.Logger(options.Logger),
cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
))
mux.Use(middleware.ExtractAccountUUID(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret)),
)
mux.Use(middleware.Version(
options.Name,
version.GetString(),
))
mux.Use(middleware.Logger(
options.Logger,
))
mux.Use(
otelchi.Middleware(
options.Name,
otelchi.WithChiRoutes(mux),
otelchi.WithTracerProvider(options.TraceProvider),
otelchi.WithPropagators(tracing.GetPropagator()),
),
)
mux.Route(options.Config.HTTP.Root, func(r chi.Router) {
settingssvc.RegisterBundleServiceWeb(r, handle)
settingssvc.RegisterValueServiceWeb(r, handle)
settingssvc.RegisterRoleServiceWeb(r, handle)
settingssvc.RegisterPermissionServiceWeb(r, handle)
})
_ = chi.Walk(mux, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
return nil
})
micro.RegisterHandler(service.Server(), mux)
return service, nil
}
@@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com
[o:qsfera-eu:p:qsfera-eu:r:qsfera-settings]
file_filter = locale/<lang>/LC_MESSAGES/settings.po
minimum_perc = 75
resource_name = qsfera-settings
source_file = settings.pot
source_lang = en
type = PO
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Ivan Fustero, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Ivan Fustero, 2025\n"
"Language-Team: Catalan (https://app.transifex.com/qsfera-eu/teams/204053/ca/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ca\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "S'ha afegit com a membre de l'espai"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Diàriament"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Interval d'enviament de correu"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "S'ha rebutjat el fitxer"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Immediat"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Mai"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Notifica'm quan m'hagin afegit com a membre a un espai"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Notifica'm quan m'hagin tret com a membre d'un espai"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Notifica quan rebi una compartició"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Notifica'm quan un fitxer que he pujat ha estat rebutjat per una infecció "
"per virus o una violació de la política"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Notifica'm quan s'hagi eliminat una compartició rebuda"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Notifica'm quan hagi caducat una compartició rebuda"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Notifica'm quan s'hagi suprimit un espai del qual soc membre"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Notifica'm quan s'hagi desactivat un espai del qual soc membre"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Notifica'm quan hagi caducat la pertinença a un espai"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Eliminat com a membre de l'espai"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valor seleccionat:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Compartició caducada"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Compartició rebuda"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Compartició eliminada"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Espai eliminat"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Espai desactivat"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "La pertinença a l'espai ha expirat"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Setmanalment"
@@ -0,0 +1,151 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jörn Friedrich Dreyer <jfd@butonic.de>, 2025
# Jannick Kuhr, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-20 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jannick Kuhr, 2026\n"
"Language-Team: German (https://app.transifex.com/qsfera-eu/teams/204053/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Als Space-Mitglied hinzugefügt"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Täglich"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "E-Mail-Versandintervall"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Datei abgelehnt"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Sofort"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Nie"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr ""
"Benachrichtigung, wenn ich zu einem Space als Mitglied hinzugefügt wurde"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr ""
"Benachrichtigung, wenn ich als Mitglied aus einem Space entfernt wurde"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Benachrichtigung, wenn ich eine Freigabe empfangen habe"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Benachrichtigung, wenn eine hochgeladene Datei aufgrund einer Vireninfektion"
" oder eines Verstoßes gegen die Richtlinien abgelehnt wurde"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Benachrichtigung, wenn eine empfangene Freigabe entfernt wurde"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Benachrichtigung, wenn eine empfangene Freigabe abgelaufen ist"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr ""
"Benachrichtigung, wenn ein Space, in dem ich Mitglied bin, gelöscht wurde"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Benachrichtigung, wenn ein Space, in dem ich Mitglied bin, deaktiviert wurde"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Benachrichtigung, wenn eine Space-Mitgliedschaft abgelaufen ist"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Space-Mitgliedschaft entfernt"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Eingestellter Wert:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Freigabe abgelaufen"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Freigabe empfangen"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Freigabe entfernt"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Space gelöscht"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Space deaktiviert"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Space-Mitgliedschaft abgelaufen"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Wöchentlich"
@@ -0,0 +1,147 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Efstathios Iosifidis <eiosifidis@gmail.com>, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-23 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Efstathios Iosifidis <eiosifidis@gmail.com>, 2026\n"
"Language-Team: Greek (https://app.transifex.com/qsfera-eu/teams/204053/el/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: el\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Προσθήκη ως μέλος χώρου"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Καθημερινά"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Συχνότητα αποστολής email"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Απόρριψη αρχείου"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Άμεσα"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Ποτέ"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Ειδοποίηση όταν προστίθεμαι ως μέλος σε έναν χώρο"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Ειδοποίηση όταν αφαιρούμαι από μέλος ενός χώρου"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Ειδοποίηση όταν λαμβάνω έναν διαμοιρασμό"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Ειδοποίηση όταν ένα αρχείο που ανέβασα απορρίφθηκε λόγω μόλυνσης από ιό ή "
"παραβίασης πολιτικής"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Ειδοποίηση όταν ένας ληφθείς διαμοιρασμός έχει αφαιρεθεί"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Ειδοποίηση όταν ένας ληφθείς διαμοιρασμός έχει λήξει"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Ειδοποίηση όταν ένας χώρος στον οποίο είμαι μέλος έχει διαγραφεί"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Ειδοποίηση όταν ένας χώρος στον οποίο είμαι μέλος έχει απενεργοποιηθεί"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Ειδοποίηση όταν η ιδιότητα μέλους σε έναν χώρο έχει λήξει"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Αφαίρεση από μέλος χώρου"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Επιλεγμένη τιμή:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Λήξη διαμοιρασμού"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Λήψη διαμοιρασμού"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Αφαίρεση διαμοιρασμού"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Διαγραφή χώρου"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Απενεργοποίηση χώρου"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Λήξη ιδιότητας μέλους χώρου"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Εβδομαδιαία"
@@ -0,0 +1,147 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Elías Martín, 2025
# Alejandro Robles, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Alejandro Robles, 2025\n"
"Language-Team: Spanish (https://app.transifex.com/qsfera-eu/teams/204053/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: es\n"
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Incluido como miembro del espacio"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Diario"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Intervalo de envío de correos"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Archivo rechazado"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instantaneo"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Nunca"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Notificar cuando me han agregado como miembro a un espacio"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Notificar cuando he sido eliminado como miembro de un espacio"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Notificarme cuando he recibido un compartido"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Notificar cuando un archivo que he subido ha sido rechazado por contener "
"virus o violar alguna política"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Notificar cuando un compartido que he recibido ha sido eliminado"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Notificar cuando un compartido recibido ha expirado."
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Notificar cuando un espacio del que soy miembro ha sido eliminado"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Notificar cuando un espacio del que soy miembro ha sido deshabilitado"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Notificar cuando la membresía a un espacio ha caducado"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Eliminado como miembro del espacio"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valor seleccionado:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Compartido expirado"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Compartido recibido"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Compartido eliminado"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Espacio eliminado"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Espacio inhabilitado"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Membresia al espacio expirada"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Semanal"
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jiri Grönroos <jiri.gronroos@iki.fi>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-07 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/qsfera-eu/teams/204053/fi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Lisätty avaruuden jäseneksi"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Päivittäin"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Sähköpostin lähetyksen aikaväli"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Tiedosto hylätty"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Välittömästi"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Ei koskaan"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Ilmoita kun minut lisätty jäseneksi avaruuteen"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Ilmoita kun jäsenyyteni avaruudesta on poistettu"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Ilmoita kun olen vastaanottanut jaon"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Ilmoita, kun lähettämäni tiedosto hylättiin joko virustartunnan tai "
"käytäntöloukkauksen vuoksi"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Ilmoita kun vastaanotettu jako on poistettu"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Ilmoita kun vastaanotettu jako on vanhentunut"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Ilmoita, kun avaruus, jonka jäsen olen, on poistettu"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Ilmoita, kun avaruus, jonka jäsen olen, on poistettu käytöstä"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Ilmoita, kun avaruuden jäsenyys on vanhentunut"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Poistettu avaruuden jäsenyydestä"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valittu arvo:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Jako vanheni"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Jako vastaanotettu"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Jako poistettu"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Avaruus poistettu"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Avaruus poistettu käytöstä"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Avaruuden jäsenyys vanhentunut"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Viikottain"
@@ -0,0 +1,148 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# eric_G <junk.eg@free.fr>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: eric_G <junk.eg@free.fr>, 2025\n"
"Language-Team: French (https://app.transifex.com/qsfera-eu/teams/204053/fr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fr\n"
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Ajouté comme membre de l'Espace"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Quotidiennement"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Intervalle d'envoi des e-mails"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Fichier rejeté"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instantané"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Jamais"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr ""
"Recevoir une notification lorsque j'ai été ajouté en tant que membre à un "
"espace"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "M'avertir lorsque j'ai été retiré d'un Espace en tant que membre"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Recevoir une notification lorsque j'ai reçu un partage"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Notifier lorsqu'un fichier que j'ai téléchargé a été rejeté en raison d'une "
"infection virale ou d'une violation de politique."
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Notifier la suppression d'un partage reçu"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Notifier l'expiration d'un partage reçue"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Notifier lorsqu'un espace dont je suis membre a été supprimé"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Notifier lorsqu'un Espace dont je suis membre a été désactivé"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Notifier l'expiration d'une adhésion à un Espace"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Supprimé en tant que membre de l'Espace"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valeur sélectionnée :"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Partage expiré"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Partage reçue"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Partage supprimée"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Espace supprimé"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Espace désactivé"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "L'adhésion à l'espace a expiré"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Hebdomadaire"
@@ -0,0 +1,147 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# mitibor, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-04 07:46+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: mitibor, 2026\n"
"Language-Team: Hungarian (https://app.transifex.com/qsfera-eu/teams/204053/hu/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: hu\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Hozzáadva tárhelytagként"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Naponta"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "E-mail küldési időköz"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Fájl elutasítva"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Azonnal"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Soha"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Értesítést kérek, ha hozzáadtak egy tárhelyhez tagként"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Értesítést kérek, ha eltávolítottak egy tárhely tagjai közül"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Értesítést kérek, ha megosztást kaptam"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Értesítést kérek, ha egy általam feltöltött fájl elutasításra került "
"vírusfertőzés vagy szabályszegés miatt"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Értesítést kérek, ha egy kapott megosztást eltávolítottak"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Értesítést kérek, ha egy kapott megosztás lejárt"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Értesítést kérek, ha egy tárhelyet, amelynek tagja vagyok, töröltek"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Értesítést kérek, ha egy tárhelyet, amelynek tagja vagyok, letiltottak"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Értesítést kérek, ha egy tárhelytagság lejárt"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Eltávolítva a tárhelytagok közül"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Kiválasztott érték:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Megosztás lejárt"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Megosztás érkezett"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Megosztás eltávolítva"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Tárhely törölve"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Tárhely letiltva"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Tárhelytagság lejárt"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Hetente"
@@ -0,0 +1,150 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# idoet <idoet@protonmail.ch>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-06 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: idoet <idoet@protonmail.ch>, 2025\n"
"Language-Team: Indonesian (https://app.transifex.com/qsfera-eu/teams/204053/id/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: id\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Ditambahkan menjadi anggota ruang penyimpanan"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Harian"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Interval pengiriman email"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Berkas ditolak"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instan"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Tidak pernah"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr ""
"Beri tahu ketika saya telah ditambahkan menjadi anggota dari suatu ruang "
"penyimpanan"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr ""
"Beri tahu ketika saya telah dihapus dari keanggotaan suatu ruang penyimpanan"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Beri tahu ketika saya telah menerima berbagi"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Beri tahu jika file yang saya unggah ditolak karena infeksi virus atau "
"pelanggaran kebijakan"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Beri tahu saat berbagi yang diterima telah dihapus"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Beri tahu ketika berbagi yang diterima telah berakhir"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Beri tahu ketika ruang penyimpanan yang saya ikuti telah dihapus"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Beri tahu ketika ruang penyimpanan yang saya ikuti telah dinonaktifkan"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Beri tahu saat keanggotaan ruang penyimpanan telah berakhir"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Dihapus dari anggota ruang penyimpanan"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Yang dipilih:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Berbagi Berakhir"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Berbagi Diterima"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Berbagi Dihapus"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Ruang penyimpanan dihapus"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Ruang penyimpanan dinonaktifkan"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Keanggotaan ruang penyimpanan telah berakhir"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Mingguan"
@@ -0,0 +1,148 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Simone Pagano, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Simone Pagano, 2025\n"
"Language-Team: Italian (https://app.transifex.com/qsfera-eu/teams/204053/it/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: it\n"
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Aggiunto come membro di uno spazio"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Giornaliero"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Intervallo di invio delle e-mail"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "File rifiutato"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Istantaneo"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Mai"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Inviami una notifica quando vengo aggiunto come membro di uno spazio"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Inviami una notifica quando vengo rimosso come membro di uno spazio"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Inviami una notifica quando ricevo una condivisione"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Inviami una notifica quando un file che ho caricato viene rifiutato a causa "
"di una violazione delle policy o a causa della presenza di virus."
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Inviami una notifica quando una condivisione viene rimossa"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Inviami una notifica quando una condivisione ricevuta scade"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr ""
"Inviami una notifica quando uno spazio di cui sono membro viene eliminato"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Inviami una notifica quando uno spazio di cui sono membro viene disattivato"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Inviami una notifica quando uno spazio di cui sono membro scade"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Non più un membro dello spazio"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valore selezionato: "
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Condivisione scaduta"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Condivisione ricevuta"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Condivisione rimossa"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Spazio eliminato"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Spazio disattivato"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Partecipazione ad uno spazio scaduta"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Settimanale"
@@ -0,0 +1,144 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# iikaka88, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-28 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: iikaka88, 2025\n"
"Language-Team: Japanese (https://app.transifex.com/qsfera-eu/teams/204053/ja/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ja\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "スペースメンバーへの追加"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "毎日"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "メール送信間隔"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "ファイルの拒否"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "即時"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "しない"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "スペースのメンバーに追加された時に通知する"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "スペースのメンバーから削除された時に通知する"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "共有を受け取った時に通知する"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr "アップロードしたファイルがウイルスやポリシー違反で拒否された時に通知する"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "受け取った共有が削除された時に通知する"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "受け取った共有の期限が切れた時に通知する"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "参加しているスペースが削除された時に通知する"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "参加しているスペースが無効化された時に通知する"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "スペースのメンバーシップ期限が切れた時に通知する"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "スペースメンバーからの削除"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "選択された値:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "共有期限切れ"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "共有受け取った時"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "共有削除"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "スペース削除"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "スペース無効化"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "スペース期限切れ"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "毎週"
@@ -0,0 +1,144 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# gapho shin, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: gapho shin, 2025\n"
"Language-Team: Korean (https://app.transifex.com/qsfera-eu/teams/204053/ko/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ko\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "스페이스 멤버로 추가됨"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "일간"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "이메일 전송 간격"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "파일 거부"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "즉시"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "지속"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "내가 공간에 멤버로 추가되었을 때 알림"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "내가 공간에서 멤버로 제거되었을 때 알림"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "공유 받으면 알림"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr "바이러스 감염 또는 정책 위반으로 인해 업로드한 파일이 거부되었을 때 알림"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "허가된 공유가 제거되면 알림"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "허가된 공유가 만료되면 알림"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "내가 속한 공간이 삭제되었을 때 알림"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "내가 속한 공간이 비활성화되었을 때 알림"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "스페이스 멤버십이 만료되면 알림"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "스페이스 멤버가 제거됨"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "선택한 값:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "만료된 공유"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "공유됨"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "공유제거"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "스페이스 삭제"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "스페이스 비활성화"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "스페이스 멤버십 만료"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "주간"
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# BoneNI, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-30 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: BoneNI, 2026\n"
"Language-Team: Lao (https://app.transifex.com/qsfera-eu/teams/204053/lo/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: lo\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "ຖືກເພີ່ມເປັນສະມາຊິກພື້ນທີ່"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "ປະຈຳວັນ"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "ໄລຍະຫ່າງການສົ່ງອີເມລ"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "ໄຟລ໌ຖືກປະຕິເສດ"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "ທັນທີ"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "ບໍ່ເຄີຍ"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "ແຈ້ງເຕືອນເມື່ອຂ້ອຍຖືກເພີ່ມເປັນສະມາຊິກໃນພື້ນທີ່"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "ແຈ້ງເຕືອນເມື່ອຂ້ອຍຖືກຖອນອອກຈາກການເປັນສະມາຊິກໃນພື້ນທີ່"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "ແຈ້ງເຕືອນເມື່ອຂ້ອຍໄດ້ຮັບການແບ່ງປັນ"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"ແຈ້ງເຕືອນເມື່ອໄຟລ໌ທີ່ຂ້ອຍອັບໂຫລດຖືກປະຕິເສດ ເນື່ອງຈາກກວດພົບໄວຣັສ ຫຼື "
"ລະເມີດນະໂຍບາຍ"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "ແຈ້ງເຕືອນເມື່ອການແບ່ງປັນທີ່ໄດ້ຮັບຖືກຖອນອອກ"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "ແຈ້ງເຕືອນເມື່ອການແບ່ງປັນທີ່ໄດ້ຮັບໝົດອາຍຸ"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "ແຈ້ງເຕືອນເມື່ອພື້ນທີ່ທີ່ຂ້ອຍເປັນສະມາຊິກຖືກລຶບ"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "ແຈ້ງເຕືອນເມື່ອພື້ນທີ່ທີ່ຂ້ອຍເປັນສະມາຊິກຖືກປິດການໃຊ້ງານ"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "ແຈ້ງເຕືອນເມື່ອການເປັນສະມາຊິກພື້ນທີ່ໝົດອາຍຸ"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "ຖືກຖອນອອກຈາກການເປັນສະມາຊິກພື້ນທີ່"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "ຄ່າທີ່ເລືອກ:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "ການແບ່ງປັນໝົດອາຍຸ"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "ໄດ້ຮັບການແບ່ງປັນ"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "ການແບ່ງປັນຖືກຖອນອອກ"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "ພື້ນທີ່ຖືກລຶບ"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "ພື້ນທີ່ຖືກປິດການໃຊ້ງານ"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "ການເປັນສະມາຊິກພື້ນທີ່ໝົດອາຍຸ"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "ປະຈຳອາທິດ"
@@ -0,0 +1,147 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Stephan Paternotte <stephan@paternottes.net>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-26 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/qsfera-eu/teams/204053/nl/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: nl\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Toegevoegd als lid aan ruimte"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Dagelijks"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Interval van e-mails"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Bestand afgewezen"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instantaan"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Nooit"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Stuur een melding wanneer ik als lid aan een ruimte ben toegevoegd"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Meld me wanneer ik ben verwijderd als lid van een ruimte"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Stuur een melding wanneer iemand iets met mij deelt"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Stuur een melding wanneer een bestand dat ik heb geüpload is afgewezen "
"vanwege een virusinfectie of schending van het beleid."
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Stuur een melding wanneer een share met mij is ingetrokken"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Stuur een melding wanneer een share met mij is verlopen"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Stuur een melding wanneer een ruimte waarvan ik lid ben is verwijderd"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Stuur een melding wanner een ruimte waarvan ik lid ben is uitgeschakeld"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Stuur een melding waneer het lidmaatschap aan een ruimte is verlopen"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Verwijderd als lid van een ruimte"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Geselecteerde waarde:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Share verlopen"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Share ontvangen"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Share ingetrokken"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Ruimte verwijderd"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Ruimte uitgeschakeld"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Lidmaatschap van ruimte verlopen"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Wekelijks"
@@ -0,0 +1,148 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# skrzat <skrzacikus@gmail.com>, 2025
# Xiaomi Box, 2025
# Radoslaw Posim, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-06 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Radoslaw Posim, 2025\n"
"Language-Team: Polish (https://app.transifex.com/qsfera-eu/teams/204053/pl/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pl\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Dodano jako członek Przestrzeni"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Dziennie"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Interwał wysyłania email"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Plik odrzucony"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Natychmiast"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Nigdy"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Powiadom, jeśli dodano mnie jako członka przestrzeni"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Powiadom, jeśli usunięto mnie jako członka z przestrzeni."
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Powiadom o otrzymanym udostępnieniu."
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Poinformuj, jeśli przesłany plik został odrzucony z powodu wirusa lub "
"naruszenia zasad"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Powiadom, jeśli otrzymane udostępnienie zostało usunięte"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Powiadom, jeśli otrzymane udostępnienie wygasło"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Powiadom, jeśli Przestrzeń gdzie jestem członkiem została usunięta"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Poinformuj, jeśli przestrzeń gdzie jestem członkiem została wyłączona"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Powiadom, jeśli członkostwo w przestrzeni wygasło"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Usunięto z przestrzeni"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Wybrana wartość:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Udostępnienie wygasło"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Otrzymano udostępnienie"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Usunięto udostępnienie"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Przestrzeń usunięta"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Przestrzeń wyłączona"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Członkostwo w przestrzeni wygasło"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Co tydzień"
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Mário Machado, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-06 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/qsfera-eu/teams/204053/pt/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt\n"
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Adicionado como membro do espaço"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Diário"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Intervalo de envio de e-mail"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Ficheiro rejeitado"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instantâneo"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Nunca"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Notificar quando for adicionado como membro a um espaço"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Notificar quando for removido como membro de um espaço"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Notificar quando receber uma partilha"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Notificar quando um ficheiro que carreguei for rejeitado devido a uma "
"infeção por vírus ou violação de política"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Notificar quando uma partilha recebida for removida"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Notificar quando uma partilha recebida expirar"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Notificar quando um espaço do qual sou membro for eliminado"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Notificar quando um espaço do qual sou membro for desativado"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Notificar quando uma subscrição de espaço expirar"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Removido como membro do espaço"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valor selecionado:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Partilha expirada"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Partilha recebida"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Partilha removida"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Espaço eliminado"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Espaço desativado"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Subscrição de espaço expirada"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Semanal"
@@ -0,0 +1,151 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# yellow sky, 2025
# Lulufox, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Lulufox, 2025\n"
"Language-Team: Russian (https://app.transifex.com/qsfera-eu/teams/204053/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ru\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Добавлен как член пространства"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Ежедневно"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Интервал отправки электронных писем"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Файл отклонен"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Немедленно"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Никогда"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Уведомлять, когда меня добавляют в пространства"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Уведомлять, когда меня исключают из пространств"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr ""
"Уведомлять, когда мне предоставили доступ для совместного использования "
"ресура"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Уведомлять, когда в загруженный мной файл был отклонен из-за проверки на "
"вирусы"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr ""
"Уведомлять, когда у меня забрали доступ для совместного использования "
"ресурса"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Уведомлять, когда доступ для совместного использования ресурса истек"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Уведомлять, когда пространство, в котором я состою, было удалено"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Уведомлять, когда пространство, в котором я состою, было отключено"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Уведомлять, когда мое членство в пространстве истекло"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Удален как участник пространства"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Выбранное значение:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Доступ для совместного использования истек"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Получен доступ для совместного использования ресурса"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Доступ для совместного использования ресурса отозван"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Пространство удалено"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Пространство отключено"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Членство в пространстве истекло"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Еженедельно"
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Davis Kaza, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Davis Kaza, 2025\n"
"Language-Team: Swedish (https://app.transifex.com/qsfera-eu/teams/204053/sv/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: sv\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Tillagd som medlem i arbetsytan"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Dagligen"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Sändningsintervall för e-post"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Fil nekad"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Omedelbart"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Aldrig"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Meddela mig när jag blivit tillagd som medlem i en arbetsyta"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Meddela mig när jag tagits bort som medlem i en arbetsyta"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Meddela mig när jag tagit emot en delning"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Meddela mig när en fil jag laddat upp har nekats på grund av virus eller "
"policybrott"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Meddela när en mottagen delning har raderats"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Meddela när en mottagen delning har löpt ut"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Meddela när en arbetsyta jag är medlem i har raderats"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Meddela när en arbetsyta jag är medlem i har avaktiverats"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Meddela när ett medlemskap i en arbetsyta har löpt ut"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Borttagen som medlem i arbetsyta"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valt värde:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Delning har löpt ut"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Delning mottagen"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Delning raderad"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Arbetsyta raderad"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Arbetsyta avaktiverad"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Medlemskap i arbetsyta har löpt ut"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Veckovis"
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# LinkinWires <darkinsonic13@gmail.com>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-08 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: LinkinWires <darkinsonic13@gmail.com>, 2025\n"
"Language-Team: Ukrainian (https://app.transifex.com/qsfera-eu/teams/204053/uk/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: uk\n"
"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Додавання до простору у якості учасника (-ці)"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Щоденно"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Інтервал надсилання електронних листів"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Відхилення файлу"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Миттєво"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Ніколи"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Сповіщати, коли мене додають як учасника (-цю) до простору"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Сповіщати, коли мене видаляють з учасників простору"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Сповіщати, коли зі мною поділилися файлом або папкою"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Сповіщати, коли завантажений мною файл було відхилено через знайдений вірус "
"або порушення політик"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Сповіщати, коли зі мною більше не діляться файлом або папкою"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Сповіщати, коли термін доступу до файлу або папки сплинув"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Сповіщати, коли простір, учасником (-цею) якого я є, був видалений"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Сповіщати, коли простір, учасником (-цею) якого я є, був вимкнений"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Сповіщати, коли термін доступу до простору сплинув"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Видалення з простору"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Обране значення:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Сплинення доступу до файлу / папки"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Надання доступу"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Видалення доступу"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Видалення простору"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Вимкнення простору"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Сплинення доступу до простору"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Щотижнево"
@@ -0,0 +1,144 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# YQS Yang, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: YQS Yang, 2025\n"
"Language-Team: Chinese (https://app.transifex.com/qsfera-eu/teams/204053/zh/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "添加为空间成员"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "每日"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "邮件发送频率"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "文件被拒绝"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "实时"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "从不"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "当我被添加为空间成员时通知我"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "当我被从空间中移除成员资格时通知我"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "当我收到共享时通知我"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr "当我上传的文件因病毒感染或违反策略而被拒绝时通知我"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "当收到的共享被移除时通知我"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "当收到的共享已过期时通知我"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "当我所在的空间被删除时通知我"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "当我所在的空间被禁用时通知我"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "当空间成员资格已过期时通知我"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "移除空间成员资格"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "已选值:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "共享已过期"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "收到共享"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "共享已移除"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "空间已删除"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "空间已禁用"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "空间成员资格已过期"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "每周"
@@ -0,0 +1,39 @@
package svc
import (
"net/http"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/settings/pkg/config"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Middleware provides a function to set the middleware option.
func Middleware(val ...func(http.Handler) http.Handler) Option {
return func(o *Options) {
o.Middleware = val
}
}
@@ -0,0 +1,59 @@
package svc
import (
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
)
func (g Service) hasPermission(
roleIDs []string,
resource *settingsmsg.Resource,
operations []settingsmsg.Permission_Operation,
constraint settingsmsg.Permission_Constraint,
) bool {
permissions, err := g.manager.ListPermissionsByResource(resource, roleIDs)
if err != nil {
g.logger.Debug().Err(err).
Str("resource-type", resource.Type.String()).
Str("resource-id", resource.Id).
Msg("permissions could not be loaded for resource")
return false
}
permissions = getFilteredPermissionsByOperations(permissions, operations)
return isConstraintFulfilled(permissions, constraint)
}
// filterPermissionsByOperations returns the subset of the given permissions, where at least one of the given operations is fulfilled.
func getFilteredPermissionsByOperations(permissions []*settingsmsg.Permission, operations []settingsmsg.Permission_Operation) []*settingsmsg.Permission {
var filteredPermissions []*settingsmsg.Permission
for _, permission := range permissions {
if isAnyOperationFulfilled(permission, operations) {
filteredPermissions = append(filteredPermissions, permission)
}
}
return filteredPermissions
}
// isAnyOperationFulfilled checks if the permissions is about any of the operations
func isAnyOperationFulfilled(permission *settingsmsg.Permission, operations []settingsmsg.Permission_Operation) bool {
for _, operation := range operations {
if operation == permission.Operation {
return true
}
}
return false
}
// isConstraintFulfilled checks if one of the permissions has the same or a parent of the constraint.
// this is only a comparison on ENUM level. More sophisticated checks cannot happen here...
func isConstraintFulfilled(permissions []*settingsmsg.Permission, constraint settingsmsg.Permission_Constraint) bool {
for _, permission := range permissions {
// comparing enum by order is not a feasible solution, because `SHARED` is not a superset of `OWN`.
if permission.Constraint == settingsmsg.Permission_CONSTRAINT_ALL {
return true
}
if permission.Constraint != settingsmsg.Permission_CONSTRAINT_UNKNOWN && permission.Constraint == constraint {
return true
}
}
return false
}
@@ -0,0 +1,720 @@
package svc
import (
"context"
"embed"
"errors"
"fmt"
"strings"
cs3permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/leonelquinteros/gotext"
"github.com/qsfera/server/pkg/l10n"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/middleware"
"github.com/qsfera/server/pkg/roles"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/qsfera/server/services/settings/pkg/settings"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
metastore "github.com/qsfera/server/services/settings/pkg/store/metadata"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
"google.golang.org/protobuf/types/known/emptypb"
)
//go:embed l10n/locale
var _translationFS embed.FS
var _domain = "settings"
// Service represents a service.
type Service struct {
id string
config *config.Config
logger log.Logger
manager settings.Manager
}
// NewService returns a service implementation for Service.
func NewService(cfg *config.Config, logger log.Logger) settings.ServiceHandler {
service := Service{
id: "qsfera-settings",
config: cfg,
logger: logger,
}
service.manager = metastore.New(cfg)
return service
}
// CheckPermission implements the CS3 API Permssions service.
// It's used to check if a subject (user or group) has a permission.
func (g Service) CheckPermission(ctx context.Context, req *cs3permissions.CheckPermissionRequest) (*cs3permissions.CheckPermissionResponse, error) {
spec := req.GetSubjectRef().GetSpec()
var accountID string
switch ref := spec.(type) {
case *cs3permissions.SubjectReference_UserId:
accountID = ref.UserId.GetOpaqueId()
case *cs3permissions.SubjectReference_GroupId:
accountID = ref.GroupId.GetOpaqueId()
}
assignments, err := g.manager.ListRoleAssignments(accountID)
if err != nil {
return &cs3permissions.CheckPermissionResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
roleIDs := make([]string, 0, len(assignments))
for _, a := range assignments {
roleIDs = append(roleIDs, a.GetRoleId())
}
permission, err := g.manager.ReadPermissionByName(req.GetPermission(), roleIDs)
if err != nil {
if !errors.Is(err, settings.ErrNotFound) {
return &cs3permissions.CheckPermissionResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
if permission == nil {
return &cs3permissions.CheckPermissionResponse{
Status: &rpcv1beta1.Status{
Code: rpcv1beta1.Code_CODE_PERMISSION_DENIED,
},
}, nil
}
return &cs3permissions.CheckPermissionResponse{
Status: status.NewOK(ctx),
}, nil
}
// TODO: check permissions on every request
// SaveBundle implements the BundleServiceHandler interface
func (g Service) SaveBundle(ctx context.Context, req *settingssvc.SaveBundleRequest, res *settingssvc.SaveBundleResponse) error {
cleanUpResource(ctx, req.GetBundle().GetResource())
if err := g.checkStaticPermissionsByBundleType(ctx, req.GetBundle().GetType()); err != nil {
return err
}
if validationError := validateSaveBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.WriteBundle(req.GetBundle())
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
res.Bundle = r
return nil
}
// GetBundle implements the BundleServiceHandler interface
func (g Service) GetBundle(ctx context.Context, req *settingssvc.GetBundleRequest, res *settingssvc.GetBundleResponse) error {
if validationError := validateGetBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
bundle, err := g.manager.ReadBundle(req.GetBundleId())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
filteredBundle := g.getFilteredBundle(g.getRoleIDs(ctx), bundle)
if len(filteredBundle.GetSettings()) == 0 {
err = fmt.Errorf("could not read bundle: %s", req.GetBundleId())
return merrors.NotFound(g.id, "%s", err)
}
res.Bundle = filteredBundle
return nil
}
// ListBundles implements the BundleServiceHandler interface
func (g Service) ListBundles(ctx context.Context, req *settingssvc.ListBundlesRequest, res *settingssvc.ListBundlesResponse) error {
// fetch all bundles
if validationError := validateListBundles(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
bundles, err := g.manager.ListBundles(settingsmsg.Bundle_TYPE_DEFAULT, req.GetBundleIds())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
roleIDs := g.getRoleIDs(ctx)
// find user locale
var locale string
if u, ok := ctxpkg.ContextGetUser(ctx); ok {
var err error
locale, err = g.getUserLocale(ctx, u.GetId().GetOpaqueId())
if err != nil {
g.logger.Error().Err(err).Str("userid", u.GetId().GetOpaqueId()).Msg("failed to get user locale")
}
}
// filter settings in bundles that are allowed according to roles
var filteredBundles []*settingsmsg.Bundle
for _, bundle := range bundles {
filteredBundle := g.getFilteredBundle(roleIDs, bundle)
if len(filteredBundle.GetSettings()) > 0 {
t := l10n.NewTranslatorFromCommonConfig(g.config.DefaultLanguage, _domain, g.config.TranslationPath, _translationFS, "l10n/locale").Locale(locale)
filteredBundles = append(filteredBundles, translateBundle(filteredBundle, t))
}
}
res.Bundles = filteredBundles
return nil
}
func (g Service) getFilteredBundle(roleIDs []string, bundle *settingsmsg.Bundle) *settingsmsg.Bundle {
// check if full bundle is whitelisted
bundleResource := &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
Id: bundle.GetId(),
}
if g.hasPermission(
roleIDs,
bundleResource,
[]settingsmsg.Permission_Operation{settingsmsg.Permission_OPERATION_READ, settingsmsg.Permission_OPERATION_READWRITE},
settingsmsg.Permission_CONSTRAINT_OWN,
) {
return bundle
}
// filter settings based on permissions
var filteredSettings []*settingsmsg.Setting
for _, setting := range bundle.GetSettings() {
settingResource := &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: setting.GetId(),
}
if g.hasPermission(
roleIDs,
settingResource,
[]settingsmsg.Permission_Operation{settingsmsg.Permission_OPERATION_READ, settingsmsg.Permission_OPERATION_READWRITE},
settingsmsg.Permission_CONSTRAINT_OWN,
) {
filteredSettings = append(filteredSettings, setting)
}
}
bundle.Settings = filteredSettings
return bundle
}
// AddSettingToBundle implements the BundleServiceHandler interface
func (g Service) AddSettingToBundle(ctx context.Context, req *settingssvc.AddSettingToBundleRequest, res *settingssvc.AddSettingToBundleResponse) error {
cleanUpResource(ctx, req.GetSetting().GetResource())
if err := g.checkStaticPermissionsByBundleID(ctx, req.GetBundleId()); err != nil {
return err
}
if validationError := validateAddSettingToBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.AddSettingToBundle(req.GetBundleId(), req.GetSetting())
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
res.Setting = r
return nil
}
// RemoveSettingFromBundle implements the BundleServiceHandler interface
func (g Service) RemoveSettingFromBundle(ctx context.Context, req *settingssvc.RemoveSettingFromBundleRequest, _ *emptypb.Empty) error {
if err := g.checkStaticPermissionsByBundleID(ctx, req.GetBundleId()); err != nil {
return err
}
if validationError := validateRemoveSettingFromBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
if err := g.manager.RemoveSettingFromBundle(req.GetBundleId(), req.GetSettingId()); err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
return nil
}
// SaveValue implements the ValueServiceHandler interface
func (g Service) SaveValue(ctx context.Context, req *settingssvc.SaveValueRequest, res *settingssvc.SaveValueResponse) error {
req.Value.AccountUuid = getValidatedAccountUUID(ctx, req.GetValue().GetAccountUuid())
if !g.isCurrentUser(ctx, req.GetValue().GetAccountUuid()) {
return merrors.Forbidden(g.id, "can't save value for another user")
}
cleanUpResource(ctx, req.GetValue().GetResource())
// TODO: we need to check, if the authenticated user has permission to write the value for the specified resource (e.g. global, file with id xy, ...)
if validationError := validateSaveValue(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError.Error())
}
r, err := g.manager.WriteValue(req.GetValue())
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
valueWithIdentifier, err := g.getValueWithIdentifier(r)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
res.Value = valueWithIdentifier
return nil
}
// GetValue implements the ValueServiceHandler interface
func (g Service) GetValue(_ context.Context, req *settingssvc.GetValueRequest, res *settingssvc.GetValueResponse) error {
if validationError := validateGetValue(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.ReadValue(req.GetId())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
valueWithIdentifier, err := g.getValueWithIdentifier(r)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
res.Value = valueWithIdentifier
return nil
}
// GetValueByUniqueIdentifiers implements the ValueService interface
func (g Service) GetValueByUniqueIdentifiers(ctx context.Context, req *settingssvc.GetValueByUniqueIdentifiersRequest, res *settingssvc.GetValueResponse) error {
req.AccountUuid = getValidatedAccountUUID(ctx, req.GetAccountUuid())
if !g.isCurrentUser(ctx, req.GetAccountUuid()) {
return merrors.Forbidden(g.id, "can't get value of another user")
}
if validationError := validateGetValueByUniqueIdentifiers(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
v, err := g.manager.ReadValueByUniqueIdentifiers(req.GetAccountUuid(), req.GetSettingId())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
if v.GetBundleId() != "" {
valueWithIdentifier, err := g.getValueWithIdentifier(v)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
res.Value = valueWithIdentifier
}
return nil
}
// ListValues implements the ValueServiceHandler interface
func (g Service) ListValues(ctx context.Context, req *settingssvc.ListValuesRequest, res *settingssvc.ListValuesResponse) error {
req.AccountUuid = getValidatedAccountUUID(ctx, req.GetAccountUuid())
if !g.isCurrentUser(ctx, req.GetAccountUuid()) {
return merrors.Forbidden(g.id, "can't list values of another user")
}
if validationError := validateListValues(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
values, err := g.manager.ListValues(req.GetBundleId(), req.GetAccountUuid())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
result := make([]*settingsmsg.ValueWithIdentifier, 0, len(values))
for _, value := range values {
valueWithIdentifier, err := g.getValueWithIdentifier(value)
if err == nil {
result = append(result, valueWithIdentifier)
}
}
res.Values = result
return nil
}
// ListRoles implements the RoleServiceHandler interface
func (g Service) ListRoles(_ context.Context, req *settingssvc.ListBundlesRequest, res *settingssvc.ListBundlesResponse) error {
//accountUUID := getValidatedAccountUUID(c, "me")
if validationError := validateListRoles(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.ListBundles(settingsmsg.Bundle_TYPE_ROLE, req.GetBundleIds())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
// TODO: only allow listing roles when user has account/role/... management permissions
res.Bundles = r
return nil
}
// ListRoleAssignments implements the RoleServiceHandler interface
func (g Service) ListRoleAssignments(ctx context.Context, req *settingssvc.ListRoleAssignmentsRequest, res *settingssvc.ListRoleAssignmentsResponse) error {
req.AccountUuid = getValidatedAccountUUID(ctx, req.GetAccountUuid())
if validationError := validateListRoleAssignments(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.ListRoleAssignments(req.GetAccountUuid())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
res.Assignments = r
return nil
}
// ListRoleAssignmentsFiltered implements the RoleServiceHandler interface. Who made this up? And why is everyone copying it? So this methods lists role assignments filtered by account or role.
func (g Service) ListRoleAssignmentsFiltered(ctx context.Context, req *settingssvc.ListRoleAssignmentsFilteredRequest, res *settingssvc.ListRoleAssignmentsResponse) error {
if validationError := validateListRoleAssignmentsFiltered(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
filters := req.GetFilters()
var r []*settingsmsg.UserRoleAssignment
var err error
switch filters[0].GetType() {
case settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT:
accountUUID := getValidatedAccountUUID(ctx, filters[0].GetAccountUuid())
r, err = g.manager.ListRoleAssignments(accountUUID)
case settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE:
roleID := filters[0].GetRoleId()
r, err = g.manager.ListRoleAssignmentsByRole(roleID)
}
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
res.Assignments = r
return nil
}
// AssignRoleToUser implements the RoleServiceHandler interface
func (g Service) AssignRoleToUser(ctx context.Context, req *settingssvc.AssignRoleToUserRequest, res *settingssvc.AssignRoleToUserResponse) error {
req.AccountUuid = getValidatedAccountUUID(ctx, req.GetAccountUuid())
if validationError := validateAssignRoleToUser(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
ownAccountUUID, ok := metadata.Get(ctx, middleware.AccountID)
if !ok {
g.logger.Debug().Str("id", g.id).Msg("user not in context")
return merrors.InternalServerError(g.id, "user not in context")
}
switch {
case ownAccountUUID == req.GetAccountUuid():
// Allow users to assign themself to the user or user light role
// deny any other attempt to change the user's own assignment
if r, err := g.manager.ListRoleAssignments(req.GetAccountUuid()); err == nil && len(r) > 0 {
return merrors.Forbidden(g.id, "Changing own role assignment forbidden")
}
if req.GetRoleId() != defaults.BundleUUIDRoleUser && req.GetRoleId() != defaults.BundleUUIDRoleUserLight {
return merrors.Forbidden(g.id, "Changing own role assignment forbidden")
}
g.logger.Debug().Str("userid", ownAccountUUID).Msg("Self-assignment for default 'user' role permitted")
case g.canManageRoles(ctx):
default:
return merrors.Forbidden(g.id, "user has no role management permission")
}
r, err := g.manager.WriteRoleAssignment(req.GetAccountUuid(), req.GetRoleId())
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
res.Assignment = r
return nil
}
// RemoveRoleFromUser implements the RoleServiceHandler interface
func (g Service) RemoveRoleFromUser(ctx context.Context, req *settingssvc.RemoveRoleFromUserRequest, _ *emptypb.Empty) error {
if !g.canManageRoles(ctx) {
return merrors.Forbidden(g.id, "user has no role management permission")
}
if validationError := validateRemoveRoleFromUser(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
ownAccountUUID, ok := metadata.Get(ctx, middleware.AccountID)
if !ok {
g.logger.Debug().Str("id", g.id).Msg("user not in context")
return merrors.InternalServerError(g.id, "user not in context")
}
al, err := g.manager.ListRoleAssignments(ownAccountUUID)
if err != nil {
g.logger.Debug().Err(err).Str("id", g.id).Msg("ListRoleAssignments failed")
return merrors.InternalServerError(g.id, "%s", err)
}
for _, a := range al {
if a.GetId() == req.GetId() {
g.logger.Debug().Str("id", g.id).Msg("Removing own role assignment forbidden")
return merrors.Forbidden(g.id, "Removing own role assignment forbidden")
}
}
if err := g.manager.RemoveRoleAssignment(req.GetId()); err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
return nil
}
// ListPermissions implements the PermissionServiceHandler interface
func (g Service) ListPermissions(ctx context.Context, req *settingssvc.ListPermissionsRequest, res *settingssvc.ListPermissionsResponse) error {
ownAccountUUID, ok := metadata.Get(ctx, middleware.AccountID)
if !ok {
g.logger.Debug().Str("id", g.id).Msg("user not in context")
return merrors.InternalServerError(g.id, "user not in context")
}
if ownAccountUUID != req.GetAccountUuid() {
return merrors.NotFound(g.id, "user not found: %s", req.GetAccountUuid())
}
assignments, err := g.manager.ListRoleAssignments(req.GetAccountUuid())
if err != nil {
return err
}
// deduplicate role ids
roleIDs := map[string]struct{}{}
for _, a := range assignments {
roleIDs[a.GetRoleId()] = struct{}{}
}
// deduplicate permission names
permissionNames := map[string]struct{}{}
for roleID := range roleIDs {
bundle, err := g.manager.ReadBundle(roleID)
if err != nil {
if !errors.Is(err, settings.ErrNotFound) {
return err
}
continue
}
if bundle != nil {
for _, setting := range bundle.GetSettings() {
permissionNames[formatPermissionName(setting)] = struct{}{}
}
}
}
res.Permissions = make([]string, 0, len(permissionNames))
for p := range permissionNames {
res.Permissions = append(res.Permissions, p)
}
return nil
}
// ListPermissionsByResource implements the PermissionServiceHandler interface
func (g Service) ListPermissionsByResource(ctx context.Context, req *settingssvc.ListPermissionsByResourceRequest, res *settingssvc.ListPermissionsByResourceResponse) error {
if validationError := validateListPermissionsByResource(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
permissions, err := g.manager.ListPermissionsByResource(req.GetResource(), g.getRoleIDs(ctx))
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
res.Permissions = permissions
return nil
}
// GetPermissionByID implements the PermissionServiceHandler interface
func (g Service) GetPermissionByID(ctx context.Context, req *settingssvc.GetPermissionByIDRequest, res *settingssvc.GetPermissionByIDResponse) error {
if validationError := validateGetPermissionByID(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
permission, err := g.manager.ReadPermissionByID(req.GetPermissionId(), g.getRoleIDs(ctx))
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
if permission == nil {
return merrors.NotFound(g.id, "%s", fmt.Errorf("permission %s not found in roles", req.GetPermissionId()))
}
res.Permission = permission
return nil
}
// cleanUpResource makes sure that the account uuid of the authenticated user is injected if needed.
func cleanUpResource(ctx context.Context, resource *settingsmsg.Resource) {
if resource != nil && resource.GetType() == settingsmsg.Resource_TYPE_USER {
resource.Id = getValidatedAccountUUID(ctx, resource.GetId())
}
}
// getValidatedAccountUUID converts `me` into an actual account uuid from the context, if possible.
// the result of this function will always be a valid lower-case UUID or an empty string.
func getValidatedAccountUUID(ctx context.Context, accountUUID string) string {
if accountUUID == "me" {
if ownAccountUUID, ok := metadata.Get(ctx, middleware.AccountID); ok {
accountUUID = ownAccountUUID
}
}
if accountUUID == "me" {
// no matter what happens above, an accountUUID of `me` must not be passed on. Clear it instead.
accountUUID = ""
}
return accountUUID
}
// getRoleIDs extracts the roleIDs of the authenticated user from the context.
func (g Service) getRoleIDs(ctx context.Context) []string {
if ownRoleIDs, ok := roles.ReadRoleIDsFromContext(ctx); ok {
return ownRoleIDs
}
if accountID, ok := metadata.Get(ctx, middleware.AccountID); ok {
assignments, err := g.manager.ListRoleAssignments(accountID)
if err != nil {
g.logger.Info().Err(err).Str("userid", accountID).Msg("failed to get roles for user")
return nil
}
ownRoleIDs := make([]string, 0, len(assignments))
for _, a := range assignments {
ownRoleIDs = append(ownRoleIDs, a.GetRoleId())
}
return ownRoleIDs
}
g.logger.Info().Msg("failed to get accountID from context")
return nil
}
func (g Service) getValueWithIdentifier(value *settingsmsg.Value) (*settingsmsg.ValueWithIdentifier, error) {
bundle, err := g.manager.ReadBundle(value.GetBundleId())
if err != nil {
return nil, err
}
setting, err := g.manager.ReadSetting(value.GetSettingId())
if err != nil {
return nil, err
}
return &settingsmsg.ValueWithIdentifier{
Identifier: &settingsmsg.Identifier{
Extension: bundle.GetExtension(),
Bundle: bundle.GetName(),
Setting: setting.GetName(),
},
Value: value,
}, nil
}
func (g Service) hasStaticPermission(ctx context.Context, permissionID string) bool {
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
if !ok {
// TODO add system role for internal requests.
// - at least the proxy needs to look up account info
// - glauth needs to make bind requests
// tracked as OCIS-454
accountID, ok := metadata.Get(ctx, middleware.AccountID)
if !ok {
return false
}
assignments, err := g.manager.ListRoleAssignments(accountID)
if err != nil {
return false
}
// deduplicate roleids
uniqueRoleIDs := make(map[string]struct{})
for _, a := range assignments {
uniqueRoleIDs[a.GetRoleId()] = struct{}{}
}
roleIDs = make([]string, 0, len(uniqueRoleIDs))
for a := range uniqueRoleIDs {
roleIDs = append(roleIDs, a)
}
}
p, err := g.manager.ReadPermissionByID(permissionID, roleIDs)
return err == nil && p != nil
}
func (g Service) checkStaticPermissionsByBundleID(ctx context.Context, bundleID string) error {
bundle, err := g.manager.ReadBundle(bundleID)
if err != nil {
return merrors.NotFound(g.id, "bundle not found: %s", err)
}
return g.checkStaticPermissionsByBundleType(ctx, bundle.GetType())
}
func (g Service) checkStaticPermissionsByBundleType(ctx context.Context, bundleType settingsmsg.Bundle_Type) error {
if bundleType == settingsmsg.Bundle_TYPE_ROLE {
if !g.hasStaticPermission(ctx, RoleManagementPermissionID) {
return merrors.Forbidden(g.id, "user has no role management permission")
}
return nil
}
if !g.hasStaticPermission(ctx, SettingsManagementPermissionID) {
return merrors.Forbidden(g.id, "user has no settings management permission")
}
return nil
}
func (g Service) isCurrentUser(ctx context.Context, accountID string) bool {
ownAccountID, ok := metadata.Get(ctx, middleware.AccountID)
if !ok {
return false
}
return accountID == ownAccountID
}
func (g Service) canManageRoles(ctx context.Context) bool {
return g.hasStaticPermission(ctx, RoleManagementPermissionID)
}
func (g Service) getUserLocale(ctx context.Context, userID string) (string, error) {
var resp settingssvc.GetValueResponse
err := g.GetValueByUniqueIdentifiers(
ctx,
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: userID,
SettingId: defaults.SettingUUIDProfileLanguage,
},
&resp,
)
if err != nil {
return "", err
}
val := resp.GetValue().GetValue().GetListValue().GetValues()
if len(val) == 0 {
return "", errors.New("no language setting found")
}
return val[0].GetStringValue(), nil
}
func formatPermissionName(setting *settingsmsg.Setting) string {
constraint := strings.TrimPrefix(setting.GetPermissionValue().GetConstraint().String(), "CONSTRAINT_")
return setting.GetName() + "." + strings.ToLower(constraint)
}
func translateBundle(bundle *settingsmsg.Bundle, t *gotext.Locale) *settingsmsg.Bundle {
for i, set := range bundle.GetSettings() {
switch set.GetId() {
default:
continue
case defaults.SettingUUIDProfileEmailSendingInterval:
// translate interval names ('Instant', 'Daily', 'Weekly', 'Never')
value := set.GetSingleChoiceValue()
for i, v := range value.GetOptions() {
value.Options[i].DisplayValue = t.Get(v.GetDisplayValue(), []any{}...)
}
set.Value = &settingsmsg.Setting_SingleChoiceValue{SingleChoiceValue: value}
fallthrough
case defaults.SettingUUIDProfileEventShareCreated,
defaults.SettingUUIDProfileEventShareRemoved,
defaults.SettingUUIDProfileEventShareExpired,
defaults.SettingUUIDProfileEventSpaceShared,
defaults.SettingUUIDProfileEventSpaceUnshared,
defaults.SettingUUIDProfileEventSpaceMembershipExpired,
defaults.SettingUUIDProfileEventSpaceDisabled,
defaults.SettingUUIDProfileEventSpaceDeleted:
// translate event names ('Share Received', 'Share Removed', ...)
set.DisplayName = t.Get(set.GetDisplayName(), []any{}...)
// translate event descriptions ('Notify me when I receive a share', ...)
set.Description = t.Get(set.GetDescription(), []any{}...)
bundle.Settings[i] = set
}
}
return bundle
}
@@ -0,0 +1,461 @@
package svc
import (
"context"
"net/http"
"testing"
"github.com/qsfera/server/pkg/middleware"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
v0 "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/settings/pkg/settings/mocks"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
)
var (
ctxWithUUID = metadata.Set(context.Background(), middleware.AccountID, "61445573-4dbe-4d56-88dc-88ab47aceba7")
ctxWithEmptyUUID = metadata.Set(context.Background(), middleware.AccountID, "")
emptyCtx = context.Background()
scenarios = []struct {
name string
accountUUID string
ctx context.Context
expect string
}{
{
name: "context with UUID; identifier = 'me'",
ctx: ctxWithUUID,
accountUUID: "me",
expect: "61445573-4dbe-4d56-88dc-88ab47aceba7",
},
{
name: "context with empty UUID; identifier = 'me'",
ctx: ctxWithEmptyUUID,
accountUUID: "me",
expect: "",
},
{
name: "context without UUID; identifier = 'me'",
ctx: emptyCtx,
accountUUID: "me",
expect: "",
},
{
name: "context with UUID; identifier not 'me'",
ctx: ctxWithUUID,
accountUUID: "",
expect: "",
},
}
)
func TestGetValidatedAccountUUID(t *testing.T) {
for _, s := range scenarios {
scenario := s
t.Run(scenario.name, func(t *testing.T) {
got := getValidatedAccountUUID(scenario.ctx, scenario.accountUUID)
assert.NotPanics(t, func() {
getValidatedAccountUUID(emptyCtx, scenario.accountUUID)
})
assert.Equal(t, scenario.expect, got)
})
}
}
func TestEditOwnRoleAssignment(t *testing.T) {
manager := &mocks.Manager{}
svc := Service{
manager: manager,
}
a := []*settingsmsg.UserRoleAssignment{}
manager.On("ListRoleAssignments", mock.Anything).Return(a, nil)
manager.On("WriteRoleAssignment", mock.Anything, mock.Anything).Return(nil, nil)
// Creating an initial self assignment is expected to succeed for UserRole when no assignment exists yet
req := v0.AssignRoleToUserRequest{
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: defaults.BundleUUIDRoleUser,
}
res := v0.AssignRoleToUserResponse{}
err := svc.AssignRoleToUser(ctxWithUUID, &req, &res)
assert.Nil(t, err)
// Creating an initial self assignment is expected to succeed for UserLightRole when no assignment exists yet
req = v0.AssignRoleToUserRequest{
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: defaults.BundleUUIDRoleUserLight,
}
res = v0.AssignRoleToUserResponse{}
err = svc.AssignRoleToUser(ctxWithUUID, &req, &res)
assert.Nil(t, err)
// Creating an initial self assignment is expected to fail for non UserRole when no assignment exists yet
req = v0.AssignRoleToUserRequest{
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: defaults.BundleUUIDRoleAdmin,
}
res = v0.AssignRoleToUserResponse{}
err = svc.AssignRoleToUser(ctxWithUUID, &req, &res)
assert.NotNil(t, err)
manager = &mocks.Manager{}
svc = Service{
manager: manager,
}
a = []*settingsmsg.UserRoleAssignment{
{
Id: "00000000-0000-0000-0000-000000000001",
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39",
},
}
editRolePermission := &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
}
manager.On("ListRoleAssignments", mock.Anything).Return(a, nil)
manager.On("ReadPermissionByID", mock.Anything, mock.Anything).Return(editRolePermission, nil)
// Creating an self assignment is expect to fail if there is already an assingment
req = v0.AssignRoleToUserRequest{
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: defaults.BundleUUIDRoleUser,
}
res = v0.AssignRoleToUserResponse{}
err = svc.AssignRoleToUser(ctxWithUUID, &req, &res)
assert.NotNil(t, err)
manager.On("WriteRoleAssignment", mock.Anything, mock.Anything).Return(nil, nil)
// Creating an assignment for somebody else is expected to succeed, give the right permissions
req = v0.AssignRoleToUserRequest{
AccountUuid: "00000000-0000-0000-0000-000000000000",
RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39",
}
res = v0.AssignRoleToUserResponse{}
err = svc.AssignRoleToUser(ctxWithUUID, &req, &res)
assert.Nil(t, err)
}
func TestRemoveOwnRoleAssignment(t *testing.T) {
manager := &mocks.Manager{}
a := []*settingsmsg.UserRoleAssignment{
{
Id: "00000000-0000-0000-0000-000000000001",
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39",
},
}
editRolePermission := &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
}
manager.On("ReadPermissionByID", mock.Anything, mock.Anything).Return(editRolePermission, nil)
manager.On("ListRoleAssignments", mock.Anything).Return(a, nil)
svc := Service{
manager: manager,
}
// Removing a role for oneself is expected to fail
req := v0.RemoveRoleFromUserRequest{
Id: "00000000-0000-0000-0000-000000000001",
}
err := svc.RemoveRoleFromUser(ctxWithUUID, &req, nil)
assert.NotNil(t, err)
manager = &mocks.Manager{}
manager.On("ListRoleAssignments", mock.Anything).Return(nil, nil)
manager.On("RemoveRoleAssignment", mock.Anything).Return(nil)
manager.On("ReadPermissionByID", mock.Anything, mock.Anything).Return(editRolePermission, nil)
svc = Service{
manager: manager,
}
// Removing a role for someone else is expected to fail
req = v0.RemoveRoleFromUserRequest{
Id: "00000000-0000-0000-0000-000000000002",
}
err = svc.RemoveRoleFromUser(ctxWithUUID, &req, nil)
assert.Nil(t, err)
}
func TestListPermissionsOfCurrentUser(t *testing.T) {
manager := &mocks.Manager{}
a := []*settingsmsg.UserRoleAssignment{
{
Id: "00000000-0000-0000-0000-000000000001",
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39",
},
}
manager.On("ListRoleAssignments", mock.Anything).Return(a, nil)
b := &settingsmsg.Bundle{
Id: "aceb15b8-7486-479f-ae32-c91118e07a39",
Settings: []*settingsmsg.Setting{
{
Name: "some-permission",
},
{
Name: "other-permission",
},
{
Name: "duplicate-permission",
},
{
Name: "duplicate-permission",
},
},
}
manager.On("ReadBundle", mock.Anything).Return(b, nil)
svc := Service{
manager: manager,
}
// Listing permissions for yourself
req := v0.ListPermissionsRequest{
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
}
res := v0.ListPermissionsResponse{}
err := svc.ListPermissions(ctxWithUUID, &req, &res)
assert.NoError(t, err)
assert.Len(t, res.Permissions, 3)
}
func TestListPermissionsOfOtherUser(t *testing.T) {
manager := &mocks.Manager{}
svc := Service{
manager: manager,
}
// Listing permissions for another user produces a not found error
req := v0.ListPermissionsRequest{
AccountUuid: "66666666-4444-4444-8888-88ab47aceba7",
}
res := v0.ListPermissionsResponse{}
err := svc.ListPermissions(ctxWithUUID, &req, &res)
assert.Error(t, err)
// assert the requested account uuid was not found
merr, ok := merrors.As(err)
assert.True(t, ok)
assert.Equal(t, int32(http.StatusNotFound), merr.Code)
assert.Contains(t, err.Error(), req.AccountUuid)
}
func TestListRoleAssignmentsFilteredValidation(t *testing.T) {
manager := &mocks.Manager{}
svc := Service{
manager: manager,
}
tests := map[string]struct {
req *v0.ListRoleAssignmentsFilteredRequest
statusCode int32
}{
"no filters": {
req: &v0.ListRoleAssignmentsFilteredRequest{},
statusCode: http.StatusBadRequest,
},
"multiple filters": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT,
Term: &settingsmsg.UserRoleAssignmentFilter_AccountUuid{
AccountUuid: "uid",
},
},
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE,
Term: &settingsmsg.UserRoleAssignmentFilter_RoleId{
RoleId: "rid",
},
},
},
},
statusCode: http.StatusBadRequest,
},
"bad filtertype": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_UNKNOWN,
},
},
},
statusCode: http.StatusBadRequest,
},
"account filter without term": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT,
},
},
},
statusCode: http.StatusBadRequest,
},
"account filter with invalid term": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT,
Term: &settingsmsg.UserRoleAssignmentFilter_AccountUuid{
AccountUuid: "invalid-&*&^%$#",
},
},
},
},
statusCode: http.StatusBadRequest,
},
"role filter without term": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE,
},
},
},
statusCode: http.StatusBadRequest,
},
"role filter with invalid uuid": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE,
Term: &settingsmsg.UserRoleAssignmentFilter_RoleId{
RoleId: "this is no uuid",
},
},
},
},
statusCode: http.StatusBadRequest,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
res := v0.ListRoleAssignmentsResponse{}
err := svc.ListRoleAssignmentsFiltered(ctxWithUUID, test.req, &res)
merr, ok := merrors.As(err)
assert.True(t, ok)
assert.Equal(t, int32(test.statusCode), merr.Code)
})
}
}
func TestListRoleAssignmentsFilteredByAccount(t *testing.T) {
accountUUID := "61445573-4dbe-4d56-88dc-88ab47aceba7"
tests := map[string]struct {
result []*settingsmsg.UserRoleAssignment
err error
status int32
}{
"handles manager error": {
result: nil,
err: assert.AnError,
status: http.StatusNotFound,
},
"succeeds with results": {
result: []*settingsmsg.UserRoleAssignment{
{
Id: "00000000-0000-0000-0000-000000000001",
AccountUuid: accountUUID,
RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39",
},
},
err: nil,
status: http.StatusOK,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
manager := &mocks.Manager{}
svc := Service{
manager: manager,
}
manager.On("ListRoleAssignments", mock.Anything).Return(test.result, test.err)
req := &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT,
Term: &settingsmsg.UserRoleAssignmentFilter_AccountUuid{
AccountUuid: accountUUID,
},
},
},
}
res := v0.ListRoleAssignmentsResponse{}
err := svc.ListRoleAssignmentsFiltered(ctxWithUUID, req, &res)
switch test.err {
case nil:
assert.Nil(t, err)
default:
merr, ok := merrors.As(err)
assert.True(t, ok)
assert.Equal(t, int32(test.status), merr.Code)
}
})
}
}
func TestListRoleAssignmentsFilteredByRole(t *testing.T) {
roleID := "61445573-4dbe-4d56-88dc-88ab47aceba7"
tests := map[string]struct {
result []*settingsmsg.UserRoleAssignment
err error
status int32
}{
"handles manager error": {
result: nil,
err: assert.AnError,
status: http.StatusNotFound,
},
"succeeds with results": {
result: []*settingsmsg.UserRoleAssignment{
{
Id: "00000000-0000-0000-0000-000000000001",
AccountUuid: "aceb15b8-7486-479f-ae32-c91118e07a39",
RoleId: roleID,
},
},
err: nil,
status: http.StatusOK,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
manager := &mocks.Manager{}
svc := Service{
manager: manager,
}
manager.On("ListRoleAssignmentsByRole", mock.Anything).Return(test.result, test.err)
req := &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE,
Term: &settingsmsg.UserRoleAssignmentFilter_RoleId{
RoleId: roleID,
},
},
},
}
res := v0.ListRoleAssignmentsResponse{}
err := svc.ListRoleAssignmentsFiltered(ctxWithUUID, req, &res)
switch test.err {
case nil:
assert.Nil(t, err)
default:
merr, ok := merrors.As(err)
assert.True(t, ok)
assert.Equal(t, int32(test.status), merr.Code)
}
})
}
}
@@ -0,0 +1,197 @@
package svc
import (
"context"
"strings"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/qsfera/server/services/settings/pkg/settings"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
)
var _defaultLanguage = "en"
// NewDefaultLanguageService returns a default language decorator for ServiceHandler.
func NewDefaultLanguageService(cfg *config.Config, serviceHandler settings.ServiceHandler) settings.ServiceHandler {
defaultLanguage := cfg.DefaultLanguage
if defaultLanguage == "" {
defaultLanguage = _defaultLanguage
}
return &defaultLanguageDecorator{defaultLanguage: defaultLanguage, ServiceHandler: serviceHandler}
}
type defaultLanguageDecorator struct {
defaultLanguage string
settings.ServiceHandler
}
// GetValueByUniqueIdentifiers implements the ValueService interface
func (s *defaultLanguageDecorator) GetValueByUniqueIdentifiers(ctx context.Context, req *settingssvc.GetValueByUniqueIdentifiersRequest, res *settingssvc.GetValueResponse) error {
err := s.ServiceHandler.GetValueByUniqueIdentifiers(ctx, req, res)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "not found") && res.GetValue() == nil {
defaultValueList := getDefaultValueList()
// Ensure the default values for profile settings
if _, ok := defaultValueList[req.GetSettingId()]; ok {
res.Value = s.withDefaultProfileValue(ctx, req.AccountUuid, req.GetSettingId())
return nil
}
}
return err
}
return nil
}
// ListValues implements the ValueServiceHandler interface
func (s *defaultLanguageDecorator) ListValues(ctx context.Context, req *settingssvc.ListValuesRequest, res *settingssvc.ListValuesResponse) error {
err := s.ServiceHandler.ListValues(ctx, req, res)
if err != nil {
return err
}
defaultValueList := getDefaultValueList()
for _, v := range res.Values {
delete(defaultValueList, v.GetValue().GetSettingId())
}
// Ensure the default values for profile settings
defaultValueList = s.withDefaultProfileValueList(ctx, req.AccountUuid, defaultValueList)
if len(defaultValueList) > 0 {
for _, v := range defaultValueList {
if v != nil {
res.Values = append(res.Values, v)
}
}
}
return nil
}
func (s *defaultLanguageDecorator) withDefaultLanguageSetting(accountUUID string) *settingsmsg.ValueWithIdentifier {
return &settingsmsg.ValueWithIdentifier{
Identifier: &settingsmsg.Identifier{
Extension: "qsfera-accounts",
Bundle: "profile",
Setting: "language",
},
Value: &settingsmsg.Value{
BundleId: defaults.BundleUUIDProfile,
SettingId: defaults.SettingUUIDProfileLanguage,
AccountUuid: accountUUID,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Value_ListValue{
ListValue: &settingsmsg.ListValue{Values: []*settingsmsg.ListOptionValue{
{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: s.defaultLanguage,
},
},
}},
},
},
}
}
func (s *defaultLanguageDecorator) withDefaultProfileValue(ctx context.Context, accountUUID string, settingId string) *settingsmsg.ValueWithIdentifier {
if settingId == defaults.SettingUUIDProfileLanguage {
return s.withDefaultLanguageSetting(accountUUID)
}
res := s.withDefaultProfileValueList(ctx, accountUUID, map[string]*settingsmsg.ValueWithIdentifier{settingId: nil})
if v, ok := res[settingId]; ok {
return v
}
return nil
}
func (s *defaultLanguageDecorator) withDefaultProfileValueList(ctx context.Context,
accountUUID string, requested map[string]*settingsmsg.ValueWithIdentifier) map[string]*settingsmsg.ValueWithIdentifier {
// we use the default profile bundle instead of s.GetBundle(ctx, req, resp)
bundle := defaults.GenerateDefaultProfileBundle()
for _, setting := range bundle.GetSettings() {
if v, ok := requested[setting.GetId()]; !ok || v != nil {
continue
}
if setting.GetId() == defaults.SettingUUIDProfileLanguage {
requested[setting.GetId()] = s.withDefaultLanguageSetting(accountUUID)
continue
}
newVal := &settingsmsg.ValueWithIdentifier{
Identifier: &settingsmsg.Identifier{
Extension: bundle.GetExtension(),
Bundle: bundle.GetName(),
Setting: setting.GetName(),
},
Value: &settingsmsg.Value{
BundleId: bundle.GetId(),
SettingId: setting.GetId(),
AccountUuid: accountUUID,
Resource: setting.GetResource(),
},
}
switch val := setting.GetValue().(type) {
case *settingsmsg.Setting_MultiChoiceCollectionValue:
newVal.Value.Value = multiChoiceCollectionToValue(val.MultiChoiceCollectionValue)
requested[setting.GetId()] = newVal
case *settingsmsg.Setting_SingleChoiceValue:
sv := &settingsmsg.Value_StringValue{}
for _, option := range val.SingleChoiceValue.Options {
if option.GetDefault() {
sv.StringValue = option.Value.GetStringValue()
break
}
}
newVal.Value.Value = sv
requested[setting.GetId()] = newVal
}
}
return requested
}
func multiChoiceCollectionToValue(collection *settingsmsg.MultiChoiceCollection) *settingsmsg.Value_CollectionValue {
values := make([]*settingsmsg.CollectionOption, 0, len(collection.GetOptions()))
for _, option := range collection.GetOptions() {
switch o := option.GetValue().GetOption().(type) {
case *settingsmsg.MultiChoiceCollectionOptionValue_BoolValue:
if o != nil {
values = append(values, &settingsmsg.CollectionOption{
Key: option.GetKey(),
Option: &settingsmsg.CollectionOption_BoolValue{
BoolValue: o.BoolValue.GetDefault(),
},
})
}
}
}
return &settingsmsg.Value_CollectionValue{
CollectionValue: &settingsmsg.CollectionValue{
Values: values,
},
}
}
func getDefaultValueList() map[string]*settingsmsg.ValueWithIdentifier {
return map[string]*settingsmsg.ValueWithIdentifier{
// specific profile settings should be handled individually
defaults.SettingUUIDProfileLanguage: nil,
// all other profile settings that populated from the bundle based on type
defaults.SettingUUIDProfileEventShareCreated: nil,
defaults.SettingUUIDProfileEventShareRemoved: nil,
defaults.SettingUUIDProfileEventShareExpired: nil,
defaults.SettingUUIDProfileEventSpaceShared: nil,
defaults.SettingUUIDProfileEventSpaceUnshared: nil,
defaults.SettingUUIDProfileEventSpaceMembershipExpired: nil,
defaults.SettingUUIDProfileEventSpaceDisabled: nil,
defaults.SettingUUIDProfileEventSpaceDeleted: nil,
defaults.SettingUUIDProfileEventPostprocessingStepFinished: nil,
defaults.SettingUUIDProfileEmailSendingInterval: nil,
}
}
@@ -0,0 +1,13 @@
package svc
const (
// RoleManagementPermissionID is the hardcoded setting UUID for the role management permission
RoleManagementPermissionID string = "a53e601e-571f-4f86-8fec-d4576ef49c62"
// SettingsManagementPermissionID is the hardcoded setting UUID for the settings management permission
SettingsManagementPermissionID string = "3d58f441-4a05-42f8-9411-ef5874528ae1"
// AccountManagementPermissionID is the hardcoded setting UUID for the account management permission
AccountManagementPermissionID string = "8e587774-d929-4215-910b-a317b1e80f73"
)
@@ -0,0 +1,225 @@
package svc
import (
"errors"
"fmt"
"regexp"
validation "github.com/invopop/validation"
"github.com/invopop/validation/is"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
)
var (
regexForAccountUUID = regexp.MustCompile(`^[A-Za-z0-9\-_.+@:]+$`)
requireAccountID = []validation.Rule{
// use rule for validation error message consistency (".. must not be blank" on empty strings)
validation.Required,
validation.Match(regexForAccountUUID),
}
regexForKeys = regexp.MustCompile(`^[A-Za-z0-9\-_]*$`)
requireAlphanumeric = []validation.Rule{
validation.Required,
validation.Match(regexForKeys),
}
)
func validateSaveBundle(req *settingssvc.SaveBundleRequest) error {
if err := validation.ValidateStruct(
req.Bundle,
validation.Field(&req.Bundle.Id, validation.When(req.Bundle.Id != "", is.UUID)),
validation.Field(&req.Bundle.Name, requireAlphanumeric...),
validation.Field(&req.Bundle.Type, validation.NotIn(settingsmsg.Bundle_TYPE_UNKNOWN)),
validation.Field(&req.Bundle.Extension, requireAlphanumeric...),
validation.Field(&req.Bundle.DisplayName, validation.Required),
validation.Field(&req.Bundle.Settings, validation.Required),
); err != nil {
return err
}
if err := validateResource(req.Bundle.Resource); err != nil {
return err
}
for i := range req.Bundle.Settings {
if err := validateSetting(req.Bundle.Settings[i]); err != nil {
return err
}
}
return nil
}
func validateGetBundle(req *settingssvc.GetBundleRequest) error {
return validation.Validate(&req.BundleId, is.UUID)
}
func validateListBundles(req *settingssvc.ListBundlesRequest) error {
return nil
}
func validateAddSettingToBundle(req *settingssvc.AddSettingToBundleRequest) error {
if err := validation.ValidateStruct(req, validation.Field(&req.BundleId, is.UUID)); err != nil {
return err
}
return validateSetting(req.Setting)
}
func validateRemoveSettingFromBundle(req *settingssvc.RemoveSettingFromBundleRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.BundleId, is.UUID),
validation.Field(&req.SettingId, is.UUID),
)
}
func validateSaveValue(req *settingssvc.SaveValueRequest) error {
if err := validation.ValidateStruct(
req.Value,
validation.Field(&req.Value.Id, validation.When(req.Value.Id != "", is.UUID)),
validation.Field(&req.Value.BundleId, is.UUID),
validation.Field(&req.Value.SettingId, is.UUID),
validation.Field(&req.Value.AccountUuid, requireAccountID...),
); err != nil {
return err
}
if err := validateResource(req.Value.Resource); err != nil {
return err
}
if v := req.GetValue().GetCollectionValue().GetValues(); v != nil {
for i := range v {
if err := validation.Validate(v[i].GetKey(), validation.Required); err != nil {
return fmt.Errorf("collectionValue.values[%d].key %w", i, err)
}
}
}
// TODO: validate values against the respective setting. need to check if constraints of the setting are fulfilled.
return nil
}
func validateGetValue(req *settingssvc.GetValueRequest) error {
return validation.Validate(req.Id, is.UUID)
}
func validateGetValueByUniqueIdentifiers(req *settingssvc.GetValueByUniqueIdentifiersRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.SettingId, is.UUID),
validation.Field(&req.AccountUuid, requireAccountID...),
)
}
func validateListValues(req *settingssvc.ListValuesRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.BundleId, validation.When(req.BundleId != "", is.UUID)),
validation.Field(&req.AccountUuid, validation.When(req.AccountUuid != "", validation.Match(regexForAccountUUID))),
)
}
func validateListRoles(req *settingssvc.ListBundlesRequest) error {
return nil
}
func validateListRoleAssignments(req *settingssvc.ListRoleAssignmentsRequest) error {
return validation.Validate(req.AccountUuid, requireAccountID...)
}
func validateListRoleAssignmentsFiltered(req *settingssvc.ListRoleAssignmentsFilteredRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.Filters,
validation.Required,
validation.Length(1, 1),
validation.Each(validation.By(validateUserRoleAssignmentFilter)),
),
)
}
func validateUserRoleAssignmentFilter(values any) error {
filter, ok := values.(*settingsmsg.UserRoleAssignmentFilter)
if !ok {
return errors.New("expected UserRoleAssignmentFilter")
}
return validation.ValidateStruct(
filter,
validation.Field(&filter.Type,
validation.Required,
validation.In(settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT, settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE),
),
validation.Field(&filter.Term,
validation.When(
filter.Type == settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT,
validation.By(validateFilterAccountUUID),
),
validation.When(
filter.Type == settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE,
validation.By(validateFilterRoleID),
),
),
)
}
func validateFilterRoleID(value any) error {
roleTerm, ok := value.(*settingsmsg.UserRoleAssignmentFilter_RoleId)
if !ok {
return errors.New("expected UserRoleAssignmentFilter_RoleId")
}
return validation.Validate(&roleTerm.RoleId, is.UUID)
}
func validateFilterAccountUUID(value any) error {
accountTerm, ok := value.(*settingsmsg.UserRoleAssignmentFilter_AccountUuid)
if !ok {
return errors.New("expected UserRoleAssignmentFilter_AccountUuid")
}
return validation.Validate(&accountTerm.AccountUuid, requireAccountID...)
}
func validateAssignRoleToUser(req *settingssvc.AssignRoleToUserRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.AccountUuid, requireAccountID...),
validation.Field(&req.RoleId, is.UUID),
)
}
func validateRemoveRoleFromUser(req *settingssvc.RemoveRoleFromUserRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.Id, is.UUID),
)
}
func validateListPermissionsByResource(req *settingssvc.ListPermissionsByResourceRequest) error {
return validateResource(req.Resource)
}
func validateGetPermissionByID(req *settingssvc.GetPermissionByIDRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.PermissionId, requireAlphanumeric...),
)
}
// validateResource is an internal helper for validating the content of a resource.
func validateResource(resource *settingsmsg.Resource) error {
if err := validation.Validate(&resource, validation.Required); err != nil {
return err
}
return validation.Validate(&resource, validation.NotIn(settingsmsg.Resource_TYPE_UNKNOWN))
}
// validateSetting is an internal helper for validating the content of a setting.
func validateSetting(setting *settingsmsg.Setting) error {
// TODO: make sanity checks, like for int settings, min <= default <= max.
if err := validation.ValidateStruct(
setting,
validation.Field(&setting.Id, validation.When(setting.Id != "", is.UUID)),
validation.Field(&setting.Name, requireAlphanumeric...),
); err != nil {
return err
}
return validateResource(setting.Resource)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,72 @@
package settings
import (
"errors"
cs3permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/settings/pkg/config"
)
var (
// Registry uses the strategy pattern as a registry
Registry = map[string]RegisterFunc{}
// ErrNotFound is the error to use when a resource was not found.
ErrNotFound = errors.New("not found")
)
// RegisterFunc stores store constructors
type RegisterFunc func(*config.Config) Manager
// ServiceHandler combines handlers interfaces
type ServiceHandler interface {
settingssvc.BundleServiceHandler
settingssvc.ValueServiceHandler
settingssvc.RoleServiceHandler
settingssvc.PermissionServiceHandler
cs3permissions.PermissionsAPIServer
}
// Manager combines service interfaces for abstraction of storage implementations
type Manager interface {
BundleManager
ValueManager
RoleAssignmentManager
PermissionManager
}
// BundleManager is a bundle service interface for abstraction of storage implementations
type BundleManager interface {
ListBundles(bundleType settingsmsg.Bundle_Type, bundleIDs []string) ([]*settingsmsg.Bundle, error)
ReadBundle(bundleID string) (*settingsmsg.Bundle, error)
WriteBundle(bundle *settingsmsg.Bundle) (*settingsmsg.Bundle, error)
ReadSetting(settingID string) (*settingsmsg.Setting, error)
AddSettingToBundle(bundleID string, setting *settingsmsg.Setting) (*settingsmsg.Setting, error)
RemoveSettingFromBundle(bundleID, settingID string) error
}
// ValueManager is a value service interface for abstraction of storage implementations
type ValueManager interface {
ListValues(bundleID, accountUUID string) ([]*settingsmsg.Value, error)
ReadValue(valueID string) (*settingsmsg.Value, error)
ReadValueByUniqueIdentifiers(accountUUID, settingID string) (*settingsmsg.Value, error)
WriteValue(value *settingsmsg.Value) (*settingsmsg.Value, error)
}
// RoleAssignmentManager is a role assignment service interface for abstraction of storage implementations
type RoleAssignmentManager interface {
ListRoleAssignments(accountUUID string) ([]*settingsmsg.UserRoleAssignment, error)
ListRoleAssignmentsByRole(roleID string) ([]*settingsmsg.UserRoleAssignment, error)
WriteRoleAssignment(accountUUID, roleID string) (*settingsmsg.UserRoleAssignment, error)
RemoveRoleAssignment(assignmentID string) error
}
// PermissionManager is a permissions service interface for abstraction of storage implementations
type PermissionManager interface {
ListPermissionsByResource(resource *settingsmsg.Resource, roleIDs []string) ([]*settingsmsg.Permission, error)
ReadPermissionByID(permissionID string, roleIDs []string) (*settingsmsg.Permission, error)
ReadPermissionByName(name string, roleIDs []string) (*settingsmsg.Permission, error)
}
@@ -0,0 +1,695 @@
package defaults
import (
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/qsfera/server/services/settings/pkg/config"
)
const (
// BundleUUIDRoleAdmin represents the admin role
BundleUUIDRoleAdmin = "71881883-1768-46bd-a24d-a356a2afdf7f"
// BundleUUIDRoleSpaceAdmin represents the space admin role
BundleUUIDRoleSpaceAdmin = "2aadd357-682c-406b-8874-293091995fdd"
// BundleUUIDRoleUser represents the user role.
BundleUUIDRoleUser = "d7beeea8-8ff4-406b-8fb6-ab2dd81e6b11"
// BundleUUIDRoleUserLight represents the user light role.
BundleUUIDRoleUserLight = "38071a68-456a-4553-846a-fa67bf5596cc"
// BundleUUIDRoleGuest represents the guest role.
BundleUUIDRoleGuest = "38071a68-456a-4553-846a-fa67bf5596cc"
// BundleUUIDProfile represents the user profile.
BundleUUIDProfile = "2a506de7-99bd-4f0d-994e-c38e72c28fd9"
// BundleUUIDServiceAccount represents the service account role.
BundleUUIDServiceAccount = "bcceed81-c610-49cc-ab77-39a024e8da12"
// SettingUUIDProfileLanguage is the hardcoded setting UUID for the user profile language
SettingUUIDProfileLanguage = "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f"
// SettingUUIDProfileDisableNotifications is the hardcoded setting UUID for the disable notifications setting
SettingUUIDProfileDisableNotifications = "33ffb5d6-cd07-4dc0-afb0-84f7559ae438"
// SettingUUIDProfileAutoAcceptShares is the hardcoded setting UUID for the disable notifications setting
SettingUUIDProfileAutoAcceptShares = "ec3ed4a3-3946-4efc-8f9f-76d38b12d3a9"
// SettingUUIDProfileEmailSendingInterval is the hardcoded setting UUID for the email sending interval setting
SettingUUIDProfileEmailSendingInterval = "08dec2fe-3f97-42a9-9d1b-500855e92f25"
// SettingUUIDProfileEventShareCreated it the hardcoded setting UUID for the send in app setting
SettingUUIDProfileEventShareCreated = "872d8ef6-6f2a-42ab-af7d-f53cc81d7046"
// SettingUUIDProfileEventShareRemoved is the hardcoded setting UUID for the send in app setting
SettingUUIDProfileEventShareRemoved = "d7484394-8321-4c84-9677-741ba71e1f80"
// SettingUUIDProfileEventShareExpired is the hardcoded setting UUID for the send in app setting
SettingUUIDProfileEventShareExpired = "e1aa0b7c-1b0f-4072-9325-c643c89fee4e"
// SettingUUIDProfileEventSpaceShared is the hardcoded setting UUID for the send in app setting
SettingUUIDProfileEventSpaceShared = "694d5ee1-a41c-448c-8d14-396b95d2a918"
// SettingUUIDProfileEventSpaceUnshared is the hardcoded setting UUID for the send in app setting
SettingUUIDProfileEventSpaceUnshared = "26c20e0e-98df-4483-8a77-759b3a766af0"
// SettingUUIDProfileEventSpaceMembershipExpired is the hardcoded setting UUID for the send in app setting
SettingUUIDProfileEventSpaceMembershipExpired = "7275921e-b737-4074-ba91-3c2983be3edd"
// SettingUUIDProfileEventSpaceDisabled is the hardcoded setting UUID for the send in app setting
SettingUUIDProfileEventSpaceDisabled = "eb5c716e-03be-42c6-9ed1-1105d24e109f"
// SettingUUIDProfileEventSpaceDeleted is the hardcoded setting UUID for the send in app setting
SettingUUIDProfileEventSpaceDeleted = "094ceca9-5a00-40ba-bb1a-bbc7bccd39ee"
// SettingUUIDProfileEventPostprocessingStepFinished is the hardcoded setting UUID for the send in mail setting
SettingUUIDProfileEventPostprocessingStepFinished = "fe0a3011-d886-49c8-b797-33d02fa426ef"
)
// GenerateBundlesDefaultRoles bootstraps the default roles.
func GenerateBundlesDefaultRoles() []*settingsmsg.Bundle {
return []*settingsmsg.Bundle{
generateBundleAdminRole(),
generateBundleUserRole(),
generateBundleUserLightRole(),
generateBundleProfileRequest(),
generateBundleSpaceAdminRole(),
}
}
// GenerateDefaultProfileBundle return the default profile bundle.
func GenerateDefaultProfileBundle() *settingsmsg.Bundle {
return generateBundleProfileRequest()
}
// ServiceAccountBundle returns the service account bundle
func ServiceAccountBundle() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDServiceAccount,
Name: "service-account",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "qsfera-roles",
DisplayName: "Service Account",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
AccountManagementPermission(All),
ChangeLogoPermission(All),
CreatePublicLinkPermission(All),
CreateSharePermission(All),
CreateSpacesPermission(All),
DeletePersonalSpacesPermission(All),
DeleteProjectSpacesPermission(All),
DeleteReadOnlyPublicLinkPasswordPermission(All),
GroupManagementPermission(All),
LanguageManagementPermission(All),
ListFavoritesPermission(All),
ListSpacesPermission(All),
ManageSpacePropertiesPermission(All),
RoleManagementPermission(All),
SetPersonalSpaceQuotaPermission(All),
SetProjectSpaceQuotaPermission(All),
SettingsManagementPermission(All),
SpaceAbilityPermission(All),
WriteFavoritesPermission(All),
// TODO: add more permissions? remove some?
},
}
}
func generateBundleAdminRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleAdmin,
Name: "admin",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "qsfera-roles",
DisplayName: "Admin",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
AccountManagementPermission(All),
AutoAcceptSharesPermission(Own),
ChangeLogoPermission(All),
CreatePublicLinkPermission(All),
CreateSharePermission(All),
CreateSpacesPermission(All),
DeletePersonalSpacesPermission(All),
DeleteProjectSpacesPermission(All),
DeleteReadOnlyPublicLinkPasswordPermission(All),
DisableEmailNotificationsPermission(Own),
ProfileEmailSendingIntervalPermission(Own),
ProfileEventShareCreatedPermission(Own),
ProfileEventShareRemovedPermission(Own),
ProfileEventShareExpiredPermission(Own),
ProfileEventSpaceSharedPermission(Own),
ProfileEventSpaceUnsharedPermission(Own),
ProfileEventSpaceMembershipExpiredPermission(Own),
ProfileEventSpaceDisabledPermission(Own),
ProfileEventSpaceDeletedPermission(Own),
ProfileEventPostprocessingStepFinishedPermission(Own),
GroupManagementPermission(All),
LanguageManagementPermission(All),
ListFavoritesPermission(Own),
ListSpacesPermission(All),
ManageSpacePropertiesPermission(All),
RoleManagementPermission(All),
SetPersonalSpaceQuotaPermission(All),
SetProjectSpaceQuotaPermission(All),
SettingsManagementPermission(All),
SpaceAbilityPermission(All),
WebOfficeManagementPermssion(All),
WriteFavoritesPermission(Own),
},
}
}
func generateBundleSpaceAdminRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleSpaceAdmin,
Name: "spaceadmin",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "qsfera-roles",
DisplayName: "Space Admin",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
AutoAcceptSharesPermission(Own),
CreatePublicLinkPermission(All),
CreateSharePermission(All),
CreateSpacesPermission(All),
DeleteProjectSpacesPermission(All),
DeleteReadOnlyPublicLinkPasswordPermission(All),
DisableEmailNotificationsPermission(Own),
ProfileEmailSendingIntervalPermission(Own),
ProfileEventShareCreatedPermission(Own),
ProfileEventShareRemovedPermission(Own),
ProfileEventShareExpiredPermission(Own),
ProfileEventSpaceSharedPermission(Own),
ProfileEventSpaceUnsharedPermission(Own),
ProfileEventSpaceMembershipExpiredPermission(Own),
ProfileEventSpaceDisabledPermission(Own),
ProfileEventSpaceDeletedPermission(Own),
ProfileEventPostprocessingStepFinishedPermission(Own),
LanguageManagementPermission(Own),
ListFavoritesPermission(Own),
ListSpacesPermission(All),
ManageSpacePropertiesPermission(All),
SelfManagementPermission(Own),
SetProjectSpaceQuotaPermission(All),
SpaceAbilityPermission(All),
WriteFavoritesPermission(Own),
},
}
}
func generateBundleUserRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleUser,
Name: "user",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "qsfera-roles",
DisplayName: "User",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
AutoAcceptSharesPermission(Own),
CreatePublicLinkPermission(All),
CreateSharePermission(All),
CreateSpacesPermission(Own),
DisableEmailNotificationsPermission(Own),
ProfileEmailSendingIntervalPermission(Own),
ProfileEventShareCreatedPermission(Own),
ProfileEventShareRemovedPermission(Own),
ProfileEventShareExpiredPermission(Own),
ProfileEventSpaceSharedPermission(Own),
ProfileEventSpaceUnsharedPermission(Own),
ProfileEventSpaceMembershipExpiredPermission(Own),
ProfileEventSpaceDisabledPermission(Own),
ProfileEventSpaceDeletedPermission(Own),
ProfileEventPostprocessingStepFinishedPermission(Own),
LanguageManagementPermission(Own),
ListFavoritesPermission(Own),
SelfManagementPermission(Own),
WriteFavoritesPermission(Own),
},
}
}
func generateBundleUserLightRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleUserLight,
Name: "user-light",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "qsfera-roles",
DisplayName: "User Light",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
AutoAcceptSharesPermission(Own),
DisableEmailNotificationsPermission(Own),
ProfileEmailSendingIntervalPermission(Own),
LanguageManagementPermission(Own),
},
}
}
func generateBundleProfileRequest() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDProfile,
Name: "profile",
Extension: "qsfera-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,
},
{
Id: SettingUUIDProfileDisableNotifications,
Name: "disable-email-notifications",
DisplayName: "Disable Email Notifications",
Description: "Disable email notifications",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_BoolValue{BoolValue: &settingsmsg.Bool{Default: false, Label: "disable notifications"}},
},
{
Id: SettingUUIDProfileAutoAcceptShares,
Name: "auto-accept-shares",
DisplayName: "Auto accept shares",
Description: "Automatically accept shares",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_BoolValue{BoolValue: &settingsmsg.Bool{Default: true, Label: "auto accept shares"}},
},
{
Id: SettingUUIDProfileEmailSendingInterval,
Name: "email-sending-interval-options",
DisplayName: TemplateEmailSendingInterval,
Description: TemplateEmailSendingIntervalDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &sendEmailOptions,
},
{
Id: SettingUUIDProfileEventShareCreated,
Name: "event-share-created-options",
DisplayName: TemplateShareCreated,
Description: TemplateShareCreatedDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_MultiChoiceCollectionValue{
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailTrue,
},
},
},
},
{
Id: SettingUUIDProfileEventShareRemoved,
Name: "event-share-removed-options",
DisplayName: TemplateShareRemoved,
Description: TemplateShareRemovedDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_MultiChoiceCollectionValue{
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailFalseDisabled,
},
},
},
},
{
Id: SettingUUIDProfileEventShareExpired,
Name: "event-share-expired-options",
DisplayName: TemplateShareExpired,
Description: TemplateShareExpiredDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_MultiChoiceCollectionValue{
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailTrue,
},
},
},
},
{
Id: SettingUUIDProfileEventSpaceShared,
Name: "event-space-shared-options",
DisplayName: TemplateSpaceShared,
Description: TemplateSpaceSharedDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_MultiChoiceCollectionValue{
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailTrue,
},
},
},
},
{
Id: SettingUUIDProfileEventSpaceUnshared,
Name: "event-space-unshared-options",
DisplayName: TemplateSpaceUnshared,
Description: TemplateSpaceUnsharedDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_MultiChoiceCollectionValue{
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailTrue,
},
},
},
},
{
Id: SettingUUIDProfileEventSpaceMembershipExpired,
Name: "event-space-membership-expired-options",
DisplayName: TemplateSpaceMembershipExpired,
Description: TemplateSpaceMembershipExpiredDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_MultiChoiceCollectionValue{
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailTrue,
},
},
},
},
{
Id: SettingUUIDProfileEventSpaceDisabled,
Name: "event-space-disabled-options",
DisplayName: TemplateSpaceDisabled,
Description: TemplateSpaceDisabledDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_MultiChoiceCollectionValue{
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailFalseDisabled,
},
},
},
},
{
Id: SettingUUIDProfileEventSpaceDeleted,
Name: "event-space-deleted-options",
DisplayName: TemplateSpaceDeleted,
Description: TemplateSpaceDeletedDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_MultiChoiceCollectionValue{
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailFalseDisabled,
},
},
},
},
{
Id: SettingUUIDProfileEventPostprocessingStepFinished,
Name: "event-postprocessing-step-finished-options",
DisplayName: TemplateFileRejected,
Description: TemplateFileRejectedDescription,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Setting_MultiChoiceCollectionValue{
MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{
Options: []*settingsmsg.MultiChoiceCollectionOption{
&optionInAppTrue,
&optionMailFalseDisabled,
},
},
},
},
},
}
}
var sendEmailOptions = settingsmsg.Setting_SingleChoiceValue{
SingleChoiceValue: &settingsmsg.SingleChoiceList{
Options: []*settingsmsg.ListOption{
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "instant",
},
},
DisplayValue: TemplateIntervalInstant,
Default: true,
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "daily",
},
},
DisplayValue: TemplateIntervalDaily,
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "weekly",
},
},
DisplayValue: TemplateIntervalWeekly,
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "never",
},
},
DisplayValue: TemplateIntervalNever,
},
},
},
}
var optionInAppTrue = settingsmsg.MultiChoiceCollectionOption{
Key: "in-app",
DisplayValue: "In-App",
Value: &settingsmsg.MultiChoiceCollectionOptionValue{
Option: &settingsmsg.MultiChoiceCollectionOptionValue_BoolValue{
BoolValue: &settingsmsg.Bool{
Default: true,
},
},
},
}
var optionMailTrue = settingsmsg.MultiChoiceCollectionOption{
Key: "mail",
DisplayValue: "Email",
Value: &settingsmsg.MultiChoiceCollectionOptionValue{
Option: &settingsmsg.MultiChoiceCollectionOptionValue_BoolValue{
BoolValue: &settingsmsg.Bool{
Default: true,
},
},
},
}
var optionMailFalseDisabled = settingsmsg.MultiChoiceCollectionOption{
Key: "mail",
Attribute: "disabled",
DisplayValue: "Email",
Value: &settingsmsg.MultiChoiceCollectionOptionValue{
Option: &settingsmsg.MultiChoiceCollectionOptionValue_BoolValue{
BoolValue: &settingsmsg.Bool{
Default: false,
},
},
},
}
// TODO: languageSetting needed?
var languageSetting = settingsmsg.Setting_SingleChoiceValue{
SingleChoiceValue: &settingsmsg.SingleChoiceList{
Options: []*settingsmsg.ListOption{
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "bg",
},
},
DisplayValue: "български",
},
{
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",
},
{
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",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "nl",
},
},
DisplayValue: "Nederlands",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "ko",
},
},
DisplayValue: "한국어",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "sq",
},
},
DisplayValue: "Shqipja",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "sv",
},
},
DisplayValue: "Svenska",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "tr",
},
},
DisplayValue: "Türkçe",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "zh",
},
},
DisplayValue: "汉语",
},
},
},
}
// DefaultRoleAssignments returns (as one might guess) the default role assignments
func DefaultRoleAssignments(cfg *config.Config) []*settingsmsg.UserRoleAssignment {
assignments := []*settingsmsg.UserRoleAssignment{}
if cfg.SetupDefaultAssignments {
assignments = []*settingsmsg.UserRoleAssignment{
// default users with role "user"
{
AccountUuid: "b1f74ec4-dd7e-11ef-a543-03775734d0f7",
RoleId: BundleUUIDRoleUser,
},
{
AccountUuid: "056fc874-dd7f-11ef-ba84-af6fca4b7289",
RoleId: BundleUUIDRoleUser,
},
{
AccountUuid: "60708dda-e897-11ef-919f-bbb7437d6ec2",
RoleId: BundleUUIDRoleUser,
},
{
// additional admin user
AccountUuid: "cd88bf9a-dd7f-11ef-a609-7f78deb2345f", // demo user "dennis"
RoleId: BundleUUIDRoleAdmin,
},
{
// default users with role "spaceadmin"
AccountUuid: "801abee4-dd7f-11ef-a324-83f55a754b62",
RoleId: BundleUUIDRoleSpaceAdmin,
},
{
// service user
AccountUuid: "service-user-id",
RoleId: BundleUUIDRoleAdmin,
},
}
}
if cfg.AdminUserID != "" {
// default admin user
assignments = append(assignments, &settingsmsg.UserRoleAssignment{
AccountUuid: cfg.AdminUserID,
RoleId: BundleUUIDRoleAdmin,
})
}
return assignments
}
@@ -0,0 +1,642 @@
package defaults
import settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
var (
// All is a convenience variable to set constraint to all
All = settingsmsg.Permission_CONSTRAINT_ALL
// Own is a convenience variable to set constraint to own
Own = settingsmsg.Permission_CONSTRAINT_OWN
)
// AccountManagementPermission is the permission to manage accounts
func AccountManagementPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "8e587774-d929-4215-910b-a317b1e80f73",
Name: "Accounts.ReadWrite",
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: c,
},
},
}
}
// AutoAcceptSharesPermission is the permission to enable share auto-accept
func AutoAcceptSharesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "4e41363c-a058-40a5-aec8-958897511209",
Name: "AutoAcceptShares.ReadWriteDisabled",
DisplayName: "enable/disable auto accept shares",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileAutoAcceptShares,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ChangeLogoPermission is the permission to change the logo
func ChangeLogoPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "ed83fc10-1f54-4a9e-b5a7-fb517f5f3e01",
Name: "Logo.Write",
DisplayName: "Change logo",
Description: "This permission permits to change the system logo.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// CreatePublicLinkPermission is the permission to create public links
func CreatePublicLinkPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "11516bbd-7157-49e1-b6ac-d00c820f980b",
Name: "PublicLink.Write",
DisplayName: "Write publiclink",
Description: "This permission allows creating public links.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SHARE,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_WRITE,
Constraint: c,
},
},
}
}
// CreateSharePermission is the permission to create shares
func CreateSharePermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "069c08b1-e31f-4799-9ed6-194b310e7244",
Name: "Shares.Write",
DisplayName: "Write share",
Description: "This permission allows creating shares.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SHARE,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_WRITE,
Constraint: c,
},
},
}
}
// CreateSpacesPermission is the permission to create spaces
func CreateSpacesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "79e13b30-3e22-11eb-bc51-0b9f0bad9a58",
Name: "Drives.Create",
DisplayName: "Create Space",
Description: "This permission allows creating new spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// DeletePersonalSpacesPermission is the permission to delete personal spaces
func DeletePersonalSpacesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "5de9fe0a-4bc5-4a47-b758-28f370caf169",
Name: "Drives.DeletePersonal",
DisplayName: "Delete All Home Spaces",
Description: "This permission allows deleting home spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_DELETE,
Constraint: c,
},
},
}
}
// DeleteProjectSpacesPermission is the permission to delete project spaces
func DeleteProjectSpacesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "fb60b004-c1fa-4f09-bf87-55ce7d46ac61",
Name: "Drives.DeleteProject",
DisplayName: "Delete AllSpaces",
Description: "This permission allows deleting all spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_DELETE,
Constraint: c,
},
},
}
}
// DeleteReadOnlyPublicLinkPasswordPermission is the permission to delete read-only public link passwords
func DeleteReadOnlyPublicLinkPasswordPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "e9a697c5-c67b-40fc-982b-bcf628e9916d",
Name: "ReadOnlyPublicLinkPassword.Delete",
DisplayName: "Delete Read-Only Public link password",
Description: "This permission permits to opt out of a public link password enforcement.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SHARE,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_WRITE,
Constraint: c,
},
},
}
}
// DisableEmailNotificationsPermission is the permission to disable email notifications
func DisableEmailNotificationsPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "ad5bb5e5-dc13-4cd3-9304-09a424564ea8",
Name: "EmailNotifications.ReadWriteDisabled",
DisplayName: "Disable Email Notifications",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileDisableNotifications,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ProfileEmailSendingIntervalPermission is the permission to set the email sending interval
func ProfileEmailSendingIntervalPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "7dc204ee-799a-43b6-b85d-425fb3b1fa5a",
Name: "EmailSendingInterval.ReadWrite",
DisplayName: "Email Sending Interval",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileEmailSendingInterval,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ProfileEventShareCreatedPermission is
func ProfileEventShareCreatedPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "8a50540c-1cdd-481f-b85f-44654393c8f0",
Name: "Event.ShareCreated.ReadWrite",
DisplayName: "Event Share Created",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileEventShareCreated,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ProfileEventShareRemovedPermission is the permission to set the email sending interval
func ProfileEventShareRemovedPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "5ef55465-8e39-4a6c-ba97-1d19f5b07116",
Name: "Event.ShareRemoved.ReadWrite",
DisplayName: "Event Share Removed",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileEventShareRemoved,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ProfileEventShareExpiredPermission is the permission to set the email sending interval
func ProfileEventShareExpiredPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "7d4f961b-d471-451b-b1fd-ac6a9d59ce88",
Name: "Event.ShareExpired.ReadWrite",
DisplayName: "Event Share Expired",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileEventShareExpired,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ProfileEventSpaceSharedPermission is the permission to set the email sending interval
func ProfileEventSpaceSharedPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "feb16d2c-614c-4f79-ac37-755a028f5616",
Name: "Event.SpaceShared.ReadWrite",
DisplayName: "Event Space Shared",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileEventSpaceShared,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ProfileEventSpaceUnsharedPermission is the permission to set the email sending interval
func ProfileEventSpaceUnsharedPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "4f979732-631b-4f27-9be7-a89fb223a6d2",
Name: "Event.SpaceUnshared.ReadWrite",
DisplayName: "Event Space Unshared",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileEventSpaceUnshared,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ProfileEventSpaceMembershipExpiredPermission is the permission to set the email sending interval
func ProfileEventSpaceMembershipExpiredPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "a3cc45bf-9720-4e08-b403-b9133fe33f0b",
Name: "Event.SpaceMembershipExpired.ReadWrite",
DisplayName: "Event Space Membership Expired",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileEventSpaceMembershipExpired,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ProfileEventSpaceDisabledPermission is the permission to set the email sending interval
func ProfileEventSpaceDisabledPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "896194c2-5055-4ea3-94a3-0a1419187a00",
Name: "Event.SpaceDisabled.ReadWrite",
DisplayName: "Event Space Disabled",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileEventSpaceDisabled,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ProfileEventSpaceDeletedPermission is the permission to set the email sending interval
func ProfileEventSpaceDeletedPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "2083c280-b140-4b73-a931-9a4af2931531",
Name: "Event.SpaceDeleted.ReadWrite",
DisplayName: "Event Space Deleted",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileEventSpaceDeleted,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ProfileEventPostprocessingStepFinishedPermission is the permission to set the email sending interval
func ProfileEventPostprocessingStepFinishedPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "27ba8e97-0bdf-4b18-97d4-df44c9568cda",
Name: "Event.PostprocessingStepFinished.ReadWrite",
DisplayName: "Event Postprocessing Step Finished",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileEventPostprocessingStepFinished,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// GroupManagementPermission is the permission to manage groups
func GroupManagementPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "522adfbe-5908-45b4-b135-41979de73245",
Name: "Groups.ReadWrite",
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: c,
},
},
}
}
// LanguageManagementPermission is the permission to manage the language
func LanguageManagementPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "7d81f103-0488-4853-bce5-98dcce36d649",
Name: "Language.ReadWrite",
DisplayName: "Permission to read and set the language",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: SettingUUIDProfileLanguage,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// ListFavoritesPermission is the permission to list favorites
func ListFavoritesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "4ebaa725-bfaa-43c5-9817-78bc9994bde4",
Name: "Favorites.List",
DisplayName: "List Favorites",
Description: "This permission allows listing favorites.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: c,
},
},
}
}
// ListSpacesPermission is the permission to list spaces
func ListSpacesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "016f6ddd-9501-4a0a-8ebe-64a20ee8ec82",
Name: "Drives.List",
DisplayName: "List All Spaces",
Description: "This permission allows listing all spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: c,
},
},
}
}
// ManageSpacePropertiesPermission is the permission to manage space properties
func ManageSpacePropertiesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "b44b4054-31a2-42b8-bb71-968b15cfbd4f",
Name: "Drives.ReadWrite",
DisplayName: "Manage space properties",
Description: "This permission allows managing space properties such as name and description.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// RoleManagementPermission is the permission to manage roles
func RoleManagementPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "a53e601e-571f-4f86-8fec-d4576ef49c62",
Name: "Roles.ReadWrite",
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: c,
},
},
}
}
// SelfManagementPermission is the permission to manage itself
func SelfManagementPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "e03070e9-4362-4cc6-a872-1c7cb2eb2b8e",
Name: "Self.ReadWrite",
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: c,
},
},
}
}
// SetPersonalSpaceQuotaPermission is the permission to set the quota for personal spaces
func SetPersonalSpaceQuotaPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "4e6f9709-f9e7-44f1-95d4-b762d27b7896",
Name: "Drives.ReadWritePersonalQuota",
DisplayName: "Set Personal Space Quota",
Description: "This permission allows managing personal space quotas.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// SetProjectSpaceQuotaPermission is the permission to set the quota for project spaces
func SetProjectSpaceQuotaPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "977f0ae6-0da2-4856-93f3-22e0a8482489",
Name: "Drives.ReadWriteProjectQuota",
DisplayName: "Set Project Space Quota",
Description: "This permission allows managing project space quotas.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// SettingsManagementPermission is the permission to manage settings
func SettingsManagementPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "3d58f441-4a05-42f8-9411-ef5874528ae1",
Name: "Settings.ReadWrite",
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: c,
},
},
}
}
// SpaceAbilityPermission is the permission to enable or disable spaces
func SpaceAbilityPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "cf3faa8c-50d9-4f84-9650-ff9faf21aa9d",
Name: "Drives.ReadWriteEnabled",
DisplayName: "Space ability",
Description: "This permission allows enabling and disabling spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
// WriteFavoritesPermission is the permission to mark/unmark files as favorites
func WriteFavoritesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "a54778fd-1c45-47f0-892d-655caf5236f2",
Name: "Favorites.Write",
DisplayName: "Write Favorites",
Description: "This permission allows marking files as favorites.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_FILE,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_WRITE,
Constraint: c,
},
},
}
}
// WebOfficManagementPermssion is the permission to mark/unmark files as favorites
func WebOfficeManagementPermssion(c settingsmsg.Permission_Constraint) *settingsmsg.Setting {
return &settingsmsg.Setting{
Id: "27a29046-a816-424f-bd71-2ffb9029162f",
Name: "WebOffice.Manage",
DisplayName: "Manage WebOffice",
Description: "This permission gives access to the admin features in the WebOffice suite.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: c,
},
},
}
}
@@ -0,0 +1,55 @@
package defaults
import "github.com/qsfera/server/pkg/l10n"
// Translatable configuration options
var (
// name of the notification option 'Share Received'
TemplateShareCreated = l10n.Template("Share Received")
// description of the notification option 'Share Received'
TemplateShareCreatedDescription = l10n.Template("Notify when I have received a share")
// name of the notification option 'Share Removed'
TemplateShareRemoved = l10n.Template("Share Removed")
// description of the notification option 'Share Removed'
TemplateShareRemovedDescription = l10n.Template("Notify when a received share has been removed")
// name of the notification option 'Share Expired'
TemplateShareExpired = l10n.Template("Share Expired")
// description of the notification option 'Share Expired'
TemplateShareExpiredDescription = l10n.Template("Notify when a received share has expired")
// name of the notification option 'Space Shared'
TemplateSpaceShared = l10n.Template("Added as space member")
// description of the notification option 'Space Shared'
TemplateSpaceSharedDescription = l10n.Template("Notify when I have been added as a member to a space")
// name of the notification option 'Space Unshared'
TemplateSpaceUnshared = l10n.Template("Removed as space member")
// description of the notification option 'Space Unshared'
TemplateSpaceUnsharedDescription = l10n.Template("Notify when I have been removed as member from a space")
// name of the notification option 'Space Membership Expired'
TemplateSpaceMembershipExpired = l10n.Template("Space membership expired")
// description of the notification option 'Space Membership Expired'
TemplateSpaceMembershipExpiredDescription = l10n.Template("Notify when a space membership has expired")
// name of the notification option 'Space Disabled'
TemplateSpaceDisabled = l10n.Template("Space disabled")
// description of the notification option 'Space Disabled'
TemplateSpaceDisabledDescription = l10n.Template("Notify when a space I am member of has been disabled")
// name of the notification option 'Space Deleted'
TemplateSpaceDeleted = l10n.Template("Space deleted")
// description of the notification option 'Space Deleted'
TemplateSpaceDeletedDescription = l10n.Template("Notify when a space I am member of has been deleted")
// name of the notification option 'File Rejected'
TemplateFileRejected = l10n.Template("File rejected")
// description of the notification option 'File Rejected'
TemplateFileRejectedDescription = l10n.Template("Notify when a file I uploaded was rejected because of a virus infection or policy violation")
// name of the notification option 'Email Interval'
TemplateEmailSendingInterval = l10n.Template("Email sending interval")
// description of the notification option 'Email Interval'
TemplateEmailSendingIntervalDescription = l10n.Template("Selected value:")
// translation for the 'instant' email interval option
TemplateIntervalInstant = l10n.Template("Instant")
// translation for the 'daily' email interval option
TemplateIntervalDaily = l10n.Template("Daily")
// translation for the 'weekly' email interval option
TemplateIntervalWeekly = l10n.Template("Weekly")
// translation for the 'never' email interval option
TemplateIntervalNever = l10n.Template("Never")
)
@@ -0,0 +1,11 @@
package errortypes
// BundleNotFound is the error to use when a bundle is not found.
//
// Deprecated: use the genreric services/settings/pkg/settings.NotFound error
type BundleNotFound string
func (e BundleNotFound) Error() string { return "error: bundle not found: " + string(e) }
// IsBundleNotFound implements the IsBundleNotFound interface.
func (e BundleNotFound) IsBundleNotFound() {}
@@ -0,0 +1,189 @@
// Package store implements the go-micro store interface
package store
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/qsfera/server/services/settings/pkg/settings"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
)
// ListRoleAssignments loads and returns all role assignments matching the given assignment identifier.
func (s *Store) ListRoleAssignments(accountUUID string) ([]*settingsmsg.UserRoleAssignment, error) {
// shortcut for service accounts
for _, serviceAccountID := range s.cfg.ServiceAccountIDs {
if accountUUID == serviceAccountID {
return []*settingsmsg.UserRoleAssignment{
{
Id: uuid.NewString(), // should we hardcode this id too?
AccountUuid: accountUUID,
RoleId: defaults.BundleUUIDServiceAccount,
},
}, nil
}
}
s.Init()
ctx := context.TODO()
assIDs, err := s.mdc.ReadDir(ctx, accountPath(accountUUID))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
return make([]*settingsmsg.UserRoleAssignment, 0), nil
default:
return nil, err
}
ass := make([]*settingsmsg.UserRoleAssignment, 0, len(assIDs))
for _, assID := range assIDs {
b, err := s.mdc.SimpleDownload(ctx, assignmentPath(accountUUID, assID))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
continue
default:
return nil, err
}
a := &settingsmsg.UserRoleAssignment{}
err = json.Unmarshal(b, a)
if err != nil {
return nil, err
}
ass = append(ass, a)
}
return ass, nil
}
// ListRoleAssignmentsByRole returns all role assignmentes matching the give roleID
func (s *Store) ListRoleAssignmentsByRole(roleID string) ([]*settingsmsg.UserRoleAssignment, error) {
s.Init()
ctx := context.TODO()
accountIDs, err := s.mdc.ReadDir(ctx, accountsFolderLocation)
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
return make([]*settingsmsg.UserRoleAssignment, 0), nil
default:
return nil, err
}
assignments := make([]*settingsmsg.UserRoleAssignment, 0, len(accountIDs))
// This is very inefficient, with the current layout we need to iterated through all
// account folders and read each assignment file in there to check if that contains
// the give role ID.
for _, account := range accountIDs {
assignmentIDs, err := s.mdc.ReadDir(ctx, accountPath(account))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
return make([]*settingsmsg.UserRoleAssignment, 0), nil
default:
return nil, err
}
for _, assignmentID := range assignmentIDs {
b, err := s.mdc.SimpleDownload(ctx, assignmentPath(account, assignmentID))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
continue
default:
return nil, err
}
a := &settingsmsg.UserRoleAssignment{}
err = json.Unmarshal(b, a)
if err != nil {
return nil, err
}
if a.GetRoleId() == roleID {
assignments = append(assignments, a)
}
}
}
return assignments, nil
}
// WriteRoleAssignment appends the given role assignment to the existing assignments of the respective account.
func (s *Store) WriteRoleAssignment(accountUUID, roleID string) (*settingsmsg.UserRoleAssignment, error) {
s.Init()
ctx := context.TODO()
// as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role"
err := s.mdc.Delete(ctx, accountPath(accountUUID))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
// already gone, continue
default:
return nil, err
}
err = s.mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID))
if err != nil {
return nil, err
}
ass := &settingsmsg.UserRoleAssignment{
Id: uuid.NewString(),
AccountUuid: accountUUID,
RoleId: roleID,
}
b, err := json.Marshal(ass)
if err != nil {
return nil, err
}
return ass, s.mdc.SimpleUpload(ctx, assignmentPath(accountUUID, ass.Id), b)
}
// RemoveRoleAssignment deletes the given role assignment from the existing assignments of the respective account.
func (s *Store) RemoveRoleAssignment(assignmentID string) error {
s.Init()
ctx := context.TODO()
accounts, err := s.mdc.ReadDir(ctx, accountsFolderLocation)
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
return fmt.Errorf("assignmentID '%s' %w", assignmentID, settings.ErrNotFound)
default:
return err
}
// TODO: use indexer to avoid spamming Metadata service
for _, accID := range accounts {
assIDs, err := s.mdc.ReadDir(ctx, accountPath(accID))
if err != nil {
// TODO: error?
continue
}
for _, assID := range assIDs {
if assID == assignmentID {
// as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role"
// we also have to delete the cached dir listing
return s.mdc.Delete(ctx, accountPath(accID))
}
}
}
return fmt.Errorf("assignmentID '%s' %w", assignmentID, settings.ErrNotFound)
}
func accountPath(accountUUID string) string {
return fmt.Sprintf("%s/%s", accountsFolderLocation, accountUUID)
}
func assignmentPath(accountUUID string, assignmentID string) string {
return fmt.Sprintf("%s/%s/%s", accountsFolderLocation, accountUUID, assignmentID)
}
@@ -0,0 +1,279 @@
package store
import (
"log"
"sync"
"testing"
"github.com/google/uuid"
olog "github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/shared"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/qsfera/server/services/settings/pkg/config/defaults"
"github.com/stretchr/testify/require"
)
var (
alan = "00000000-0000-0000-0000-000000000001"
mary = "00000000-0000-0000-0000-000000000002"
dennis = "00000000-0000-0000-0000-000000000003"
role1 = "11111111-1111-1111-1111-111111111111"
role2 = "22222222-2222-2222-2222-222222222222"
logger = olog.NewLogger(
olog.Color(true),
olog.Pretty(true),
olog.Level("info"),
)
bundles = []*settingsmsg.Bundle{
{
Id: "f36db5e6-a03c-40df-8413-711c67e40b47",
Type: settingsmsg.Bundle_TYPE_ROLE,
DisplayName: "test role - reads | update",
Name: "TEST_ROLE",
Extension: "qsfera-settings",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
},
Settings: []*settingsmsg.Setting{
{
Id: "updateID",
Name: "update",
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_UPDATE,
},
},
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
},
},
{
Id: "readID",
Name: "read",
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
},
},
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
},
},
},
},
{
Id: "44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
Type: settingsmsg.Bundle_TYPE_ROLE,
DisplayName: "another",
Name: "ANOTHER_TEST_ROLE",
Extension: "qsfera-settings",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
},
Settings: []*settingsmsg.Setting{
{
Id: "readID",
Name: "read",
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
},
},
},
},
},
}
)
func initStore() *Store {
s := &Store{
Logger: logger,
l: &sync.Mutex{},
cfg: defaults.DefaultConfig(),
}
s.cfg.Commons = &shared.Commons{
AdminUserID: uuid.NewString(),
}
_ = NewMDC(s)
return s
}
func setupRoles(s *Store) {
for i := range bundles {
if _, err := s.WriteBundle(bundles[i]); err != nil {
log.Fatal("error initializing ", err)
}
}
}
func TestAssignmentUniqueness(t *testing.T) {
var scenarios = []struct {
name string
userID string
firstRole string
secondRole string
}{
{
"roles assignments",
alan,
"f36db5e6-a03c-40df-8413-711c67e40b47",
"44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
s := initStore()
setupRoles(s)
firstAssignment, err := s.WriteRoleAssignment(scenario.userID, scenario.firstRole)
require.NoError(t, err)
require.Equal(t, firstAssignment.RoleId, scenario.firstRole)
// TODO: check entry exists
list, err := s.ListRoleAssignments(scenario.userID)
require.NoError(t, err)
require.Equal(t, 1, len(list))
require.Equal(t, list[0].RoleId, scenario.firstRole)
// creating another assignment shouldn't add another entry, as we support max one role per user.
// assigning the second role should remove the old
secondAssignment, err := s.WriteRoleAssignment(scenario.userID, scenario.secondRole)
require.NoError(t, err)
require.Equal(t, secondAssignment.RoleId, scenario.secondRole)
list, err = s.ListRoleAssignments(scenario.userID)
require.NoError(t, err)
require.Equal(t, 1, len(list))
require.Equal(t, list[0].RoleId, scenario.secondRole)
})
}
}
func TestListRoleAssignmentByRole(t *testing.T) {
type assignment struct {
userID string
roleID string
}
var scenarios = []struct {
name string
assignments []assignment
queryRole string
numResults int
}{
{
name: "just 2 assignments",
assignments: []assignment{
{
userID: alan,
roleID: role1,
}, {
userID: mary,
roleID: role1,
},
},
queryRole: role1,
numResults: 2,
},
{
name: "no assignments match",
assignments: []assignment{
{
userID: alan,
roleID: role1,
}, {
userID: mary,
roleID: role1,
},
},
queryRole: role2,
numResults: 0,
},
{
name: "only one assignment matches",
assignments: []assignment{
{
userID: alan,
roleID: role1,
}, {
userID: mary,
roleID: role1,
}, {
userID: dennis,
roleID: role2,
},
},
queryRole: role2,
numResults: 1,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
s := initStore()
setupRoles(s)
for _, a := range scenario.assignments {
ass, err := s.WriteRoleAssignment(a.userID, a.roleID)
require.NoError(t, err)
require.Equal(t, ass.RoleId, a.roleID)
}
list, err := s.ListRoleAssignmentsByRole(scenario.queryRole)
require.NoError(t, err)
require.Equal(t, scenario.numResults, len(list))
for _, ass := range list {
require.Equal(t, ass.RoleId, scenario.queryRole)
}
})
}
}
func TestDeleteAssignment(t *testing.T) {
var scenarios = []struct {
name string
userID string
firstRole string
secondRole string
}{
{
"roles assignments",
alan,
"f36db5e6-a03c-40df-8413-711c67e40b47",
"44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
s := initStore()
setupRoles(s)
assignment, err := s.WriteRoleAssignment(scenario.userID, scenario.firstRole)
require.NoError(t, err)
require.Equal(t, assignment.RoleId, scenario.firstRole)
// TODO: uncomment
// require.True(t, mdc.IDExists(assignment.RoleId))
list, err := s.ListRoleAssignments(scenario.userID)
require.NoError(t, err)
require.Equal(t, 1, len(list))
require.Equal(t, assignment.Id, list[0].Id)
err = s.RemoveRoleAssignment(assignment.Id)
require.NoError(t, err)
// TODO: uncomment
// require.False(t, mdc.IDExists(assignment.RoleId))
list, err = s.ListRoleAssignments(scenario.userID)
require.NoError(t, err)
require.Equal(t, 0, len(list))
err = s.RemoveRoleAssignment(assignment.Id)
require.Error(t, err)
// TODO: do we want a custom error message?
})
}
}
@@ -0,0 +1,165 @@
// Package store implements the go-micro store interface
package store
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/qsfera/server/services/settings/pkg/settings"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
)
// ListBundles returns all bundles in the dataPath folder that match the given type.
func (s *Store) ListBundles(bundleType settingsmsg.Bundle_Type, bundleIDs []string) ([]*settingsmsg.Bundle, error) {
s.Init()
ctx := context.TODO()
if len(bundleIDs) == 0 {
bIDs, err := s.mdc.ReadDir(ctx, bundleFolderLocation)
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
return make([]*settingsmsg.Bundle, 0), nil
default:
return nil, err
}
bundleIDs = bIDs
}
var bundles []*settingsmsg.Bundle
for _, id := range bundleIDs {
if id == defaults.BundleUUIDServiceAccount {
bundles = append(bundles, defaults.ServiceAccountBundle())
continue
}
b, err := s.mdc.SimpleDownload(ctx, bundlePath(id))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
continue
default:
return nil, err
}
bundle := &settingsmsg.Bundle{}
err = json.Unmarshal(b, bundle)
if err != nil {
return nil, err
}
if bundle.Type == bundleType {
bundles = append(bundles, bundle)
}
}
return bundles, nil
}
// ReadBundle tries to find a bundle by the given id from the metadata service
func (s *Store) ReadBundle(bundleID string) (*settingsmsg.Bundle, error) {
// shortcut for service accounts
if bundleID == defaults.BundleUUIDServiceAccount {
return defaults.ServiceAccountBundle(), nil
}
s.Init()
ctx := context.TODO()
b, err := s.mdc.SimpleDownload(ctx, bundlePath(bundleID))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
return nil, fmt.Errorf("bundleID '%s' %w", bundleID, settings.ErrNotFound)
default:
return nil, err
}
bundle := &settingsmsg.Bundle{}
return bundle, json.Unmarshal(b, bundle)
}
// ReadSetting tries to find a setting by the given id from the metadata service
func (s *Store) ReadSetting(settingID string) (*settingsmsg.Setting, error) {
s.Init()
ctx := context.TODO()
ids, err := s.mdc.ReadDir(ctx, bundleFolderLocation)
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
return nil, fmt.Errorf("settingID '%s' %w", settingID, settings.ErrNotFound)
default:
return nil, err
}
// TODO: avoid spamming metadata service
for _, id := range ids {
b, err := s.ReadBundle(id)
if err != nil {
if errors.Is(err, settings.ErrNotFound) {
continue
}
return nil, err
}
for _, setting := range b.Settings {
if setting.Id == settingID {
return setting, nil
}
}
}
return nil, fmt.Errorf("settingID '%s' %w", settingID, settings.ErrNotFound)
}
// WriteBundle sends the givens record to the metadataclient. returns `record` for legacy reasons
func (s *Store) WriteBundle(record *settingsmsg.Bundle) (*settingsmsg.Bundle, error) {
s.Init()
ctx := context.TODO()
b, err := json.Marshal(record)
if err != nil {
return nil, err
}
return record, s.mdc.SimpleUpload(ctx, bundlePath(record.Id), b)
}
// AddSettingToBundle adds the given setting to the bundle with the given bundleID.
func (s *Store) AddSettingToBundle(bundleID string, setting *settingsmsg.Setting) (*settingsmsg.Setting, error) {
s.Init()
b, err := s.ReadBundle(bundleID)
if err != nil {
if !errors.Is(err, settings.ErrNotFound) {
return nil, err
}
b = new(settingsmsg.Bundle)
b.Id = bundleID
b.Type = settingsmsg.Bundle_TYPE_DEFAULT
}
if setting.Id == "" {
setting.Id = uuid.NewString()
}
b.Settings = append(b.Settings, setting)
_, err = s.WriteBundle(b)
return setting, err
}
// RemoveSettingFromBundle removes the setting from the bundle with the given ids.
func (s *Store) RemoveSettingFromBundle(bundleID string, settingID string) error {
fmt.Println("RemoveSettingFromBundle not implemented")
return errors.New("not implemented")
}
func bundlePath(id string) string {
return fmt.Sprintf("%s/%s", bundleFolderLocation, id)
}
@@ -0,0 +1,200 @@
package store
import (
"testing"
"github.com/google/uuid"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/stretchr/testify/require"
)
var bundleScenarios = []struct {
name string
bundle *settingsmsg.Bundle
}{
{
name: "generic-test-file-resource",
bundle: &settingsmsg.Bundle{
Id: bundle1,
Type: settingsmsg.Bundle_TYPE_DEFAULT,
Extension: extension1,
DisplayName: "test1",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_FILE,
Id: "beep",
},
Settings: []*settingsmsg.Setting{
{
Id: setting1,
Description: "test-desc-1",
DisplayName: "test-displayname-1",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_FILE,
Id: "bleep",
},
Value: &settingsmsg.Setting_IntValue{
IntValue: &settingsmsg.Int{
Min: 0,
Max: 42,
},
},
},
},
},
},
{
name: "generic-test-system-resource",
bundle: &settingsmsg.Bundle{
Id: bundle2,
Type: settingsmsg.Bundle_TYPE_DEFAULT,
Extension: extension2,
DisplayName: "test1",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
{
Id: setting2,
Description: "test-desc-2",
DisplayName: "test-displayname-2",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_IntValue{
IntValue: &settingsmsg.Int{
Min: 0,
Max: 42,
},
},
},
},
},
},
{
name: "generic-test-role-bundle",
bundle: &settingsmsg.Bundle{
Id: bundle3,
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: extension1,
DisplayName: "Role1",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
{
Id: setting3,
Description: "test-desc-3",
DisplayName: "test-displayname-3",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: setting1,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
},
},
}
var (
appendTestBundleID = uuid.NewString()
appendTestSetting1 = &settingsmsg.Setting{
Id: "append-test-setting-1",
Description: "test-desc-3",
DisplayName: "test-displayname-3",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: setting1,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
}
appendTestSetting2 = &settingsmsg.Setting{
Id: "append-test-setting-2",
Description: "test-desc-3",
DisplayName: "test-displayname-3",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: setting1,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
}
)
func TestBundles(t *testing.T) {
s := initStore()
for i := range bundleScenarios {
b := bundleScenarios[i]
t.Run(b.name, func(t *testing.T) {
_, err := s.WriteBundle(b.bundle)
require.NoError(t, err)
bundle, err := s.ReadBundle(b.bundle.Id)
require.NoError(t, err)
require.Equal(t, b.bundle, bundle)
})
}
// check that ListBundles only returns bundles with type DEFAULT
bundles, err := s.ListBundles(settingsmsg.Bundle_TYPE_DEFAULT, []string{})
require.NoError(t, err)
for i := range bundles {
require.Equal(t, settingsmsg.Bundle_TYPE_DEFAULT, bundles[i].Type)
}
// check that ListBundles filtered by an id only returns that bundle
filteredBundles, err := s.ListBundles(settingsmsg.Bundle_TYPE_DEFAULT, []string{bundle2})
require.NoError(t, err)
require.Equal(t, 1, len(filteredBundles))
if len(filteredBundles) == 1 {
require.Equal(t, bundle2, filteredBundles[0].Id)
}
// check that ListRoles only returns bundles with type ROLE
roles, err := s.ListBundles(settingsmsg.Bundle_TYPE_ROLE, []string{})
require.NoError(t, err)
for i := range roles {
require.Equal(t, settingsmsg.Bundle_TYPE_ROLE, roles[i].Type)
}
// check that ReadSetting works
setting, err := s.ReadSetting(setting1)
require.NoError(t, err)
require.Equal(t, "test-desc-1", setting.Description) // could be tested better ;)
}
func TestAppendSetting(t *testing.T) {
s := initStore()
setupRoles(s)
// appending to non existing bundle creates new
_, err := s.AddSettingToBundle(appendTestBundleID, appendTestSetting1)
require.NoError(t, err)
b, err := s.ReadBundle(appendTestBundleID)
require.NoError(t, err)
require.Len(t, b.Settings, 1)
_, err = s.AddSettingToBundle(appendTestBundleID, appendTestSetting2)
require.NoError(t, err)
b, err = s.ReadBundle(appendTestBundleID)
require.NoError(t, err)
require.Len(t, b.Settings, 2)
}
@@ -0,0 +1,170 @@
package store
import (
"context"
"path"
olog "github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/opencloud-eu/reva/v2/pkg/store"
"github.com/vmihailenco/msgpack/v5"
microstore "go-micro.dev/v4/store"
)
// CachedMDC is cache for the metadataclient
type CachedMDC struct {
cfg *config.Config
logger olog.Logger
next MetadataClient
filesCache microstore.Store
dirsCache microstore.Store
}
// SimpleDownload caches the answer from SimpleDownload or returns the cached one
func (c *CachedMDC) SimpleDownload(ctx context.Context, id string) ([]byte, error) {
if b, err := c.filesCache.Read(id); err == nil && len(b) == 1 {
return b[0].Value, nil
}
b, err := c.next.SimpleDownload(ctx, id)
if err != nil {
return nil, err
}
err = c.filesCache.Write(&microstore.Record{
Key: id,
Value: b,
Expiry: c.cfg.Metadata.Cache.TTL,
})
if err != nil {
c.logger.Error().Err(err).Msg("SimpleDownload: failed to update to files cache")
}
return b, nil
}
// SimpleUpload caches the answer from SimpleUpload and invalidates the cache
func (c *CachedMDC) SimpleUpload(ctx context.Context, id string, content []byte) error {
b, err := c.filesCache.Read(id)
if err == nil && len(b) == 1 && string(b[0].Value) == string(content) {
// no need to bug mdc
return nil
}
err = c.next.SimpleUpload(ctx, id, content)
if err != nil {
return err
}
// invalidate caches
if err = c.dirsCache.Delete(path.Dir(id)); err != nil {
c.logger.Error().Err(err).Msg("failed to clear dirs cache")
}
err = c.filesCache.Write(&microstore.Record{
Key: id,
Value: content,
Expiry: c.cfg.Metadata.Cache.TTL,
})
if err != nil {
c.logger.Error().Err(err).Msg("SimpleUpload: failed to update to files cache")
}
return nil
}
// Delete invalidates the cache when operation was successful
func (c *CachedMDC) Delete(ctx context.Context, id string) error {
if err := c.next.Delete(ctx, id); err != nil {
return err
}
// invalidate caches
_ = c.removePrefix(c.filesCache, id)
_ = c.removePrefix(c.dirsCache, id)
return nil
}
// ReadDir caches the response from ReadDir or returnes the cached one
func (c *CachedMDC) ReadDir(ctx context.Context, id string) ([]string, error) {
i, err := c.dirsCache.Read(id)
if err == nil && len(i) == 1 {
var ret []string
if err = msgpack.Unmarshal(i[0].Value, &ret); err == nil {
return ret, nil
}
c.logger.Error().Err(err).Msg("failed to unmarshal entry from dirs cache")
}
s, err := c.next.ReadDir(ctx, id)
if err != nil {
return nil, err
}
var value []byte
if value, err = msgpack.Marshal(s); err != nil {
c.logger.Error().Err(err).Msg("failed to marshal ReadDir result for dirs cache")
return s, err
}
err = c.dirsCache.Write(&microstore.Record{
Key: id,
Value: value,
Expiry: c.cfg.Metadata.Cache.TTL,
})
if err != nil {
c.logger.Error().Err(err).Msg("ReadDir: failed to update dirs cache")
}
return s, err
}
// MakeDirIfNotExist invalidates the cache
func (c *CachedMDC) MakeDirIfNotExist(ctx context.Context, id string) error {
err := c.next.MakeDirIfNotExist(ctx, id)
if err != nil {
return err
}
// invalidate caches
if err = c.dirsCache.Delete(path.Dir(id)); err != nil {
c.logger.Error().Err(err).Msg("failed to clear dirs cache")
}
return nil
}
// Init instantiates the caches
func (c *CachedMDC) Init(ctx context.Context, id string) error {
c.dirsCache = store.Create(
store.Store(c.cfg.Metadata.Cache.Store),
store.TTL(c.cfg.Metadata.Cache.TTL),
microstore.Nodes(c.cfg.Metadata.Cache.Nodes...),
microstore.Database(c.cfg.Metadata.Cache.Database),
microstore.Table(c.cfg.Metadata.Cache.DirectoryTable),
store.DisablePersistence(c.cfg.Metadata.Cache.DisablePersistence),
store.Authentication(c.cfg.Metadata.Cache.AuthUsername, c.cfg.Metadata.Cache.AuthPassword),
)
c.filesCache = store.Create(
store.Store(c.cfg.Metadata.Cache.Store),
store.TTL(c.cfg.Metadata.Cache.TTL),
microstore.Nodes(c.cfg.Metadata.Cache.Nodes...),
microstore.Database(c.cfg.Metadata.Cache.Database),
microstore.Table(c.cfg.Metadata.Cache.FileTable),
store.DisablePersistence(c.cfg.Metadata.Cache.DisablePersistence),
store.Authentication(c.cfg.Metadata.Cache.AuthUsername, c.cfg.Metadata.Cache.AuthPassword),
)
return c.next.Init(ctx, id)
}
func (c *CachedMDC) removePrefix(cache microstore.Store, prefix string) error {
c.logger.Debug().Str("prefix", prefix).Msg("removePrefix")
keys, err := cache.List(microstore.ListPrefix(prefix))
if err != nil {
c.logger.Error().Err(err).Msg("failed to list cache entries")
}
for _, k := range keys {
c.logger.Debug().Str("key", k).Msg("removePrefix")
if err := cache.Delete(k); err != nil {
c.logger.Error().Err(err).Msg("failed to remove prefix from cache")
return err
}
}
return nil
}
@@ -0,0 +1,72 @@
package store
import (
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/qsfera/server/services/settings/pkg/settings"
"github.com/qsfera/server/services/settings/pkg/util"
)
// ListPermissionsByResource collects all permissions from the provided roleIDs that match the requested resource
func (s *Store) ListPermissionsByResource(resource *settingsmsg.Resource, roleIDs []string) ([]*settingsmsg.Permission, error) {
records := make([]*settingsmsg.Permission, 0)
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
records = append(records, extractPermissionsByResource(resource, role)...)
}
return records, nil
}
// ReadPermissionByID finds the permission in the roles, specified by the provided roleIDs
func (s *Store) ReadPermissionByID(permissionID string, roleIDs []string) (*settingsmsg.Permission, error) {
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
for _, permission := range role.Settings {
if permission.Id == permissionID {
if value, ok := permission.Value.(*settingsmsg.Setting_PermissionValue); ok {
return value.PermissionValue, nil
}
}
}
}
return nil, nil
}
// ReadPermissionByName finds the permission in the roles, specified by the provided roleIDs
func (s *Store) ReadPermissionByName(name string, roleIDs []string) (*settingsmsg.Permission, error) {
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
for _, permission := range role.Settings {
if permission.Name == name {
if value, ok := permission.Value.(*settingsmsg.Setting_PermissionValue); ok {
return value.PermissionValue, nil
}
}
}
}
return nil, settings.ErrNotFound
}
// extractPermissionsByResource collects all permissions from the provided role that match the requested resource
func extractPermissionsByResource(resource *settingsmsg.Resource, role *settingsmsg.Bundle) []*settingsmsg.Permission {
permissions := make([]*settingsmsg.Permission, 0)
for _, setting := range role.Settings {
if value, ok := setting.Value.(*settingsmsg.Setting_PermissionValue); ok {
if util.IsResourceMatched(setting.Resource, resource) {
permissions = append(permissions, value.PermissionValue)
}
}
}
return permissions
}
@@ -0,0 +1,29 @@
package store
import (
"testing"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/stretchr/testify/require"
)
func TestPermission(t *testing.T) {
s := initStore()
setupRoles(s)
// bunldes are initialized within init func
p, err := s.ReadPermissionByID("readID", []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
require.NoError(t, err)
require.Equal(t, settingsmsg.Permission_OPERATION_READ, p.Operation)
p, err = s.ReadPermissionByName("read", []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
require.NoError(t, err)
require.Equal(t, settingsmsg.Permission_OPERATION_READ, p.Operation)
pms, err := s.ListPermissionsByResource(&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
}, []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
require.NoError(t, err)
require.Len(t, pms, 1)
require.Equal(t, settingsmsg.Permission_OPERATION_READ, pms[0].Operation)
}
@@ -0,0 +1,233 @@
// Package store implements the go-micro store interface
package store
import (
"context"
"encoding/json"
"log"
"sync"
"github.com/google/uuid"
olog "github.com/qsfera/server/pkg/log"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/qsfera/server/services/settings/pkg/settings"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
)
var (
// Name is the default name for the settings store
Name = "qsfera-settings"
managerName = "metadata"
settingsSpaceID = "f1bdd61a-da7c-49fc-8203-0558109d1b4f" // uuid.NewString()
rootFolderLocation = "settings"
bundleFolderLocation = "settings/bundles"
accountsFolderLocation = "settings/accounts"
valuesFolderLocation = "settings/values"
)
// MetadataClient is the interface to talk to metadata service
type MetadataClient interface {
SimpleDownload(ctx context.Context, id string) ([]byte, error)
SimpleUpload(ctx context.Context, id string, content []byte) error
Delete(ctx context.Context, id string) error
ReadDir(ctx context.Context, id string) ([]string, error)
MakeDirIfNotExist(ctx context.Context, id string) error
Init(ctx context.Context, id string) error
}
// Store interacts with the filesystem to manage settings information
type Store struct {
Logger olog.Logger
mdc MetadataClient
cfg *config.Config
l *sync.Mutex
}
// Init initialize the store once, later calls are noops
func (s *Store) Init() {
if s.mdc != nil {
return
}
s.l.Lock()
defer s.l.Unlock()
if s.mdc != nil {
return
}
mdc := &CachedMDC{
next: NewMetadataClient(s.cfg.Metadata),
cfg: s.cfg,
logger: s.Logger,
}
if err := s.initMetadataClient(mdc); err != nil {
s.Logger.Error().Err(err).Msg("error initializing metadata client")
}
}
// New creates a new store
func New(cfg *config.Config) settings.Manager {
s := Store{
Logger: olog.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel),
cfg: cfg,
l: &sync.Mutex{},
}
return &s
}
// NewMetadataClient returns the MetadataClient
func NewMetadataClient(cfg config.Metadata) MetadataClient {
mdc, err := metadata.NewCS3Storage(cfg.GatewayAddress, cfg.StorageAddress, cfg.SystemUserID, cfg.SystemUserIDP, cfg.SystemUserAPIKey)
if err != nil {
log.Fatal("error connecting to mdc:", err)
}
return mdc
}
// we need to lazy initialize the MetadataClient because metadata service might not be ready
func (s *Store) initMetadataClient(mdc MetadataClient) error {
ctx := context.TODO()
err := mdc.Init(ctx, settingsSpaceID)
if err != nil {
return err
}
for _, p := range []string{
rootFolderLocation,
accountsFolderLocation,
bundleFolderLocation,
valuesFolderLocation,
} {
err = mdc.MakeDirIfNotExist(ctx, p)
if err != nil {
return err
}
}
for _, p := range s.cfg.Bundles {
b, err := json.Marshal(p)
if err != nil {
return err
}
err = mdc.SimpleUpload(ctx, bundlePath(p.Id), b)
if err != nil {
return err
}
}
for _, p := range defaults.DefaultRoleAssignments(s.cfg) {
accountUUID := p.AccountUuid
roleID := p.RoleId
err = mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID))
if err != nil {
return err
}
assIDs, err := mdc.ReadDir(ctx, accountPath(accountUUID))
if err != nil {
return err
}
adminUserID := accountUUID == s.cfg.AdminUserID
if len(assIDs) > 0 && !adminUserID {
// There is already a role assignment for this ID, skip to the next
continue
}
// for the adminUserID we need to check if the user has the admin role every time
if adminUserID {
err = s.userMustHaveAdminRole(accountUUID, assIDs, mdc)
if err != nil {
return err
}
continue
}
ass := &settingsmsg.UserRoleAssignment{
Id: uuid.NewString(),
AccountUuid: accountUUID,
RoleId: roleID,
}
b, err := json.Marshal(ass)
if err != nil {
return err
}
err = mdc.SimpleUpload(ctx, assignmentPath(accountUUID, ass.Id), b)
if err != nil {
return err
}
}
s.mdc = mdc
return nil
}
func (s *Store) userMustHaveAdminRole(accountUUID string, assIDs []string, mdc MetadataClient) error {
ctx := context.TODO()
var hasAdminRole bool
// load the assignments from the store and check if the admin role is already assigned
for _, assID := range assIDs {
b, err := mdc.SimpleDownload(ctx, assignmentPath(accountUUID, assID))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
continue
default:
return err
}
a := &settingsmsg.UserRoleAssignment{}
err = json.Unmarshal(b, a)
if err != nil {
return err
}
if a.RoleId == defaults.BundleUUIDRoleAdmin {
hasAdminRole = true
}
}
// delete old role assignment and set admin role
if !hasAdminRole {
err := mdc.Delete(ctx, accountPath(accountUUID))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
// already gone, continue
default:
return err
}
err = mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID))
if err != nil {
return err
}
ass := &settingsmsg.UserRoleAssignment{
Id: uuid.NewString(),
AccountUuid: accountUUID,
RoleId: defaults.BundleUUIDRoleAdmin,
}
b, err := json.Marshal(ass)
if err != nil {
return err
}
return mdc.SimpleUpload(ctx, assignmentPath(accountUUID, ass.Id), b)
}
return nil
}
func init() {
settings.Registry[managerName] = New
}
@@ -0,0 +1,190 @@
package store
import (
"context"
"encoding/json"
"strings"
"testing"
. "github.com/onsi/gomega"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/qsfera/server/services/settings/pkg/config/defaults"
rdefaults "github.com/qsfera/server/services/settings/pkg/store/defaults"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
)
const (
// account UUIDs
accountUUID1 = "c4572da7-6142-4383-8fc6-efde3d463036"
//accountUUID2 = "e11f9769-416a-427d-9441-41a0e51391d7"
//accountUUID3 = "633ecd77-1980-412a-8721-bf598a330bb4"
// extension names
extension1 = "test-extension-1"
extension2 = "test-extension-2"
// bundle ids
bundle1 = "2f06addf-4fd2-49d5-8f71-00fbd3a3ec47"
bundle2 = "2d745744-749c-4286-8e92-74a24d8331c5"
bundle3 = "d8fd27d1-c00b-4794-a658-416b756a72ff"
// setting ids
setting1 = "c7ebbc8b-d15a-4f2e-9d7d-d6a4cf858d1a"
setting2 = "3fd9a3d9-20b7-40d4-9294-b22bb5868c10"
setting3 = "24bb9535-3df4-42f1-a622-7c0562bec99f"
// value ids
value1 = "fd3b6221-dc13-4a22-824d-2480495f1cdb"
value2 = "2a0bd9b0-ca1d-491a-8c56-d2ddfd68ded8"
value3 = "b42702d2-5e4d-4d73-b133-e1f9e285355e"
)
// use "unit" or "integration" do define test type. You need a running КуСфера instance for integration tests
var testtype = "unit"
// MockedMetadataClient mocks the metadataservice inmemory
type MockedMetadataClient struct {
data map[string][]byte
}
// NewMDC instantiates a mocked MetadataClient
func NewMDC(s *Store) error {
var mdc MetadataClient
switch testtype {
case "unit":
mdc = &MockedMetadataClient{data: make(map[string][]byte)}
case "integration":
mdc = NewMetadataClient(defaults.DefaultConfig().Metadata)
}
return s.initMetadataClient(mdc)
}
// SimpleDownload returns errtypes.NotFound if not found
func (m *MockedMetadataClient) SimpleDownload(_ context.Context, id string) ([]byte, error) {
if data, ok := m.data[id]; ok {
return data, nil
}
return nil, errtypes.NotFound("not found")
}
// SimpleUpload can't error
func (m *MockedMetadataClient) SimpleUpload(_ context.Context, id string, content []byte) error {
m.data[id] = content
return nil
}
// Delete can't error either
func (m *MockedMetadataClient) Delete(_ context.Context, id string) error {
for k := range m.data {
if strings.HasPrefix(k, id) {
delete(m.data, k)
}
}
return nil
}
// ReadDir returns nil, nil if not found
func (m *MockedMetadataClient) ReadDir(_ context.Context, id string) ([]string, error) {
var out []string
for k := range m.data {
if strings.HasPrefix(k, id) {
dir := strings.TrimPrefix(k, id+"/")
// filter subfolders the lame way
s := strings.Trim(strings.SplitAfter(dir, "/")[0], "/")
out = append(out, s)
}
}
return out, nil
}
// MakeDirIfNotExist does nothing
func (*MockedMetadataClient) MakeDirIfNotExist(_ context.Context, _ string) error {
return nil
}
// Init does nothing
func (*MockedMetadataClient) Init(_ context.Context, _ string) error {
return nil
}
// IDExists is a helper to check if an id exists
func (m *MockedMetadataClient) IDExists(id string) bool {
_, ok := m.data[id]
return ok
}
// IDHasContent returns true if the value stored under id has the given content (converted to string)
func (m *MockedMetadataClient) IDHasContent(id string, content []byte) bool {
return string(m.data[id]) == string(content)
}
// TestAdminUserIDInit test the happy path during initialization
func TestAdminUserIDInit(t *testing.T) {
RegisterTestingT(t)
s := &Store{
cfg: defaults.DefaultConfig(),
}
s.cfg.Bundles = rdefaults.GenerateBundlesDefaultRoles()
s.cfg.AdminUserID = "admin"
// the first assignment is always happening during the initialisation of the metadata client
err := NewMDC(s)
Expect(err).To(BeNil())
assID, err := s.mdc.ReadDir(context.TODO(), accountPath(s.cfg.AdminUserID))
Expect(len(assID)).To(Equal(1))
ass, err := s.mdc.SimpleDownload(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]))
Expect(ass).ToNot(BeNil())
assignment := &settingsmsg.UserRoleAssignment{}
err = json.Unmarshal(ass, assignment)
Expect(err).To(BeNil())
Expect(assignment.RoleId).To(Equal(rdefaults.BundleUUIDRoleAdmin))
}
// TestAdminUserIDUpdate test the update on following initialisations
func TestAdminUserIDUpdate(t *testing.T) {
RegisterTestingT(t)
s := &Store{
cfg: defaults.DefaultConfig(),
}
s.cfg.Bundles = rdefaults.GenerateBundlesDefaultRoles()
s.cfg.AdminUserID = "admin"
// the first assignment is always happening during the initialisation of the metadata client
err := NewMDC(s)
Expect(err).To(BeNil())
// read assignment
assID, err := s.mdc.ReadDir(context.TODO(), accountPath(s.cfg.AdminUserID))
Expect(len(assID)).To(Equal(1))
// set assignment to user role
userRoleAssignment := &settingsmsg.UserRoleAssignment{
AccountUuid: s.cfg.AdminUserID,
RoleId: rdefaults.BundleUUIDRoleUser,
}
b, err := json.Marshal(userRoleAssignment)
err = s.mdc.Delete(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]))
Expect(err).To(BeNil())
err = s.mdc.SimpleUpload(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]), b)
Expect(err).To(BeNil())
// this happens on every Read / Write on the store
// the actual init is only done if the metadata client has not been initialized before
// this normally needs a restart of the service
err = s.initMetadataClient(s.mdc)
Expect(err).To(BeNil())
// read assignment id, changes every time the assignment is written
assID, err = s.mdc.ReadDir(context.TODO(), accountPath(s.cfg.AdminUserID))
Expect(len(assID)).To(Equal(1))
// check if the assignment is the admin role again
ass, err := s.mdc.SimpleDownload(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]))
Expect(ass).ToNot(BeNil())
assignment := &settingsmsg.UserRoleAssignment{}
err = json.Unmarshal(ass, assignment)
Expect(err).To(BeNil())
Expect(assignment.RoleId).To(Equal(rdefaults.BundleUUIDRoleAdmin))
}
@@ -0,0 +1,141 @@
// Package store implements the go-micro store interface
package store
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/qsfera/server/services/settings/pkg/settings"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
)
// ListValues reads all values that match the given bundleId and accountUUID.
// If the bundleId is empty, it's ignored for filtering.
// If the accountUUID is empty, only values with empty accountUUID are returned.
// If the accountUUID is not empty, values with an empty or with a matching accountUUID are returned.
func (s *Store) ListValues(bundleID, accountUUID string) ([]*settingsmsg.Value, error) {
s.Init()
ctx := context.TODO()
vIDs, err := s.mdc.ReadDir(ctx, valuesFolderLocation)
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
return make([]*settingsmsg.Value, 0), nil
default:
return nil, err
}
// TODO: refine logic not to spam metadata service
var values []*settingsmsg.Value
for _, vid := range vIDs {
b, err := s.mdc.SimpleDownload(ctx, valuePath(vid))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
continue
default:
return nil, err
}
v := &settingsmsg.Value{}
err = json.Unmarshal(b, v)
if err != nil {
return nil, err
}
if bundleID != "" && v.BundleId != bundleID {
continue
}
if v.AccountUuid == "" {
values = append(values, v)
continue
}
if v.AccountUuid == accountUUID {
values = append(values, v)
continue
}
}
return values, nil
}
// ReadValue tries to find a value by the given valueId within the dataPath
func (s *Store) ReadValue(valueID string) (*settingsmsg.Value, error) {
s.Init()
ctx := context.TODO()
b, err := s.mdc.SimpleDownload(ctx, valuePath(valueID))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
return nil, fmt.Errorf("valueID '%s' %w", valueID, settings.ErrNotFound)
default:
return nil, err
}
val := &settingsmsg.Value{}
return val, json.Unmarshal(b, val)
}
// ReadValueByUniqueIdentifiers tries to find a value given a set of unique identifiers
func (s *Store) ReadValueByUniqueIdentifiers(accountUUID, settingID string) (*settingsmsg.Value, error) {
if settingID == "" {
return nil, fmt.Errorf("settingID can not be empty %w", settings.ErrNotFound)
}
s.Init()
ctx := context.TODO()
vIDs, err := s.mdc.ReadDir(ctx, valuesFolderLocation)
if err != nil {
return nil, err
}
for _, vid := range vIDs {
b, err := s.mdc.SimpleDownload(ctx, valuePath(vid))
switch err.(type) {
case nil:
// continue
case errtypes.NotFound:
continue
default:
return nil, err
}
v := &settingsmsg.Value{}
err = json.Unmarshal(b, v)
if err != nil {
return nil, err
}
if v.AccountUuid == accountUUID && v.SettingId == settingID {
return v, nil
}
}
return nil, settings.ErrNotFound
}
// WriteValue writes the given value into a file within the dataPath
func (s *Store) WriteValue(value *settingsmsg.Value) (*settingsmsg.Value, error) {
s.Init()
ctx := context.TODO()
if value.Id == "" {
value.Id = uuid.NewString()
}
b, err := json.Marshal(value)
if err != nil {
return nil, err
}
return value, s.mdc.SimpleUpload(ctx, valuePath(value.Id), b)
}
func valuePath(id string) string {
return fmt.Sprintf("%s/%s", valuesFolderLocation, id)
}
@@ -0,0 +1,104 @@
package store
import (
"testing"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"github.com/stretchr/testify/require"
)
var valueScenarios = []struct {
name string
value *settingsmsg.Value
}{
{
name: "generic-test-with-system-resource",
value: &settingsmsg.Value{
Id: value1,
BundleId: bundle1,
SettingId: setting1,
AccountUuid: accountUUID1,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Value_StringValue{
StringValue: "lalala",
},
},
},
{
name: "generic-test-with-file-resource",
value: &settingsmsg.Value{
Id: value2,
BundleId: bundle2,
SettingId: setting2,
AccountUuid: accountUUID1,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_FILE,
Id: "adfba82d-919a-41c3-9cd1-5a3f83b2bf76",
},
Value: &settingsmsg.Value_StringValue{
StringValue: "tralala",
},
},
},
{
name: "value without accountUUID",
value: &settingsmsg.Value{
Id: value3,
BundleId: bundle3,
SettingId: setting2,
AccountUuid: "",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_FILE,
Id: "adfba82d-919a-41c3-9cd1-5a3f83b2bf76",
},
Value: &settingsmsg.Value_StringValue{
StringValue: "tralala",
},
},
},
}
func TestValues(t *testing.T) {
for i := range valueScenarios {
index := i
t.Run(valueScenarios[index].name, func(t *testing.T) {
s := initStore()
setupRoles(s)
value := valueScenarios[index].value
v, err := s.WriteValue(value)
require.NoError(t, err)
require.Equal(t, value, v)
v, err = s.ReadValue(value.Id)
require.NoError(t, err)
require.Equal(t, value, v)
})
}
}
func TestListValues(t *testing.T) {
s := initStore()
setupRoles(s)
for _, v := range valueScenarios {
_, err := s.WriteValue(v.value)
require.NoError(t, err)
}
// empty accountid returns only values with empty accountud
vs, err := s.ListValues("", "")
require.NoError(t, err)
require.Len(t, vs, 1)
// filled accountid returns matching and empty accountUUID values
vs, err = s.ListValues("", accountUUID1)
require.NoError(t, err)
require.Len(t, vs, 3)
// filled bundleid only returns matching values
vs, err = s.ListValues(bundle3, accountUUID1)
require.NoError(t, err)
require.Len(t, vs, 1)
}
@@ -0,0 +1,6 @@
package store
import (
// init filesystem store
_ "github.com/qsfera/server/services/settings/pkg/store/metadata"
)
@@ -0,0 +1,18 @@
package util
import (
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
)
const (
// ResourceIDAll declares on a resource that it matches any id
ResourceIDAll = "all"
)
// IsResourceMatched checks if the `example` resource is an exact match or a subset of `definition`
func IsResourceMatched(definition, example *settingsmsg.Resource) bool {
if definition.Type != example.Type {
return false
}
return definition.Id == ResourceIDAll || definition.Id == example.Id
}
@@ -0,0 +1,91 @@
package util
import (
"testing"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
"gotest.tools/v3/assert"
)
func TestIsResourceMatched(t *testing.T) {
scenarios := []struct {
name string
definition *settingsmsg.Resource
example *settingsmsg.Resource
matched bool
}{
{
"same resource types without ids match",
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
true,
},
{
"different resource types without ids don't match",
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
false,
},
{
"same resource types with different ids don't match",
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "alan",
},
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "mary",
},
false,
},
{
"same resource types with same ids match",
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "alan",
},
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "alan",
},
true,
},
{
"same resource types with definition = ALL and without id in example is a match",
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: ResourceIDAll,
},
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
true,
},
{
"same resource types with definition.id = ALL and with some id in example is a match",
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: ResourceIDAll,
},
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "alan",
},
true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
assert.Equal(t, scenario.matched, IsResourceMatched(scenario.definition, scenario.example))
})
}
}