Initial QSfera import
This commit is contained in:
@@ -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(µstore.Record{
|
||||
Key: id,
|
||||
Value: b,
|
||||
Expiry: c.cfg.Metadata.Cache.TTL,
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("SimpleDownload: failed to update to files cache")
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// SimpleUpload caches the answer from SimpleUpload and invalidates the cache
|
||||
func (c *CachedMDC) SimpleUpload(ctx context.Context, id string, content []byte) error {
|
||||
b, err := c.filesCache.Read(id)
|
||||
if err == nil && len(b) == 1 && string(b[0].Value) == string(content) {
|
||||
// no need to bug mdc
|
||||
return nil
|
||||
}
|
||||
|
||||
err = c.next.SimpleUpload(ctx, id, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// invalidate caches
|
||||
if err = c.dirsCache.Delete(path.Dir(id)); err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to clear dirs cache")
|
||||
}
|
||||
|
||||
err = c.filesCache.Write(µstore.Record{
|
||||
Key: id,
|
||||
Value: content,
|
||||
Expiry: c.cfg.Metadata.Cache.TTL,
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("SimpleUpload: failed to update to files cache")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete invalidates the cache when operation was successful
|
||||
func (c *CachedMDC) Delete(ctx context.Context, id string) error {
|
||||
if err := c.next.Delete(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// invalidate caches
|
||||
_ = c.removePrefix(c.filesCache, id)
|
||||
_ = c.removePrefix(c.dirsCache, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadDir caches the response from ReadDir or returnes the cached one
|
||||
func (c *CachedMDC) ReadDir(ctx context.Context, id string) ([]string, error) {
|
||||
i, err := c.dirsCache.Read(id)
|
||||
if err == nil && len(i) == 1 {
|
||||
var ret []string
|
||||
if err = msgpack.Unmarshal(i[0].Value, &ret); err == nil {
|
||||
return ret, nil
|
||||
}
|
||||
c.logger.Error().Err(err).Msg("failed to unmarshal entry from dirs cache")
|
||||
}
|
||||
|
||||
s, err := c.next.ReadDir(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var value []byte
|
||||
if value, err = msgpack.Marshal(s); err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to marshal ReadDir result for dirs cache")
|
||||
return s, err
|
||||
}
|
||||
err = c.dirsCache.Write(µstore.Record{
|
||||
Key: id,
|
||||
Value: value,
|
||||
Expiry: c.cfg.Metadata.Cache.TTL,
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("ReadDir: failed to update dirs cache")
|
||||
}
|
||||
|
||||
return s, err
|
||||
}
|
||||
|
||||
// MakeDirIfNotExist invalidates the cache
|
||||
func (c *CachedMDC) MakeDirIfNotExist(ctx context.Context, id string) error {
|
||||
err := c.next.MakeDirIfNotExist(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// invalidate caches
|
||||
if err = c.dirsCache.Delete(path.Dir(id)); err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to clear dirs cache")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init instantiates the caches
|
||||
func (c *CachedMDC) Init(ctx context.Context, id string) error {
|
||||
c.dirsCache = store.Create(
|
||||
store.Store(c.cfg.Metadata.Cache.Store),
|
||||
store.TTL(c.cfg.Metadata.Cache.TTL),
|
||||
microstore.Nodes(c.cfg.Metadata.Cache.Nodes...),
|
||||
microstore.Database(c.cfg.Metadata.Cache.Database),
|
||||
microstore.Table(c.cfg.Metadata.Cache.DirectoryTable),
|
||||
store.DisablePersistence(c.cfg.Metadata.Cache.DisablePersistence),
|
||||
store.Authentication(c.cfg.Metadata.Cache.AuthUsername, c.cfg.Metadata.Cache.AuthPassword),
|
||||
)
|
||||
c.filesCache = store.Create(
|
||||
store.Store(c.cfg.Metadata.Cache.Store),
|
||||
store.TTL(c.cfg.Metadata.Cache.TTL),
|
||||
microstore.Nodes(c.cfg.Metadata.Cache.Nodes...),
|
||||
microstore.Database(c.cfg.Metadata.Cache.Database),
|
||||
microstore.Table(c.cfg.Metadata.Cache.FileTable),
|
||||
store.DisablePersistence(c.cfg.Metadata.Cache.DisablePersistence),
|
||||
store.Authentication(c.cfg.Metadata.Cache.AuthUsername, c.cfg.Metadata.Cache.AuthPassword),
|
||||
)
|
||||
return c.next.Init(ctx, id)
|
||||
}
|
||||
|
||||
func (c *CachedMDC) removePrefix(cache microstore.Store, prefix string) error {
|
||||
c.logger.Debug().Str("prefix", prefix).Msg("removePrefix")
|
||||
keys, err := cache.List(microstore.ListPrefix(prefix))
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to list cache entries")
|
||||
}
|
||||
for _, k := range keys {
|
||||
c.logger.Debug().Str("key", k).Msg("removePrefix")
|
||||
if err := cache.Delete(k); err != nil {
|
||||
c.logger.Error().Err(err).Msg("failed to remove prefix from cache")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/settings"
|
||||
"github.com/qsfera/server/services/settings/pkg/util"
|
||||
)
|
||||
|
||||
// ListPermissionsByResource collects all permissions from the provided roleIDs that match the requested resource
|
||||
func (s *Store) ListPermissionsByResource(resource *settingsmsg.Resource, roleIDs []string) ([]*settingsmsg.Permission, error) {
|
||||
records := make([]*settingsmsg.Permission, 0)
|
||||
for _, roleID := range roleIDs {
|
||||
role, err := s.ReadBundle(roleID)
|
||||
if err != nil {
|
||||
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
|
||||
continue
|
||||
}
|
||||
records = append(records, extractPermissionsByResource(resource, role)...)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// ReadPermissionByID finds the permission in the roles, specified by the provided roleIDs
|
||||
func (s *Store) ReadPermissionByID(permissionID string, roleIDs []string) (*settingsmsg.Permission, error) {
|
||||
for _, roleID := range roleIDs {
|
||||
role, err := s.ReadBundle(roleID)
|
||||
if err != nil {
|
||||
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
|
||||
continue
|
||||
}
|
||||
for _, permission := range role.Settings {
|
||||
if permission.Id == permissionID {
|
||||
if value, ok := permission.Value.(*settingsmsg.Setting_PermissionValue); ok {
|
||||
return value.PermissionValue, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ReadPermissionByName finds the permission in the roles, specified by the provided roleIDs
|
||||
func (s *Store) ReadPermissionByName(name string, roleIDs []string) (*settingsmsg.Permission, error) {
|
||||
for _, roleID := range roleIDs {
|
||||
role, err := s.ReadBundle(roleID)
|
||||
if err != nil {
|
||||
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
|
||||
continue
|
||||
}
|
||||
for _, permission := range role.Settings {
|
||||
if permission.Name == name {
|
||||
if value, ok := permission.Value.(*settingsmsg.Setting_PermissionValue); ok {
|
||||
return value.PermissionValue, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, settings.ErrNotFound
|
||||
}
|
||||
|
||||
// extractPermissionsByResource collects all permissions from the provided role that match the requested resource
|
||||
func extractPermissionsByResource(resource *settingsmsg.Resource, role *settingsmsg.Bundle) []*settingsmsg.Permission {
|
||||
permissions := make([]*settingsmsg.Permission, 0)
|
||||
for _, setting := range role.Settings {
|
||||
if value, ok := setting.Value.(*settingsmsg.Setting_PermissionValue); ok {
|
||||
if util.IsResourceMatched(setting.Resource, resource) {
|
||||
permissions = append(permissions, value.PermissionValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return permissions
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPermission(t *testing.T) {
|
||||
s := initStore()
|
||||
setupRoles(s)
|
||||
// bunldes are initialized within init func
|
||||
p, err := s.ReadPermissionByID("readID", []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, settingsmsg.Permission_OPERATION_READ, p.Operation)
|
||||
|
||||
p, err = s.ReadPermissionByName("read", []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, settingsmsg.Permission_OPERATION_READ, p.Operation)
|
||||
|
||||
pms, err := s.ListPermissionsByResource(&settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_BUNDLE,
|
||||
}, []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pms, 1)
|
||||
require.Equal(t, settingsmsg.Permission_OPERATION_READ, pms[0].Operation)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// Package store implements the go-micro store interface
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
olog "github.com/qsfera/server/pkg/log"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/config"
|
||||
"github.com/qsfera/server/services/settings/pkg/settings"
|
||||
"github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
// Name is the default name for the settings store
|
||||
Name = "qsfera-settings"
|
||||
managerName = "metadata"
|
||||
settingsSpaceID = "f1bdd61a-da7c-49fc-8203-0558109d1b4f" // uuid.NewString()
|
||||
rootFolderLocation = "settings"
|
||||
bundleFolderLocation = "settings/bundles"
|
||||
accountsFolderLocation = "settings/accounts"
|
||||
valuesFolderLocation = "settings/values"
|
||||
)
|
||||
|
||||
// MetadataClient is the interface to talk to metadata service
|
||||
type MetadataClient interface {
|
||||
SimpleDownload(ctx context.Context, id string) ([]byte, error)
|
||||
SimpleUpload(ctx context.Context, id string, content []byte) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
ReadDir(ctx context.Context, id string) ([]string, error)
|
||||
MakeDirIfNotExist(ctx context.Context, id string) error
|
||||
Init(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// Store interacts with the filesystem to manage settings information
|
||||
type Store struct {
|
||||
Logger olog.Logger
|
||||
|
||||
mdc MetadataClient
|
||||
cfg *config.Config
|
||||
|
||||
l *sync.Mutex
|
||||
}
|
||||
|
||||
// Init initialize the store once, later calls are noops
|
||||
func (s *Store) Init() {
|
||||
if s.mdc != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
|
||||
if s.mdc != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mdc := &CachedMDC{
|
||||
next: NewMetadataClient(s.cfg.Metadata),
|
||||
cfg: s.cfg,
|
||||
logger: s.Logger,
|
||||
}
|
||||
if err := s.initMetadataClient(mdc); err != nil {
|
||||
s.Logger.Error().Err(err).Msg("error initializing metadata client")
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new store
|
||||
func New(cfg *config.Config) settings.Manager {
|
||||
s := Store{
|
||||
Logger: olog.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel),
|
||||
cfg: cfg,
|
||||
l: &sync.Mutex{},
|
||||
}
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
// NewMetadataClient returns the MetadataClient
|
||||
func NewMetadataClient(cfg config.Metadata) MetadataClient {
|
||||
mdc, err := metadata.NewCS3Storage(cfg.GatewayAddress, cfg.StorageAddress, cfg.SystemUserID, cfg.SystemUserIDP, cfg.SystemUserAPIKey)
|
||||
if err != nil {
|
||||
log.Fatal("error connecting to mdc:", err)
|
||||
}
|
||||
return mdc
|
||||
|
||||
}
|
||||
|
||||
// we need to lazy initialize the MetadataClient because metadata service might not be ready
|
||||
func (s *Store) initMetadataClient(mdc MetadataClient) error {
|
||||
ctx := context.TODO()
|
||||
err := mdc.Init(ctx, settingsSpaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range []string{
|
||||
rootFolderLocation,
|
||||
accountsFolderLocation,
|
||||
bundleFolderLocation,
|
||||
valuesFolderLocation,
|
||||
} {
|
||||
err = mdc.MakeDirIfNotExist(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range s.cfg.Bundles {
|
||||
b, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = mdc.SimpleUpload(ctx, bundlePath(p.Id), b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range defaults.DefaultRoleAssignments(s.cfg) {
|
||||
accountUUID := p.AccountUuid
|
||||
roleID := p.RoleId
|
||||
err = mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assIDs, err := mdc.ReadDir(ctx, accountPath(accountUUID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adminUserID := accountUUID == s.cfg.AdminUserID
|
||||
if len(assIDs) > 0 && !adminUserID {
|
||||
// There is already a role assignment for this ID, skip to the next
|
||||
continue
|
||||
}
|
||||
// for the adminUserID we need to check if the user has the admin role every time
|
||||
if adminUserID {
|
||||
err = s.userMustHaveAdminRole(accountUUID, assIDs, mdc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ass := &settingsmsg.UserRoleAssignment{
|
||||
Id: uuid.NewString(),
|
||||
AccountUuid: accountUUID,
|
||||
RoleId: roleID,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(ass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = mdc.SimpleUpload(ctx, assignmentPath(accountUUID, ass.Id), b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.mdc = mdc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) userMustHaveAdminRole(accountUUID string, assIDs []string, mdc MetadataClient) error {
|
||||
ctx := context.TODO()
|
||||
var hasAdminRole bool
|
||||
|
||||
// load the assignments from the store and check if the admin role is already assigned
|
||||
for _, assID := range assIDs {
|
||||
b, err := mdc.SimpleDownload(ctx, assignmentPath(accountUUID, assID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
a := &settingsmsg.UserRoleAssignment{}
|
||||
err = json.Unmarshal(b, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.RoleId == defaults.BundleUUIDRoleAdmin {
|
||||
hasAdminRole = true
|
||||
}
|
||||
}
|
||||
|
||||
// delete old role assignment and set admin role
|
||||
if !hasAdminRole {
|
||||
err := mdc.Delete(ctx, accountPath(accountUUID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
// already gone, continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
err = mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ass := &settingsmsg.UserRoleAssignment{
|
||||
Id: uuid.NewString(),
|
||||
AccountUuid: accountUUID,
|
||||
RoleId: defaults.BundleUUIDRoleAdmin,
|
||||
}
|
||||
b, err := json.Marshal(ass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mdc.SimpleUpload(ctx, assignmentPath(accountUUID, ass.Id), b)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
settings.Registry[managerName] = New
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/config/defaults"
|
||||
rdefaults "github.com/qsfera/server/services/settings/pkg/store/defaults"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
)
|
||||
|
||||
const (
|
||||
// account UUIDs
|
||||
accountUUID1 = "c4572da7-6142-4383-8fc6-efde3d463036"
|
||||
//accountUUID2 = "e11f9769-416a-427d-9441-41a0e51391d7"
|
||||
//accountUUID3 = "633ecd77-1980-412a-8721-bf598a330bb4"
|
||||
|
||||
// extension names
|
||||
extension1 = "test-extension-1"
|
||||
extension2 = "test-extension-2"
|
||||
|
||||
// bundle ids
|
||||
bundle1 = "2f06addf-4fd2-49d5-8f71-00fbd3a3ec47"
|
||||
bundle2 = "2d745744-749c-4286-8e92-74a24d8331c5"
|
||||
bundle3 = "d8fd27d1-c00b-4794-a658-416b756a72ff"
|
||||
|
||||
// setting ids
|
||||
setting1 = "c7ebbc8b-d15a-4f2e-9d7d-d6a4cf858d1a"
|
||||
setting2 = "3fd9a3d9-20b7-40d4-9294-b22bb5868c10"
|
||||
setting3 = "24bb9535-3df4-42f1-a622-7c0562bec99f"
|
||||
|
||||
// value ids
|
||||
value1 = "fd3b6221-dc13-4a22-824d-2480495f1cdb"
|
||||
value2 = "2a0bd9b0-ca1d-491a-8c56-d2ddfd68ded8"
|
||||
value3 = "b42702d2-5e4d-4d73-b133-e1f9e285355e"
|
||||
)
|
||||
|
||||
// use "unit" or "integration" do define test type. You need a running КуСфера instance for integration tests
|
||||
var testtype = "unit"
|
||||
|
||||
// MockedMetadataClient mocks the metadataservice inmemory
|
||||
type MockedMetadataClient struct {
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
// NewMDC instantiates a mocked MetadataClient
|
||||
func NewMDC(s *Store) error {
|
||||
var mdc MetadataClient
|
||||
switch testtype {
|
||||
case "unit":
|
||||
mdc = &MockedMetadataClient{data: make(map[string][]byte)}
|
||||
case "integration":
|
||||
mdc = NewMetadataClient(defaults.DefaultConfig().Metadata)
|
||||
}
|
||||
return s.initMetadataClient(mdc)
|
||||
}
|
||||
|
||||
// SimpleDownload returns errtypes.NotFound if not found
|
||||
func (m *MockedMetadataClient) SimpleDownload(_ context.Context, id string) ([]byte, error) {
|
||||
if data, ok := m.data[id]; ok {
|
||||
return data, nil
|
||||
}
|
||||
return nil, errtypes.NotFound("not found")
|
||||
}
|
||||
|
||||
// SimpleUpload can't error
|
||||
func (m *MockedMetadataClient) SimpleUpload(_ context.Context, id string, content []byte) error {
|
||||
m.data[id] = content
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete can't error either
|
||||
func (m *MockedMetadataClient) Delete(_ context.Context, id string) error {
|
||||
for k := range m.data {
|
||||
if strings.HasPrefix(k, id) {
|
||||
delete(m.data, k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadDir returns nil, nil if not found
|
||||
func (m *MockedMetadataClient) ReadDir(_ context.Context, id string) ([]string, error) {
|
||||
var out []string
|
||||
for k := range m.data {
|
||||
if strings.HasPrefix(k, id) {
|
||||
dir := strings.TrimPrefix(k, id+"/")
|
||||
// filter subfolders the lame way
|
||||
s := strings.Trim(strings.SplitAfter(dir, "/")[0], "/")
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MakeDirIfNotExist does nothing
|
||||
func (*MockedMetadataClient) MakeDirIfNotExist(_ context.Context, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init does nothing
|
||||
func (*MockedMetadataClient) Init(_ context.Context, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IDExists is a helper to check if an id exists
|
||||
func (m *MockedMetadataClient) IDExists(id string) bool {
|
||||
_, ok := m.data[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
// IDHasContent returns true if the value stored under id has the given content (converted to string)
|
||||
func (m *MockedMetadataClient) IDHasContent(id string, content []byte) bool {
|
||||
return string(m.data[id]) == string(content)
|
||||
}
|
||||
|
||||
// TestAdminUserIDInit test the happy path during initialization
|
||||
func TestAdminUserIDInit(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
s := &Store{
|
||||
cfg: defaults.DefaultConfig(),
|
||||
}
|
||||
s.cfg.Bundles = rdefaults.GenerateBundlesDefaultRoles()
|
||||
s.cfg.AdminUserID = "admin"
|
||||
|
||||
// the first assignment is always happening during the initialisation of the metadata client
|
||||
err := NewMDC(s)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
assID, err := s.mdc.ReadDir(context.TODO(), accountPath(s.cfg.AdminUserID))
|
||||
Expect(len(assID)).To(Equal(1))
|
||||
ass, err := s.mdc.SimpleDownload(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]))
|
||||
Expect(ass).ToNot(BeNil())
|
||||
|
||||
assignment := &settingsmsg.UserRoleAssignment{}
|
||||
err = json.Unmarshal(ass, assignment)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(assignment.RoleId).To(Equal(rdefaults.BundleUUIDRoleAdmin))
|
||||
}
|
||||
|
||||
// TestAdminUserIDUpdate test the update on following initialisations
|
||||
func TestAdminUserIDUpdate(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
s := &Store{
|
||||
cfg: defaults.DefaultConfig(),
|
||||
}
|
||||
s.cfg.Bundles = rdefaults.GenerateBundlesDefaultRoles()
|
||||
s.cfg.AdminUserID = "admin"
|
||||
|
||||
// the first assignment is always happening during the initialisation of the metadata client
|
||||
err := NewMDC(s)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// read assignment
|
||||
assID, err := s.mdc.ReadDir(context.TODO(), accountPath(s.cfg.AdminUserID))
|
||||
Expect(len(assID)).To(Equal(1))
|
||||
|
||||
// set assignment to user role
|
||||
userRoleAssignment := &settingsmsg.UserRoleAssignment{
|
||||
AccountUuid: s.cfg.AdminUserID,
|
||||
RoleId: rdefaults.BundleUUIDRoleUser,
|
||||
}
|
||||
b, err := json.Marshal(userRoleAssignment)
|
||||
err = s.mdc.Delete(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]))
|
||||
Expect(err).To(BeNil())
|
||||
err = s.mdc.SimpleUpload(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]), b)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// this happens on every Read / Write on the store
|
||||
// the actual init is only done if the metadata client has not been initialized before
|
||||
// this normally needs a restart of the service
|
||||
err = s.initMetadataClient(s.mdc)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// read assignment id, changes every time the assignment is written
|
||||
assID, err = s.mdc.ReadDir(context.TODO(), accountPath(s.cfg.AdminUserID))
|
||||
Expect(len(assID)).To(Equal(1))
|
||||
|
||||
// check if the assignment is the admin role again
|
||||
ass, err := s.mdc.SimpleDownload(context.TODO(), assignmentPath(s.cfg.AdminUserID, assID[0]))
|
||||
Expect(ass).ToNot(BeNil())
|
||||
assignment := &settingsmsg.UserRoleAssignment{}
|
||||
err = json.Unmarshal(ass, assignment)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(assignment.RoleId).To(Equal(rdefaults.BundleUUIDRoleAdmin))
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Package store implements the go-micro store interface
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/qsfera/server/services/settings/pkg/settings"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
)
|
||||
|
||||
// ListValues reads all values that match the given bundleId and accountUUID.
|
||||
// If the bundleId is empty, it's ignored for filtering.
|
||||
// If the accountUUID is empty, only values with empty accountUUID are returned.
|
||||
// If the accountUUID is not empty, values with an empty or with a matching accountUUID are returned.
|
||||
func (s *Store) ListValues(bundleID, accountUUID string) ([]*settingsmsg.Value, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
vIDs, err := s.mdc.ReadDir(ctx, valuesFolderLocation)
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return make([]*settingsmsg.Value, 0), nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: refine logic not to spam metadata service
|
||||
var values []*settingsmsg.Value
|
||||
for _, vid := range vIDs {
|
||||
b, err := s.mdc.SimpleDownload(ctx, valuePath(vid))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
continue
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := &settingsmsg.Value{}
|
||||
err = json.Unmarshal(b, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bundleID != "" && v.BundleId != bundleID {
|
||||
continue
|
||||
}
|
||||
|
||||
if v.AccountUuid == "" {
|
||||
values = append(values, v)
|
||||
continue
|
||||
}
|
||||
|
||||
if v.AccountUuid == accountUUID {
|
||||
values = append(values, v)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// ReadValue tries to find a value by the given valueId within the dataPath
|
||||
func (s *Store) ReadValue(valueID string) (*settingsmsg.Value, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
b, err := s.mdc.SimpleDownload(ctx, valuePath(valueID))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
return nil, fmt.Errorf("valueID '%s' %w", valueID, settings.ErrNotFound)
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
val := &settingsmsg.Value{}
|
||||
return val, json.Unmarshal(b, val)
|
||||
}
|
||||
|
||||
// ReadValueByUniqueIdentifiers tries to find a value given a set of unique identifiers
|
||||
func (s *Store) ReadValueByUniqueIdentifiers(accountUUID, settingID string) (*settingsmsg.Value, error) {
|
||||
if settingID == "" {
|
||||
return nil, fmt.Errorf("settingID can not be empty %w", settings.ErrNotFound)
|
||||
}
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
vIDs, err := s.mdc.ReadDir(ctx, valuesFolderLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, vid := range vIDs {
|
||||
b, err := s.mdc.SimpleDownload(ctx, valuePath(vid))
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// continue
|
||||
case errtypes.NotFound:
|
||||
continue
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := &settingsmsg.Value{}
|
||||
err = json.Unmarshal(b, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v.AccountUuid == accountUUID && v.SettingId == settingID {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return nil, settings.ErrNotFound
|
||||
}
|
||||
|
||||
// WriteValue writes the given value into a file within the dataPath
|
||||
func (s *Store) WriteValue(value *settingsmsg.Value) (*settingsmsg.Value, error) {
|
||||
s.Init()
|
||||
ctx := context.TODO()
|
||||
|
||||
if value.Id == "" {
|
||||
value.Id = uuid.NewString()
|
||||
}
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return value, s.mdc.SimpleUpload(ctx, valuePath(value.Id), b)
|
||||
}
|
||||
|
||||
func valuePath(id string) string {
|
||||
return fmt.Sprintf("%s/%s", valuesFolderLocation, id)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var valueScenarios = []struct {
|
||||
name string
|
||||
value *settingsmsg.Value
|
||||
}{
|
||||
{
|
||||
name: "generic-test-with-system-resource",
|
||||
value: &settingsmsg.Value{
|
||||
Id: value1,
|
||||
BundleId: bundle1,
|
||||
SettingId: setting1,
|
||||
AccountUuid: accountUUID1,
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_SYSTEM,
|
||||
},
|
||||
Value: &settingsmsg.Value_StringValue{
|
||||
StringValue: "lalala",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "generic-test-with-file-resource",
|
||||
value: &settingsmsg.Value{
|
||||
Id: value2,
|
||||
BundleId: bundle2,
|
||||
SettingId: setting2,
|
||||
AccountUuid: accountUUID1,
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_FILE,
|
||||
Id: "adfba82d-919a-41c3-9cd1-5a3f83b2bf76",
|
||||
},
|
||||
Value: &settingsmsg.Value_StringValue{
|
||||
StringValue: "tralala",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "value without accountUUID",
|
||||
value: &settingsmsg.Value{
|
||||
Id: value3,
|
||||
BundleId: bundle3,
|
||||
SettingId: setting2,
|
||||
AccountUuid: "",
|
||||
Resource: &settingsmsg.Resource{
|
||||
Type: settingsmsg.Resource_TYPE_FILE,
|
||||
Id: "adfba82d-919a-41c3-9cd1-5a3f83b2bf76",
|
||||
},
|
||||
Value: &settingsmsg.Value_StringValue{
|
||||
StringValue: "tralala",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestValues(t *testing.T) {
|
||||
for i := range valueScenarios {
|
||||
index := i
|
||||
t.Run(valueScenarios[index].name, func(t *testing.T) {
|
||||
s := initStore()
|
||||
setupRoles(s)
|
||||
value := valueScenarios[index].value
|
||||
v, err := s.WriteValue(value)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, value, v)
|
||||
|
||||
v, err = s.ReadValue(value.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, value, v)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListValues(t *testing.T) {
|
||||
s := initStore()
|
||||
setupRoles(s)
|
||||
for _, v := range valueScenarios {
|
||||
_, err := s.WriteValue(v.value)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// empty accountid returns only values with empty accountud
|
||||
vs, err := s.ListValues("", "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, vs, 1)
|
||||
|
||||
// filled accountid returns matching and empty accountUUID values
|
||||
vs, err = s.ListValues("", accountUUID1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, vs, 3)
|
||||
|
||||
// filled bundleid only returns matching values
|
||||
vs, err = s.ListValues(bundle3, accountUUID1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, vs, 1)
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user