refactor settings

This commit is contained in:
Christian Richter
2022-04-13 17:04:37 +02:00
parent 92d76e00ab
commit 1a1a4d6a3c
114 changed files with 80 additions and 8747 deletions
+2
View File
@@ -0,0 +1,2 @@
*
!bin/
+17
View File
@@ -0,0 +1,17 @@
{
"env": {
"browser": true,
"es6": true,
"amd": true
},
"extends": [
"standard",
"plugin:vue/essential"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
}
}
+17
View File
@@ -0,0 +1,17 @@
# yarn2 with Zero-Installs: https://yarnpkg.com/features/zero-installs
#.yarn/*
#!.yarn/cache
#!.yarn/patches
#!.yarn/plugins
#!.yarn/releases
#!.yarn/sdks
#!.yarn/versions
# yarn2 not using Zero-Installs: https://yarnpkg.com/features/zero-installs
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
+38
View File
@@ -0,0 +1,38 @@
issues:
exclude-rules:
- path: pkg/proto/v0/settings.pb.go
text: "SA1019:"
linters:
- staticcheck
- path: pkg/store/filesystem/io.go
text: "SA1019:"
linters:
- staticcheck
# Exclude scopelint for tests files because of https://github.com/kyoh86/scopelint/issues/4
- path: _test\.go
linters:
- scopelint
linters:
enable:
- bodyclose
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
- depguard
- revive
- goimports
- unconvert
- scopelint
- maligned
- misspell
# - gocritic
- prealloc
#- gosec
File diff suppressed because one or more lines are too long
+66
View File
@@ -0,0 +1,66 @@
SHELL := bash
NAME := settings
include ../../.make/recursion.mk
.PHONY: test-acceptance-webui
test-acceptance-webui:
./ui/tests/run-acceptance-test.sh $(FEATURE_PATH)
############ tooling ############
ifneq (, $(shell which go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include ../../.bingo/Variables.mk
endif
############ go tooling ############
include ../../.make/go.mk
############ release ############
include ../../.make/release.mk
############ docs generate ############
include ../../.make/docs.mk
############ l10n ############
include ../../.make/l10n.mk
.PHONY: docs-generate
docs-generate: config-docs-generate \
grpc-docs-generate
############ generate ############
include ../../.make/generate.mk
.PHONY: ci-go-generate
ci-go-generate: protobuf # CI runs ci-node-generate automatically before this target
.PHONY: ci-node-generate
ci-node-generate: yarn-build
.PHONY: yarn-build
yarn-build: node_modules
yarn lint
yarn test
yarn build
.PHONY: node_modules
node_modules:
@yarn install --immutable 2>&1 >/dev/null
############ protobuf ############
include ../../.make/protobuf.mk
.PHONY: protobuf
protobuf: buf-generate
############ licenses ############
.PHONY: ci-node-check-licenses
ci-node-check-licenses: node_modules
yarn licenses:check
.PHONY: ci-node-save-licenses
ci-node-save-licenses: node_modules
yarn licenses:csv
yarn licenses:save
+25
View File
@@ -0,0 +1,25 @@
module.exports = function (api) {
api.cache(true)
const presets = [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: '3'
}
]
]
const plugins = [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-transform-runtime',
'@babel/plugin-proposal-export-default-from'
]
return {
presets,
plugins
}
}
+14
View File
@@ -0,0 +1,14 @@
package main
import (
"os"
"github.com/owncloud/ocis/extensions/settings/pkg/command"
"github.com/owncloud/ocis/extensions/settings/pkg/config/defaults"
)
func main() {
if err := command.Execute(defaults.DefaultConfig()); err != nil {
os.Exit(1)
}
}
@@ -0,0 +1,19 @@
FROM amd64/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS Settings" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9190 9194
ENTRYPOINT ["/usr/bin/ocis-settings"]
CMD ["server"]
COPY bin/ocis-settings /usr/bin/ocis-settings
@@ -0,0 +1,19 @@
FROM arm32v6/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS Settings" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9190 9194
ENTRYPOINT ["/usr/bin/ocis-settings"]
CMD ["server"]
COPY bin/ocis-settings /usr/bin/ocis-settings
@@ -0,0 +1,19 @@
FROM arm64v8/alpine:latest
RUN apk update && \
apk upgrade && \
apk add ca-certificates mailcap && \
rm -rf /var/cache/apk/* && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="ownCloud GmbH <devops@owncloud.com>" \
org.label-schema.name="oCIS Settings" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9190 9194
ENTRYPOINT ["/usr/bin/ocis-settings"]
CMD ["server"]
COPY bin/ocis-settings /usr/bin/ocis-settings
+22
View File
@@ -0,0 +1,22 @@
image: owncloud/ocis-settings:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: owncloud/ocis-settings:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: owncloud/ocis-settings:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
variant: v8
os: linux
- image: owncloud/ocis-settings:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
variant: v6
os: linux
+9
View File
@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[owncloud.ocis-settings]
file_filter = locale/<lang>/LC_MESSAGES/app.po
minimum_perc = 0
source_file = template.pot
source_lang = en
type = PO
@@ -0,0 +1 @@
{"de":{"{{ bundle.displayName }}":"{{ bundle.displayName }}","Account":"Konto","Cancel":"Abbrechen","Hello":"Hallo","Language":"Sprache","Loading personal settings":"Lade persönliche Einstellungen","Loading personal settings...":"Lade persönliche Einstellungen...","msg":"msg","No settings available":"Keine Einstellungen verfügbar","Save":"Speichern","Settings":"Einstellungen","Settings type not implemented: %{type}":"Einstellungs-Typ nicht verfügbar: %{type}"},"de_DE":{"{{ bundle.displayName }}":"{{ bundle.displayName }}","Account":"Benutzerkonto","Cancel":"Abbrechen","Hello":"Hallo","Language":"Sprache","Loading personal settings":"Lade persönliche Einstellungen","Loading personal settings...":"Lade persönliche Einstellungen...","msg":"msg","No settings available":"Keine Einstellungen verfügbar","Save":"Speichern","Settings":"Einstellungen","Settings type not implemented: %{type}":"Einstellungs-Typ nicht verfügbar: %{type}"},"el":{"{{ bundle.displayName }}":"{{ bundle.displayName }}","Account":"Λογαριασμός","Cancel":"Aκύρωση","Hello":"Γεια σας","Language":"Γλώσσα","Loading personal settings":"Γίνεται φόρτωση προσωπικών ρυθμίσεων","Loading personal settings...":"Γίνεται φόρτωση προσωπικών ρυθμίσεων...","msg":"μνμ","No settings available":"Δεν υπάρχουν διαθέσιμες ρυθμίσεις","Save":"Αποθήκευση","Settings":"Ρυθμίσεις","Settings type not implemented: %{type}":"Ο τύπος ρυθμίσεων δεν εφαρμόστηκε: %{type}"},"es":{"{{ bundle.displayName }}":"{{bundle.displayName}}","Account":"Cuenta","Cancel":"Cancelar","Hello":"Hola","Language":"Idioma","Loading personal settings":"Cargando configuraciones personales","Loading personal settings...":"Cargando configuración personal ...","msg":"msg","No settings available":"No hay configuraciones disponibles","Save":"Guardar","Settings":"Configuración","Settings type not implemented: %{type}":"Tipo de configuración no implementado: %{type}"},"pl":{"{{ bundle.displayName }}":"{{ bundle.displayName }}","Account":"Konto","Cancel":"Anuluj","Hello":"Witaj","Language":"Język","Loading personal settings":"Ładowanie ustawień osobistych","Loading personal settings...":"Ładowanie ustawień osobistych...","msg":"wiad","No settings available":"Brak dostępnych ustawień","Save":"Zapisz","Settings":"Ustawienia","Settings type not implemented: %{type}":"Typ ustawień nie jest wdrożony: %{type}"},"pt_BR":{"{{ bundle.displayName }}":"{{ bundle.displayName }}","Account":"Conta","Cancel":"Cancelar","Hello":"Olá","Language":"Idioma","Loading personal settings":"Carregando configurações pessoais ","Loading personal settings...":"Carregando configurações pessoais... ","msg":"msg","No settings available":"Nenhuma configuração disponível ","Save":"Salvar","Settings":"Configurações","Settings type not implemented: %{type}":"Tipo de configuração não implementado: %{type}"},"ru":{"{{ bundle.displayName }}":"{{ bundle.displayName }}","Account":"Уч.запись","Cancel":"Отмена","Hello":"Здравствуйте","Language":"Язык","Loading personal settings":"Загрузка личных настроек","Loading personal settings...":"Загрузка личных настроек…","msg":"сообщ","No settings available":"Нет доступных настроек","Save":"Сохранить","Settings":"Настройки","Settings type not implemented: %{type}":"Тип настроек не реализован: %{type}"},"sq":{"{{ bundle.displayName }}":"{{ bundle.displayName }}","Account":"Llogari","Cancel":"Anuloje","Hello":"Tungjatjeta","Language":"Gjuhë","Loading personal settings":"Ngarkim rregullimesh personale","Loading personal settings...":"Po ngarkohen rregullime personale…","msg":"msz","No settings available":"Ska rregullime","Save":"Ruaje","Settings":"Rregullime","Settings type not implemented: %{type}":"Lloj rregullimi i pasendërtuar: %{type}"},"th_TH":{"{{ bundle.displayName }}":"{{ bundle.displayName }}","Account":"บัญชี","Cancel":"ยกเลิก","Hello":"สวัสดี","Language":"ภาษา","Loading personal settings":"กำลังโหลดการตั้งค่าส่วนบุคคล","Loading personal settings...":"กำลังโหลดการตั้งค่าส่วนบุคคล...","msg":"msg","No settings available":"ไม่มีการตั้งค่า","Save":"บันทึก","Settings":"ตั้งค่า","Settings type not implemented: %{type}":"ประเภทการตั้งค่าที่ยังไม่ถูกดำเนินการ: %{type}"}}
+9
View File
@@ -0,0 +1,9 @@
const path = require('path')
const TEST_INFRA_DIRECTORY = process.env.TEST_INFRA_DIRECTORY
const config = require(path.join(TEST_INFRA_DIRECTORY, 'nightwatch.conf.js'))
config.page_objects_path = [TEST_INFRA_DIRECTORY + '/pageObjects', 'ui/tests/acceptance/pageobjects']
config.custom_commands_path = TEST_INFRA_DIRECTORY + '/customCommands'
module.exports = config
+89
View File
@@ -0,0 +1,89 @@
{
"name": "ocis-settings",
"version": "0.0.0",
"private": true,
"description": "",
"homepage": "https://github.com/owncloud/ocis-settings#readme",
"bugs": {
"url": "https://github.com/owncloud/ocis/issues",
"email": "support@owncloud.com"
},
"repository": "https://github.com/owncloud/ocis-settings.git",
"license": "Apache-2.0",
"author": "ownCloud GmbH <devops@owncloud.com>",
"scripts": {
"acceptance-tests": "cucumber-js --retry 1 --require-module @babel/register --require-module @babel/polyfill --require ${TEST_INFRA_DIRECTORY}/setup.js --require ui/tests/acceptance/stepDefinitions --require ${TEST_INFRA_DIRECTORY}/stepDefinitions --format @cucumber/pretty-formatter -t \"${TEST_TAGS:-not @skip and not @skipOnOC10}\"",
"build": "rollup -c",
"generate-api": "node node_modules/swagger-vue-generator/bin/generate-api.js --package-version v0 --source pkg/proto/v0/settings.swagger.json --moduleName settings --destination ui/client/settings/index.js",
"lint": "eslint ui/**/*.vue ui/**/*.js --color --global requirejs --global require",
"test": "echo 'Not implemented'",
"watch": "rollup -c -w",
"licenses:check": "license-checker-rseidelsohn --summary --relativeLicensePath --onlyAllow 'Python-2.0;Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Unicode-TOU;Unlicense;WTFPL' --excludePackages 'ocis-settings@0.0.0'",
"licenses:csv": "license-checker-rseidelsohn --relativeLicensePath --csv --out ../third-party-licenses/node/settings/third-party-licenses.csv",
"licenses:save": "license-checker-rseidelsohn --relativeLicensePath --out /dev/null --files ../third-party-licenses/node/settings/third-party-licenses"
},
"browserslist": [
"> 1%",
"not dead"
],
"dependencies": {
"axios": "^0.21.4",
"core-js": "^3.21.0",
"debounce": "^1.2.1",
"vuex": "^3.5.1"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-export-default-from": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.15.6",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/polyfill": "^7.10.1",
"@babel/preset-env": "^7.16.11",
"@babel/register": "^7.17.7",
"@cucumber/cucumber": "^7.3.2",
"@cucumber/pretty-formatter": "^1.0.0-alpha.2",
"@erquhart/rollup-plugin-node-builtins": "^2.1.5",
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-json": "^4.0.1",
"@rollup/plugin-replace": "^2.4.2",
"archiver": "^5.3.0",
"chromedriver": "^93.0.1",
"cross-env": "^7.0.3",
"easygettext": "^2.7.0",
"eslint": "7.32.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.24.1",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^7.8.0",
"fs-extra": "^10.0.0",
"join-path": "^1.1.1",
"ldapjs": "^2.3.2",
"license-checker-rseidelsohn": "^3.1.0",
"lodash-es": "^4.17.21",
"nightwatch": "1.7.11",
"nightwatch-api": "3.0.2",
"nightwatch-vrt": "^0.2.10",
"node-fetch": "^2.6.1",
"qs": "^6.10.3",
"rimraf": "^3.0.0",
"rollup": "^2.70.1",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-eslint": "^7.0.0",
"rollup-plugin-filesize": "^9.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-vue": "^5.1.9",
"swagger-vue-generator": "^1.0.6",
"url-search-params-polyfill": "^8.1.1",
"vue-template-compiler": "^2.6.11",
"xml-js": "^1.6.11"
},
"peerDependencies": {
"owncloud-design-system": "^12.2.2"
}
}
+50
View File
@@ -0,0 +1,50 @@
package assets
import (
"net/http"
"github.com/owncloud/ocis/extensions/settings"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/assetsfs"
"github.com/owncloud/ocis/ocis-pkg/log"
)
// New returns a new http filesystem to serve assets.
func New(opts ...Option) http.FileSystem {
options := newOptions(opts...)
return assetsfs.New(settings.Assets, options.Config.Asset.Path, options.Logger)
}
// 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
}
// 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
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
+52
View File
@@ -0,0 +1,52 @@
package command
import (
"fmt"
"net/http"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/extensions/settings/pkg/config/parser"
"github.com/owncloud/ocis/extensions/settings/pkg/logging"
"github.com/urfave/cli/v2"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "health",
Usage: "Check health status",
Before: func(c *cli.Context) error {
return parser.ParseConfig(cfg)
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
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
},
}
}
+64
View File
@@ -0,0 +1,64 @@
package command
import (
"context"
"os"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/clihelper"
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
"github.com/thejerf/suture/v4"
"github.com/urfave/cli/v2"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) cli.Commands {
return []*cli.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 ocis-settings command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "ocis-settings",
Usage: "Provide settings and permissions for oCIS",
Commands: GetCommands(cfg),
})
cli.HelpFlag = &cli.BoolFlag{
Name: "help,h",
Usage: "Show the help",
}
return app.Run(os.Args)
}
// SutureService allows for the settings command to be embedded and supervised by a suture supervisor tree.
type SutureService struct {
cfg *config.Config
}
// NewSutureService creates a new settings.SutureService
func NewSutureService(cfg *ociscfg.Config) suture.Service {
cfg.Settings.Commons = cfg.Commons
return SutureService{
cfg: cfg.Settings,
}
}
func (s SutureService) Serve(ctx context.Context) error {
s.cfg.Context = ctx
if err := Execute(s.cfg); err != nil {
return err
}
return nil
}
+83
View File
@@ -0,0 +1,83 @@
package command
import (
"context"
"fmt"
"github.com/oklog/run"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/extensions/settings/pkg/config/parser"
"github.com/owncloud/ocis/extensions/settings/pkg/logging"
"github.com/owncloud/ocis/extensions/settings/pkg/metrics"
"github.com/owncloud/ocis/extensions/settings/pkg/server/debug"
"github.com/owncloud/ocis/extensions/settings/pkg/server/grpc"
"github.com/owncloud/ocis/extensions/settings/pkg/server/http"
"github.com/owncloud/ocis/extensions/settings/pkg/tracing"
"github.com/owncloud/ocis/ocis-pkg/version"
"github.com/urfave/cli/v2"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "server",
Usage: fmt.Sprintf("start %s extension without runtime (unsupervised mode)", cfg.Service.Name),
Category: "server",
Before: func(c *cli.Context) error {
return parser.ParseConfig(cfg)
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
err := tracing.Configure(cfg)
if err != nil {
return err
}
servers := run.Group{}
ctx, cancel := func() (context.Context, context.CancelFunc) {
if cfg.Context == nil {
return context.WithCancel(context.Background())
}
return context.WithCancel(cfg.Context)
}()
defer cancel()
mtrcs := metrics.New()
mtrcs.BuildInfo.WithLabelValues(version.String).Set(1)
// prepare an HTTP server and add it to the group run.
httpServer := http.Server(
http.Name(cfg.Service.Name),
http.Logger(logger),
http.Context(ctx),
http.Config(cfg),
http.Metrics(mtrcs),
)
servers.Add(httpServer.Run, func(_ error) {
logger.Info().Str("server", "http").Msg("Shutting down server")
cancel()
})
// 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))
servers.Add(grpcServer.Run, func(_ error) {
logger.Info().Str("server", "grpc").Msg("Shutting down server")
cancel()
})
// 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
}
servers.Add(debugServer.ListenAndServe, func(_ error) {
_ = debugServer.Shutdown(ctx)
cancel()
})
return servers.Run()
},
}
}
@@ -0,0 +1,50 @@
package command
import (
"fmt"
"os"
"github.com/owncloud/ocis/ocis-pkg/registry"
"github.com/owncloud/ocis/ocis-pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/urfave/cli/v2"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "version",
Usage: "print the version of this binary and the running extension instances",
Category: "info",
Action: func(c *cli.Context) error {
fmt.Println("Version: " + version.String)
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 := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}
+45
View File
@@ -0,0 +1,45 @@
package config
import (
"context"
"github.com/owncloud/ocis/ocis-pkg/shared"
)
// Config combines all available configuration parts.
type Config struct {
*shared.Commons `yaml:"-"`
Service Service `yaml:"-"`
Tracing *Tracing `yaml:"tracing"`
Log *Log `yaml:"log"`
Debug Debug `yaml:"debug"`
HTTP HTTP `yaml:"http"`
GRPC GRPC `yaml:"grpc"`
StoreType string `yaml:"store_type" env:"SETTINGS_STORE_TYPE"`
DataPath string `yaml:"data_path" env:"SETTINGS_DATA_PATH"`
Metadata Metadata `yaml:"metadata_config"`
Asset Asset `yaml:"asset"`
TokenManager TokenManager `yaml:"token_manager"`
Context context.Context `yaml:"-"`
}
// Asset defines the available asset configuration.
type Asset struct {
Path string `yaml:"path" env:"SETTINGS_ASSET_PATH"`
}
// Metadata configures the metadata store to use
type Metadata struct {
GatewayAddress string `yaml:"gateway_addr" env:"STORAGE_GATEWAY_GRPC_ADDR"`
StorageAddress string `yaml:"storage_addr" env:"STORAGE_GRPC_ADDR"`
ServiceUserID string `yaml:"service_user_id" env:"METADATA_SERVICE_USER_UUID"`
ServiceUserIDP string `yaml:"service_user_idp" env:"OCIS_URL;METADATA_SERVICE_USER_IDP"`
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `yaml:"addr" env:"SETTINGS_DEBUG_ADDR"`
Token string `yaml:"token" env:"SETTINGS_DEBUG_TOKEN"`
Pprof bool `yaml:"pprof" env:"SETTINGS_DEBUG_PPROF"`
Zpages bool `yaml:"zpages" env:"SETTINGS_DEBUG_ZPAGES"`
}
@@ -0,0 +1,97 @@
package defaults
import (
"path"
"strings"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/config/defaults"
)
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: "com.owncloud.web",
Root: "/",
CacheTTL: 604800, // 7 days
CORS: config.CORS{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With"},
AllowCredentials: true,
},
},
GRPC: config.GRPC{
Addr: "127.0.0.1:9191",
Namespace: "com.owncloud.api",
},
StoreType: "metadata", // use metadata or filesystem
DataPath: path.Join(defaults.BaseDataPath(), "settings"),
Asset: config.Asset{
Path: "",
},
TokenManager: config.TokenManager{
JWTSecret: "Pive-Fumkiu4",
},
Metadata: config.Metadata{
GatewayAddress: "127.0.0.1:9142",
StorageAddress: "127.0.0.1:9215",
ServiceUserID: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad",
ServiceUserIDP: "https://localhost:9200",
MachineAuthAPIKey: "change-me-please",
},
}
}
func EnsureDefaults(cfg *config.Config) {
// provide with defaults for shared logging, since we need a valid destination address for BindEnv.
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
cfg.Log = &config.Log{
Level: cfg.Commons.Log.Level,
Pretty: cfg.Commons.Log.Pretty,
Color: cfg.Commons.Log.Color,
File: cfg.Commons.Log.File,
}
} else if cfg.Log == nil {
cfg.Log = &config.Log{}
}
// provide with defaults for shared tracing, since we need a valid destination address for BindEnv.
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
cfg.Tracing = &config.Tracing{
Enabled: cfg.Commons.Tracing.Enabled,
Type: cfg.Commons.Tracing.Type,
Endpoint: cfg.Commons.Tracing.Endpoint,
Collector: cfg.Commons.Tracing.Collector,
}
} else if cfg.Tracing == nil {
cfg.Tracing = &config.Tracing{}
}
}
func Sanitize(cfg *config.Config) {
// sanitize config
if cfg.HTTP.Root != "/" {
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
}
}
+7
View File
@@ -0,0 +1,7 @@
package config
// GRPC defines the available grpc configuration.
type GRPC struct {
Addr string `yaml:"addr" env:"SETTINGS_GRPC_ADDR"`
Namespace string `yaml:"-"`
}
+18
View File
@@ -0,0 +1,18 @@
package config
// HTTP defines the available http configuration.
type HTTP struct {
Addr string `yaml:"addr" env:"SETTINGS_HTTP_ADDR"`
Namespace string `yaml:"-"`
Root string `yaml:"root" env:"SETTINGS_HTTP_ROOT"`
CacheTTL int `yaml:"cache_ttl" env:"SETTINGS_CACHE_TTL"`
CORS CORS `yaml:"cors"`
}
// CORS defines the available cors configuration.
type CORS struct {
AllowedOrigins []string `yaml:"allowed_origins"`
AllowedMethods []string `yaml:"allowed_methods"`
AllowedHeaders []string `yaml:"allowed_headers"`
AllowCredentials bool `yaml:"allowed_credentials"`
}
+9
View File
@@ -0,0 +1,9 @@
package config
// Log defines the available log configuration.
type Log struct {
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;SETTINGS_LOG_LEVEL"`
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;SETTINGS_LOG_PRETTY"`
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;SETTINGS_LOG_COLOR"`
File string `mapstructure:"file" env:"OCIS_LOG_FILE;SETTINGS_LOG_FILE"`
}
@@ -0,0 +1,32 @@
package parser
import (
"errors"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/extensions/settings/pkg/config/defaults"
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
)
// ParseConfig loads accounts configuration from known paths.
func ParseConfig(cfg *config.Config) error {
_, err := ociscfg.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)
return nil
}
+6
View File
@@ -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:"OCIS_JWT_SECRET;SETTINGS_JWT_SECRET"`
}
@@ -0,0 +1,6 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
}
@@ -0,0 +1,9 @@
package config
// Tracing defines the available tracing configuration.
type Tracing struct {
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;SETTINGS_TRACING_ENABLED"`
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;SETTINGS_TRACING_TYPE"`
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;SETTINGS_TRACING_ENDPOINT"`
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;SETTINGS_TRACING_COLLECTOR"`
}
@@ -0,0 +1,17 @@
package logging
import (
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
)
// LoggerFromConfig initializes a service-specific logger instance.
func Configure(name string, cfg *config.Log) log.Logger {
return log.NewLogger(
log.Name(name),
log.Level(cfg.Level),
log.Pretty(cfg.Pretty),
log.Color(cfg.Color),
log.File(cfg.File),
)
}
@@ -0,0 +1,45 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
// Namespace defines the namespace for the defines metrics.
Namespace = "ocis"
// 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/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
)
// 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,63 @@
package debug
import (
"io"
"net/http"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/service/debug"
"github.com/owncloud/ocis/ocis-pkg/version"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.String),
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(health(options.Config)),
debug.Ready(ready(options.Config)),
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
}
// health implements the health check.
func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does we want to know.
if err != nil {
panic(err)
}
}
}
// ready implements the ready check.
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does we want to know.
if err != nil {
panic(err)
}
}
}
@@ -0,0 +1,76 @@
package grpc
import (
"context"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/extensions/settings/pkg/metrics"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/urfave/cli/v2"
)
// 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
Flags []cli.Flag
}
// 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(val []cli.Flag) Option {
return func(o *Options) {
o.Flags = append(o.Flags, val...)
}
}
@@ -0,0 +1,78 @@
package grpc
import (
"context"
permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
svc "github.com/owncloud/ocis/extensions/settings/pkg/service/v0"
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/ocis-pkg/version"
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/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 := grpc.NewService(
grpc.Logger(options.Logger),
grpc.Name(options.Name),
grpc.Version(version.String),
grpc.Address(options.Config.GRPC.Addr),
grpc.Namespace(options.Config.GRPC.Namespace),
grpc.Context(options.Context),
grpc.Flags(options.Flags...),
)
handle := svc.NewService(options.Config, options.Logger)
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"},
Body: "*",
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,76 @@
package http
import (
"context"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/extensions/settings/pkg/metrics"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/urfave/cli/v2"
)
// 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
Flags []cli.Flag
}
// 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(val []cli.Flag) Option {
return func(o *Options) {
o.Flags = append(o.Flags, val...)
}
}
@@ -0,0 +1,85 @@
package http
import (
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/owncloud/ocis/extensions/settings/pkg/assets"
svc "github.com/owncloud/ocis/extensions/settings/pkg/service/v0"
"github.com/owncloud/ocis/ocis-pkg/account"
"github.com/owncloud/ocis/ocis-pkg/cors"
"github.com/owncloud/ocis/ocis-pkg/middleware"
"github.com/owncloud/ocis/ocis-pkg/service/http"
"github.com/owncloud/ocis/ocis-pkg/version"
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
"go-micro.dev/v4"
)
// Server initializes the http service and server.
func Server(opts ...Option) http.Service {
options := newOptions(opts...)
service := http.NewService(
http.Logger(options.Logger),
http.Name(options.Name),
http.Version(version.String),
http.Address(options.Config.HTTP.Addr),
http.Namespace(options.Config.HTTP.Namespace),
http.Context(options.Context),
http.Flags(options.Flags...),
)
handle := svc.NewService(options.Config, options.Logger)
{
handle = svc.NewInstrument(handle, options.Metrics)
handle = svc.NewLogging(handle, options.Logger)
handle = svc.NewTracing(handle)
}
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.Secure)
mux.Use(middleware.ExtractAccountUUID(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret)),
)
mux.Use(middleware.Version(
options.Name,
version.String,
))
mux.Use(middleware.Logger(
options.Logger,
))
mux.Use(middleware.Static(
options.Config.HTTP.Root,
assets.New(
assets.Logger(options.Logger),
assets.Config(options.Config),
),
options.Config.HTTP.CacheTTL,
))
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)
})
micro.RegisterHandler(service.Server(), mux)
return service
}
@@ -0,0 +1,13 @@
package svc
import (
"github.com/owncloud/ocis/extensions/settings/pkg/metrics"
)
// NewInstrument returns a service that instruments metrics.
func NewInstrument(next Service, metrics *metrics.Metrics) Service {
return Service{
manager: next.manager,
config: next.config,
}
}
@@ -0,0 +1,13 @@
package svc
import (
"github.com/owncloud/ocis/ocis-pkg/log"
)
// NewLogging returns a service that logs messages.
func NewLogging(next Service, logger log.Logger) Service {
return Service{
manager: next.manager,
config: next.config,
}
}
@@ -0,0 +1,39 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
)
// 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/owncloud/ocis/protogen/gen/ocis/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,538 @@
package svc
import (
"context"
"errors"
"fmt"
permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/extensions/settings/pkg/settings"
filestore "github.com/owncloud/ocis/extensions/settings/pkg/store/filesystem"
metastore "github.com/owncloud/ocis/extensions/settings/pkg/store/metadata"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/ocis-pkg/middleware"
"github.com/owncloud/ocis/ocis-pkg/roles"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
"google.golang.org/protobuf/types/known/emptypb"
)
// 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) Service {
service := Service{
id: "ocis-settings",
config: cfg,
logger: logger,
}
switch cfg.StoreType {
default:
fallthrough
case "metadata":
service.manager = metastore.New(cfg)
case "filesystem":
service.manager = filestore.New(cfg)
// TODO: if we want to further support filesystem store it should use default permissions from store/defaults/defaults.go instead using this duplicate
service.RegisterDefaultRoles()
}
return service
}
func (g Service) CheckPermission(ctx context.Context, req *permissions.CheckPermissionRequest) (*permissions.CheckPermissionResponse, error) {
spec := req.SubjectRef.Spec
var accountID string
switch ref := spec.(type) {
case *permissions.SubjectReference_UserId:
accountID = ref.UserId.OpaqueId
case *permissions.SubjectReference_GroupId:
accountID = ref.GroupId.OpaqueId
}
assignments, err := g.manager.ListRoleAssignments(accountID)
if err != nil {
return &permissions.CheckPermissionResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
roleIDs := make([]string, 0, len(assignments))
for _, a := range assignments {
roleIDs = append(roleIDs, a.RoleId)
}
permission, err := g.manager.ReadPermissionByName(req.Permission, roleIDs)
if err != nil {
if !errors.Is(err, settings.ErrPermissionNotFound) {
return &permissions.CheckPermissionResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
if permission == nil {
return &permissions.CheckPermissionResponse{
Status: &rpcv1beta1.Status{
Code: rpcv1beta1.Code_CODE_PERMISSION_DENIED,
},
}, nil
}
return &permissions.CheckPermissionResponse{
Status: status.NewOK(ctx),
}, nil
}
// RegisterDefaultRoles composes default roles and saves them. Skipped if the roles already exist.
func (g Service) RegisterDefaultRoles() {
// FIXME: we're writing default roles per service start (i.e. twice at the moment, for http and grpc server). has to happen only once.
for _, role := range generateBundlesDefaultRoles() {
bundleID := role.Extension + "." + role.Id
// check if the role already exists
bundle, _ := g.manager.ReadBundle(role.Id)
if bundle != nil {
g.logger.Debug().Str("bundleID", bundleID).Msg("bundle already exists. skipping.")
continue
}
// create the role
_, err := g.manager.WriteBundle(role)
if err != nil {
g.logger.Error().Err(err).Str("bundleID", bundleID).Msg("failed to register bundle")
}
g.logger.Debug().Str("bundleID", bundleID).Msg("successfully registered bundle")
}
for _, req := range generatePermissionRequests() {
_, err := g.manager.AddSettingToBundle(req.GetBundleId(), req.GetSetting())
if err != nil {
g.logger.Error().
Err(err).
Str("bundleID", req.GetBundleId()).
Interface("setting", req.GetSetting()).
Msg("failed to register permission")
}
}
for _, req := range defaultRoleAssignments() {
if _, err := g.manager.WriteRoleAssignment(req.AccountUuid, req.RoleId); err != nil {
g.logger.Error().Err(err).Msg("failed to register role assignment")
}
}
}
// 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.Bundle.Resource)
if err := g.checkStaticPermissionsByBundleType(ctx, req.Bundle.Type); err != nil {
return err
}
if validationError := validateSaveBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.WriteBundle(req.Bundle)
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.BundleId)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
filteredBundle := g.getFilteredBundle(g.getRoleIDs(ctx), bundle)
if len(filteredBundle.Settings) == 0 {
err = fmt.Errorf("could not read bundle: %s", req.BundleId)
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.BundleIds)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
roleIDs := g.getRoleIDs(ctx)
// 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.Settings) > 0 {
filteredBundles = append(filteredBundles, filteredBundle)
}
}
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.Id,
}
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.Settings {
settingResource := &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: setting.Id,
}
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.Setting.Resource)
if err := g.checkStaticPermissionsByBundleID(ctx, req.BundleId); err != nil {
return err
}
if validationError := validateAddSettingToBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.AddSettingToBundle(req.BundleId, req.Setting)
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.BundleId); err != nil {
return err
}
if validationError := validateRemoveSettingFromBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
if err := g.manager.RemoveSettingFromBundle(req.BundleId, req.SettingId); 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.Value.AccountUuid)
cleanUpResource(ctx, req.Value.Resource)
// 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)
}
r, err := g.manager.WriteValue(req.Value)
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(ctx 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.Id)
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 {
if validationError := validateGetValueByUniqueIdentifiers(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
v, err := g.manager.ReadValueByUniqueIdentifiers(req.AccountUuid, req.SettingId)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
if v.BundleId != "" {
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.AccountUuid)
if validationError := validateListValues(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.ListValues(req.BundleId, req.AccountUuid)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
var result []*settingsmsg.ValueWithIdentifier
for _, value := range r {
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(c 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.BundleIds)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
// TODO: only allow to list 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.AccountUuid)
if validationError := validateListRoleAssignments(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.ListRoleAssignments(req.AccountUuid)
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 {
if err := g.checkStaticPermissionsByBundleType(ctx, settingsmsg.Bundle_TYPE_ROLE); err != nil {
return err
}
req.AccountUuid = getValidatedAccountUUID(ctx, req.AccountUuid)
if validationError := validateAssignRoleToUser(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.WriteRoleAssignment(req.AccountUuid, req.RoleId)
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 err := g.checkStaticPermissionsByBundleType(ctx, settingsmsg.Bundle_TYPE_ROLE); err != nil {
return err
}
if validationError := validateRemoveRoleFromUser(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
if err := g.manager.RemoveRoleAssignment(req.Id); err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
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.Resource, 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.PermissionId, 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.PermissionId))
}
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.Type == settingsmsg.Resource_TYPE_USER {
resource.Id = getValidatedAccountUUID(ctx, resource.Id)
}
}
// 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 {
var ownRoleIDs []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 []string{}
}
for _, a := range assignments {
ownRoleIDs = append(ownRoleIDs, a.RoleId)
}
return ownRoleIDs
}
g.logger.Info().Msg("failed to get accountID from context")
return []string{}
}
func (g Service) getValueWithIdentifier(value *settingsmsg.Value) (*settingsmsg.ValueWithIdentifier, error) {
bundle, err := g.manager.ReadBundle(value.BundleId)
if err != nil {
return nil, err
}
setting, err := g.manager.ReadSetting(value.SettingId)
if err != nil {
return nil, err
}
return &settingsmsg.ValueWithIdentifier{
Identifier: &settingsmsg.Identifier{
Extension: bundle.Extension,
Bundle: bundle.Name,
Setting: setting.Name,
},
Value: value,
}, nil
}
func (g Service) hasStaticPermission(ctx context.Context, permissionID string) bool {
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
if !ok {
/**
* FIXME: with this we are skipping permission checks on all requests that are coming in without roleIDs in the
* metadata context. This is a huge security impairment, as that's the case not only for grpc requests but also
* for unauthenticated http requests and http requests coming in without hitting the ocis-proxy first.
*/
// 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
return true
}
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.Type)
}
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
}
@@ -0,0 +1,61 @@
package svc
import (
"context"
"testing"
"github.com/owncloud/ocis/ocis-pkg/middleware"
"github.com/stretchr/testify/assert"
"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)
})
}
}
@@ -0,0 +1,591 @@
package svc
import (
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
)
const (
// BundleUUIDRoleMetadata represents the metadata user role
BundleUUIDRoleMetadata = "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad"
// 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"
// BundleUUIDRoleGuest represents the guest role.
BundleUUIDRoleGuest = "38071a68-456a-4553-846a-fa67bf5596cc"
// RoleManagementPermissionID is the hardcoded setting UUID for the role management permission
RoleManagementPermissionID string = "a53e601e-571f-4f86-8fec-d4576ef49c62"
// RoleManagementPermissionName is the hardcoded setting name for the role management permission
RoleManagementPermissionName string = "role-management"
// SettingsManagementPermissionID is the hardcoded setting UUID for the settings management permission
SettingsManagementPermissionID string = "79e13b30-3e22-11eb-bc51-0b9f0bad9a58"
// SettingsManagementPermissionName is the hardcoded setting name for the settings management permission
SettingsManagementPermissionName string = "settings-management"
// SetSpaceQuotaPermissionID is the hardcoded setting UUID for the set space quota permission
SetSpaceQuotaPermissionID string = "4e6f9709-f9e7-44f1-95d4-b762d27b7896"
// SetSpaceQuotaPermissionName is the hardcoded setting name for the set space quota permission
SetSpaceQuotaPermissionName string = "set-space-quota"
// ListAllSpacesPermissionID is the hardcoded setting UUID for the list all spaces permission
ListAllSpacesPermissionID string = "016f6ddd-9501-4a0a-8ebe-64a20ee8ec82"
// ListAllSpacesPermissionName is the hardcoded setting name for the list all spaces permission
ListAllSpacesPermissionName string = "list-all-spaces"
// CreateSpacePermissionID is the hardcoded setting UUID for the create space permission
CreateSpacePermissionID string = "79e13b30-3e22-11eb-bc51-0b9f0bad9a58"
// CreateSpacePermissionName is the hardcoded setting name for the create space permission
CreateSpacePermissionName string = "create-space"
settingUUIDProfileLanguage = "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f"
// AccountManagementPermissionID is the hardcoded setting UUID for the account management permission
AccountManagementPermissionID string = "8e587774-d929-4215-910b-a317b1e80f73"
// AccountManagementPermissionName is the hardcoded setting name for the account management permission
AccountManagementPermissionName string = "account-management"
// GroupManagementPermissionID is the hardcoded setting UUID for the group management permission
GroupManagementPermissionID string = "522adfbe-5908-45b4-b135-41979de73245"
// GroupManagementPermissionName is the hardcoded setting name for the group management permission
GroupManagementPermissionName string = "group-management"
// SelfManagementPermissionID is the hardcoded setting UUID for the self management permission
SelfManagementPermissionID string = "e03070e9-4362-4cc6-a872-1c7cb2eb2b8e"
// SelfManagementPermissionName is the hardcoded setting name for the self management permission
SelfManagementPermissionName string = "self-management"
)
// generateBundlesDefaultRoles bootstraps the default roles.
func generateBundlesDefaultRoles() []*settingsmsg.Bundle {
return []*settingsmsg.Bundle{
generateBundleAdminRole(),
generateBundleSpaceAdminRole(),
generateBundleUserRole(),
generateBundleGuestRole(),
generateBundleProfileRequest(),
}
}
func generateBundleAdminRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleAdmin,
Name: "admin",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "ocis-roles",
DisplayName: "Admin",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{},
}
}
func generateBundleSpaceAdminRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleSpaceAdmin,
Name: "spaceadmin",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "ocis-roles",
DisplayName: "Space Admin",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{},
}
}
func generateBundleUserRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleUser,
Name: "user",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "ocis-roles",
DisplayName: "User",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{},
}
}
func generateBundleGuestRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleGuest,
Name: "guest",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "ocis-roles",
DisplayName: "Guest",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{},
}
}
var languageSetting = settingsmsg.Setting_SingleChoiceValue{
SingleChoiceValue: &settingsmsg.SingleChoiceList{
Options: []*settingsmsg.ListOption{
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "cs",
},
},
DisplayValue: "Czech",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "de",
},
},
DisplayValue: "Deutsch",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "en",
},
},
DisplayValue: "English",
Default: true,
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "es",
},
},
DisplayValue: "Español",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "fr",
},
},
DisplayValue: "Français",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "gl",
},
},
DisplayValue: "Galego",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "it",
},
},
DisplayValue: "Italiano",
},
},
},
}
func generateBundleProfileRequest() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: "2a506de7-99bd-4f0d-994e-c38e72c28fd9",
Name: "profile",
Extension: "ocis-accounts",
Type: settingsmsg.Bundle_TYPE_DEFAULT,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
DisplayName: "Profile",
Settings: []*settingsmsg.Setting{
{
Id: settingUUIDProfileLanguage,
Name: "language",
DisplayName: "Language",
Description: "User language",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &languageSetting,
},
},
}
}
func generatePermissionRequests() []*settingssvc.AddSettingToBundleRequest {
return []*settingssvc.AddSettingToBundleRequest{
{
BundleId: BundleUUIDRoleAdmin,
Setting: &settingsmsg.Setting{
Id: RoleManagementPermissionID,
Name: RoleManagementPermissionName,
DisplayName: "Role Management",
Description: "This permission gives full access to everything that is related to role management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "all",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleAdmin,
Setting: &settingsmsg.Setting{
Id: SettingsManagementPermissionID,
Name: SettingsManagementPermissionName,
DisplayName: "Settings Management",
Description: "This permission gives full access to everything that is related to settings management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "all",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleAdmin,
Setting: &settingsmsg.Setting{
Id: "7d81f103-0488-4853-bce5-98dcce36d649",
Name: "language-readwrite",
DisplayName: "Permission to read and set the language (anyone)",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: settingUUIDProfileLanguage,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleUser,
Setting: &settingsmsg.Setting{
Id: "640e00d2-4df8-41bd-b1c2-9f30a01e0e99",
Name: "language-readwrite",
DisplayName: "Permission to read and set the language (self)",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: settingUUIDProfileLanguage,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
{
BundleId: BundleUUIDRoleGuest,
Setting: &settingsmsg.Setting{
Id: "ca878636-8b1a-4fae-8282-8617a4c13597",
Name: "language-readwrite",
DisplayName: "Permission to read and set the language (self)",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: settingUUIDProfileLanguage,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
{
BundleId: BundleUUIDRoleAdmin,
Setting: &settingsmsg.Setting{
Id: AccountManagementPermissionID,
Name: AccountManagementPermissionName,
DisplayName: "Account Management",
Description: "This permission gives full access to everything that is related to account management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "all",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleAdmin,
Setting: &settingsmsg.Setting{
Id: GroupManagementPermissionID,
Name: GroupManagementPermissionName,
DisplayName: "Group Management",
Description: "This permission gives full access to everything that is related to group management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_GROUP,
Id: "all",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleUser,
Setting: &settingsmsg.Setting{
Id: SelfManagementPermissionID,
Name: SelfManagementPermissionName,
DisplayName: "Self Management",
Description: "This permission gives access to self management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "me",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
{
BundleId: BundleUUIDRoleAdmin,
Setting: &settingsmsg.Setting{
Id: SetSpaceQuotaPermissionID,
Name: SetSpaceQuotaPermissionName,
DisplayName: "Set Space Quota",
Description: "This permission allows to manage space quotas.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleUser,
Setting: &settingsmsg.Setting{
Id: CreateSpacePermissionID,
Name: CreateSpacePermissionName,
DisplayName: "Create own Space",
Description: "This permission allows to create a space owned by the current user.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM, // TODO resource type space? self? me? own?
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_CREATE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
{
BundleId: BundleUUIDRoleAdmin,
Setting: &settingsmsg.Setting{
Id: CreateSpacePermissionID,
Name: CreateSpacePermissionName,
DisplayName: "Create Space",
Description: "This permission allows to create new spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleAdmin,
Setting: &settingsmsg.Setting{
Id: ListAllSpacesPermissionID,
Name: ListAllSpacesPermissionName,
DisplayName: "List All Spaces",
Description: "This permission allows list all spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleSpaceAdmin,
Setting: &settingsmsg.Setting{
Id: CreateSpacePermissionID,
Name: CreateSpacePermissionName,
DisplayName: "Create Space",
Description: "This permission allows to create new spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleSpaceAdmin,
Setting: &settingsmsg.Setting{
Id: SetSpaceQuotaPermissionID,
Name: SetSpaceQuotaPermissionName,
DisplayName: "Set Space Quota",
Description: "This permission allows to manage space quotas.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleSpaceAdmin,
Setting: &settingsmsg.Setting{
Id: ListAllSpacesPermissionID,
Name: ListAllSpacesPermissionName,
DisplayName: "List All Spaces",
Description: "This permission allows list all spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
{
BundleId: BundleUUIDRoleSpaceAdmin,
Setting: &settingsmsg.Setting{
Id: "640e00d2-4df8-41bd-b1c2-9f30a01e0e99",
Name: "language-readwrite",
DisplayName: "Permission to read and set the language (self)",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: settingUUIDProfileLanguage,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
{
BundleId: BundleUUIDRoleSpaceAdmin,
Setting: &settingsmsg.Setting{
Id: SelfManagementPermissionID,
Name: SelfManagementPermissionName,
DisplayName: "Self Management",
Description: "This permission gives access to self management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "me",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
{
BundleId: BundleUUIDRoleMetadata,
Setting: &settingsmsg.Setting{
Id: CreateSpacePermissionID,
Name: CreateSpacePermissionName,
DisplayName: "Create own Space",
Description: "This permission allows to create a space owned by the current user.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM, // TODO resource type space? self? me? own?
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_CREATE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
}
}
func defaultRoleAssignments() []*settingsmsg.UserRoleAssignment {
return []*settingsmsg.UserRoleAssignment{
// accounts service user for the metadata user is allowed to create spaces
{
AccountUuid: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad",
RoleId: BundleUUIDRoleAdmin,
},
// default admin users
{
AccountUuid: "058bff95-6708-4fe5-91e4-9ea3d377588b",
RoleId: BundleUUIDRoleAdmin,
}, {
AccountUuid: "ddc2004c-0977-11eb-9d3f-a793888cd0f8",
RoleId: BundleUUIDRoleAdmin,
}, {
AccountUuid: "820ba2a1-3f54-4538-80a4-2d73007e30bf",
RoleId: BundleUUIDRoleAdmin,
}, {
AccountUuid: "bc596f3c-c955-4328-80a0-60d018b4ad57",
RoleId: BundleUUIDRoleAdmin,
},
// default users with role "user"
{
AccountUuid: "4c510ada-c86b-4815-8820-42cdf82c3d51",
RoleId: BundleUUIDRoleUser,
}, {
AccountUuid: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c",
RoleId: BundleUUIDRoleUser,
},
// default users with role "spaceadmin"
{
AccountUuid: "534bb038-6f9d-4093-946f-133be61fa4e7",
RoleId: BundleUUIDRoleSpaceAdmin,
},
}
}
@@ -0,0 +1,9 @@
package svc
// NewTracing returns a service that instruments traces.
func NewTracing(next Service) Service {
return Service{
manager: next.manager,
config: next.config,
}
}
@@ -0,0 +1,164 @@
package svc
import (
"regexp"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/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
}
// 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 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)
}
@@ -0,0 +1,59 @@
package settings
import (
"errors"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
var (
// Registry uses the strategy pattern as a registry
Registry = map[string]RegisterFunc{}
// ErrPermissionNotFound defines a new error for when a permission was not found
ErrPermissionNotFound = errors.New("permission not found")
)
// RegisterFunc stores store constructors
type RegisterFunc func(*config.Config) Manager
// 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)
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,571 @@
package defaults
import (
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
const (
// BundleUUIDRoleAdmin represents the admin role
BundleUUIDRoleAdmin = "71881883-1768-46bd-a24d-a356a2afdf7f"
// BundleUUIDRoleSpaceAdmin represents the space admin role
BundleUUIDRoleSpaceAdmin = "2aadd357-682c-406b-8874-293091995fdd"
// BundleUUIDRoleUser represents the user role.
BundleUUIDRoleUser = "d7beeea8-8ff4-406b-8fb6-ab2dd81e6b11"
// BundleUUIDRoleGuest represents the guest role.
BundleUUIDRoleGuest = "38071a68-456a-4553-846a-fa67bf5596cc"
// BundleUUIDRoleMetadata represents the metadata user role
BundleUUIDRoleMetadata = "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad"
// RoleManagementPermissionID is the hardcoded setting UUID for the role management permission
RoleManagementPermissionID string = "a53e601e-571f-4f86-8fec-d4576ef49c62"
// RoleManagementPermissionName is the hardcoded setting name for the role management permission
RoleManagementPermissionName string = "role-management"
// SettingsManagementPermissionID is the hardcoded setting UUID for the settings management permission
SettingsManagementPermissionID string = "79e13b30-3e22-11eb-bc51-0b9f0bad9a58"
// SettingsManagementPermissionName is the hardcoded setting name for the settings management permission
SettingsManagementPermissionName string = "settings-management"
// SetSpaceQuotaPermissionID is the hardcoded setting UUID for the set space quota permission
SetSpaceQuotaPermissionID string = "4e6f9709-f9e7-44f1-95d4-b762d27b7896"
// SetSpaceQuotaPermissionName is the hardcoded setting name for the set space quota permission
SetSpaceQuotaPermissionName string = "set-space-quota"
// ListAllSpacesPermissionID is the hardcoded setting UUID for the list all spaces permission
ListAllSpacesPermissionID string = "016f6ddd-9501-4a0a-8ebe-64a20ee8ec82"
// ListAllSpacesPermissionName is the hardcoded setting name for the list all spaces permission
ListAllSpacesPermissionName string = "list-all-spaces"
// CreateSpacePermissionID is the hardcoded setting UUID for the create space permission
CreateSpacePermissionID string = "79e13b30-3e22-11eb-bc51-0b9f0bad9a58"
// CreateSpacePermissionName is the hardcoded setting name for the create space permission
CreateSpacePermissionName string = "create-space"
settingUUIDProfileLanguage = "aa8cfbe5-95d4-4f7e-a032-c3c01f5f062f"
// AccountManagementPermissionID is the hardcoded setting UUID for the account management permission
AccountManagementPermissionID string = "8e587774-d929-4215-910b-a317b1e80f73"
// AccountManagementPermissionName is the hardcoded setting name for the account management permission
AccountManagementPermissionName string = "account-management"
// GroupManagementPermissionID is the hardcoded setting UUID for the group management permission
GroupManagementPermissionID string = "522adfbe-5908-45b4-b135-41979de73245"
// GroupManagementPermissionName is the hardcoded setting name for the group management permission
GroupManagementPermissionName string = "group-management"
// SelfManagementPermissionID is the hardcoded setting UUID for the self management permission
SelfManagementPermissionID string = "e03070e9-4362-4cc6-a872-1c7cb2eb2b8e"
// SelfManagementPermissionName is the hardcoded setting name for the self management permission
SelfManagementPermissionName string = "self-management"
)
// GenerateBundlesDefaultRoles bootstraps the default roles.
func GenerateBundlesDefaultRoles() []*settingsmsg.Bundle {
return []*settingsmsg.Bundle{
generateBundleAdminRole(),
generateBundleUserRole(),
generateBundleGuestRole(),
generateBundleProfileRequest(),
generateBundleMetadataRole(),
generateBundleSpaceAdminRole(),
}
}
func generateBundleAdminRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleAdmin,
Name: "admin",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "ocis-roles",
DisplayName: "Admin",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
{
Id: RoleManagementPermissionID,
Name: RoleManagementPermissionName,
DisplayName: "Role Management",
Description: "This permission gives full access to everything that is related to role management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "all",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
{
Id: SettingsManagementPermissionID,
Name: SettingsManagementPermissionName,
DisplayName: "Settings Management",
Description: "This permission gives full access to everything that is related to settings management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "all",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
{
Id: "7d81f103-0488-4853-bce5-98dcce36d649",
Name: "language-readwrite",
DisplayName: "Permission to read and set the language (anyone)",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: settingUUIDProfileLanguage,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
{
Id: AccountManagementPermissionID,
Name: AccountManagementPermissionName,
DisplayName: "Account Management",
Description: "This permission gives full access to everything that is related to account management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "all",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
{
Id: GroupManagementPermissionID,
Name: GroupManagementPermissionName,
DisplayName: "Group Management",
Description: "This permission gives full access to everything that is related to group management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_GROUP,
Id: "all",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
{
Id: SetSpaceQuotaPermissionID,
Name: SetSpaceQuotaPermissionName,
DisplayName: "Set Space Quota",
Description: "This permission allows to manage space quotas.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
{
Id: CreateSpacePermissionID,
Name: CreateSpacePermissionName,
DisplayName: "Create Space",
Description: "This permission allows to create new spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
{
Id: ListAllSpacesPermissionID,
Name: ListAllSpacesPermissionName,
DisplayName: "List All Spaces",
Description: "This permission allows list all spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
},
}
}
func generateBundleSpaceAdminRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleSpaceAdmin,
Name: "spaceadmin",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "ocis-roles",
DisplayName: "Space Admin",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
{
Id: SetSpaceQuotaPermissionID,
Name: SetSpaceQuotaPermissionName,
DisplayName: "Set Space Quota",
Description: "This permission allows to manage space quotas.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
{
Id: CreateSpacePermissionID,
Name: CreateSpacePermissionName,
DisplayName: "Create Space",
Description: "This permission allows to create new spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
{
Id: ListAllSpacesPermissionID,
Name: ListAllSpacesPermissionName,
DisplayName: "List All Spaces",
Description: "This permission allows list all spaces.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
},
},
},
{
Id: "640e00d2-4df8-41bd-b1c2-9f30a01e0e99",
Name: "language-readwrite",
DisplayName: "Permission to read and set the language (self)",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: settingUUIDProfileLanguage,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
{
Id: SelfManagementPermissionID,
Name: SelfManagementPermissionName,
DisplayName: "Self Management",
Description: "This permission gives access to self management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "me",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
{
Id: CreateSpacePermissionID,
Name: CreateSpacePermissionName,
DisplayName: "Create own Space",
Description: "This permission allows to create a space owned by the current user.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM, // TODO resource type space? self? me? own?
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_CREATE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
}
}
func generateBundleUserRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleUser,
Name: "user",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "ocis-roles",
DisplayName: "User",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
{
Id: "640e00d2-4df8-41bd-b1c2-9f30a01e0e99",
Name: "language-readwrite",
DisplayName: "Permission to read and set the language (self)",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: settingUUIDProfileLanguage,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
{
Id: SelfManagementPermissionID,
Name: SelfManagementPermissionName,
DisplayName: "Self Management",
Description: "This permission gives access to self management.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "me",
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
{
Id: CreateSpacePermissionID,
Name: CreateSpacePermissionName,
DisplayName: "Create own Space",
Description: "This permission allows to create a space owned by the current user.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM, // TODO resource type space? self? me? own?
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_CREATE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
}
}
func generateBundleGuestRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleGuest,
Name: "guest",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "ocis-roles",
DisplayName: "Guest",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
{
Id: "ca878636-8b1a-4fae-8282-8617a4c13597",
Name: "language-readwrite",
DisplayName: "Permission to read and set the language (self)",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: settingUUIDProfileLanguage,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
}
}
func generateBundleProfileRequest() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: "2a506de7-99bd-4f0d-994e-c38e72c28fd9",
Name: "profile",
Extension: "ocis-accounts",
Type: settingsmsg.Bundle_TYPE_DEFAULT,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
DisplayName: "Profile",
Settings: []*settingsmsg.Setting{
{
Id: settingUUIDProfileLanguage,
Name: "language",
DisplayName: "Language",
Description: "User language",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &languageSetting,
},
},
}
}
func generateBundleMetadataRole() *settingsmsg.Bundle {
return &settingsmsg.Bundle{
Id: BundleUUIDRoleMetadata,
Name: "metadata",
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: "ocis-roles",
DisplayName: "Metadata",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
{
Id: CreateSpacePermissionID,
Name: CreateSpacePermissionName,
DisplayName: "Create own Space",
Description: "This permission allows to create a space owned by the current user.",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM, // TODO resource type space? self? me? own?
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_CREATE,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
}
}
// TODO: languageSetting needed?
var languageSetting = settingsmsg.Setting_SingleChoiceValue{
SingleChoiceValue: &settingsmsg.SingleChoiceList{
Options: []*settingsmsg.ListOption{
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "cs",
},
},
DisplayValue: "Czech",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "de",
},
},
DisplayValue: "Deutsch",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "en",
},
},
DisplayValue: "English",
Default: true,
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "es",
},
},
DisplayValue: "Español",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "fr",
},
},
DisplayValue: "Français",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "gl",
},
},
DisplayValue: "Galego",
},
{
Value: &settingsmsg.ListOptionValue{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: "it",
},
},
DisplayValue: "Italiano",
},
},
},
}
// DefaultRoleAssignments returns (as one might guess) the default role assignments
func DefaultRoleAssignments() []*settingsmsg.UserRoleAssignment {
return []*settingsmsg.UserRoleAssignment{
// accounts service user for the metadata user is allowed to create spaces
{
AccountUuid: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad",
RoleId: BundleUUIDRoleAdmin,
},
// default admin users
{
AccountUuid: "058bff95-6708-4fe5-91e4-9ea3d377588b",
RoleId: BundleUUIDRoleAdmin,
}, {
AccountUuid: "ddc2004c-0977-11eb-9d3f-a793888cd0f8",
RoleId: BundleUUIDRoleAdmin,
}, {
AccountUuid: "820ba2a1-3f54-4538-80a4-2d73007e30bf",
RoleId: BundleUUIDRoleAdmin,
}, {
AccountUuid: "bc596f3c-c955-4328-80a0-60d018b4ad57",
RoleId: BundleUUIDRoleAdmin,
},
// default users with role "user"
{
AccountUuid: "4c510ada-c86b-4815-8820-42cdf82c3d51",
RoleId: BundleUUIDRoleUser,
}, {
AccountUuid: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c",
RoleId: BundleUUIDRoleUser,
}, {
AccountUuid: "932b4540-8d16-481e-8ef4-588e4b6b151c",
RoleId: BundleUUIDRoleUser,
},
// default users with role "spaceadmin"
{
AccountUuid: "534bb038-6f9d-4093-946f-133be61fa4e7",
RoleId: BundleUUIDRoleSpaceAdmin,
},
}
}
@@ -0,0 +1,9 @@
package errortypes
// BundleNotFound is the error to use when a bundle is not found.
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,67 @@
// Package store implements the go-micro store interface
package store
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/gofrs/uuid"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
// ListRoleAssignments loads and returns all role assignments matching the given assignment identifier.
func (s Store) ListRoleAssignments(accountUUID string) ([]*settingsmsg.UserRoleAssignment, error) {
var records []*settingsmsg.UserRoleAssignment
assignmentsFolder := s.buildFolderPathForRoleAssignments(false)
assignmentFiles, err := ioutil.ReadDir(assignmentsFolder)
if err != nil {
return records, nil
}
for _, assignmentFile := range assignmentFiles {
record := settingsmsg.UserRoleAssignment{}
err = s.parseRecordFromFile(&record, filepath.Join(assignmentsFolder, assignmentFile.Name()))
if err == nil {
if record.AccountUuid == accountUUID {
records = append(records, &record)
}
}
}
return records, 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) {
// as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role"
list, err := s.ListRoleAssignments(accountUUID)
if err != nil {
return nil, err
}
if len(list) > 0 {
filePath := s.buildFilePathForRoleAssignment(list[0].Id, true)
if err := os.Remove(filePath); err != nil {
return nil, err
}
}
assignment := &settingsmsg.UserRoleAssignment{
Id: uuid.Must(uuid.NewV4()).String(),
AccountUuid: accountUUID,
RoleId: roleID,
}
filePath := s.buildFilePathForRoleAssignment(assignment.Id, true)
if err := s.writeRecordToFile(assignment, filePath); err != nil {
return nil, err
}
s.Logger.Debug().Msgf("request contents written to file: %v", filePath)
return assignment, nil
}
// RemoveRoleAssignment deletes the given role assignment from the existing assignments of the respective account.
func (s Store) RemoveRoleAssignment(assignmentID string) error {
filePath := s.buildFilePathForRoleAssignment(assignmentID, false)
return os.Remove(filePath)
}
@@ -0,0 +1,178 @@
package store
import (
"errors"
"log"
"os"
"path/filepath"
"testing"
olog "github.com/owncloud/ocis/ocis-pkg/log"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
"github.com/stretchr/testify/assert"
)
var (
einstein = "a4d07560-a670-4be9-8d60-9b547751a208"
//marie = "3c054db3-eec1-4ca4-b985-bc56dcf560cb"
s = Store{
dataPath: dataRoot,
Logger: logger,
}
logger = olog.NewLogger(
olog.Color(true),
olog.Pretty(true),
olog.Level("info"),
)
bundles = []*settingsmsg.Bundle{
{
Id: "f36db5e6-a03c-40df-8413-711c67e40b47",
Type: settingsmsg.Bundle_TYPE_ROLE,
DisplayName: "test role - reads | update",
Name: "TEST_ROLE",
Extension: "ocis-settings",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
},
Settings: []*settingsmsg.Setting{
{
Name: "update",
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_UPDATE,
},
},
},
{
Name: "read",
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
},
},
},
},
},
{
Id: "44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
Type: settingsmsg.Bundle_TYPE_ROLE,
DisplayName: "another",
Name: "ANOTHER_TEST_ROLE",
Extension: "ocis-settings",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
},
Settings: []*settingsmsg.Setting{
{
Name: "read",
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
},
},
},
},
},
}
)
func init() {
setupRoles()
}
func setupRoles() {
for i := range bundles {
if _, err := s.WriteBundle(bundles[i]); err != nil {
log.Fatal(err)
}
}
}
func TestAssignmentUniqueness(t *testing.T) {
var scenarios = []struct {
name string
userID string
firstRole string
secondRole string
}{
{
"roles assignments",
einstein,
"f36db5e6-a03c-40df-8413-711c67e40b47",
"44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.name, func(t *testing.T) {
firstAssignment, err := s.WriteRoleAssignment(scenario.userID, scenario.firstRole)
assert.NoError(t, err)
assert.Equal(t, firstAssignment.RoleId, scenario.firstRole)
assert.FileExists(t, filepath.Join(dataRoot, "assignments", firstAssignment.Id+".json"))
list, err := s.ListRoleAssignments(scenario.userID)
assert.NoError(t, err)
assert.Equal(t, 1, len(list))
// creating another assignment shouldn't add another entry, as we support max one role per user.
secondAssignment, err := s.WriteRoleAssignment(scenario.userID, scenario.secondRole)
assert.NoError(t, err)
assert.Equal(t, 1, len(list))
// assigning the second role should remove the old file and create a new one.
list, err = s.ListRoleAssignments(scenario.userID)
assert.NoError(t, err)
assert.Equal(t, 1, len(list))
assert.Equal(t, secondAssignment.RoleId, scenario.secondRole)
assert.NoFileExists(t, filepath.Join(dataRoot, "assignments", firstAssignment.Id+".json"))
assert.FileExists(t, filepath.Join(dataRoot, "assignments", secondAssignment.Id+".json"))
})
}
burnRoot()
}
func TestDeleteAssignment(t *testing.T) {
var scenarios = []struct {
name string
userID string
firstRole string
secondRole string
}{
{
"roles assignments",
einstein,
"f36db5e6-a03c-40df-8413-711c67e40b47",
"44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.name, func(t *testing.T) {
assignment, err := s.WriteRoleAssignment(scenario.userID, scenario.firstRole)
assert.NoError(t, err)
assert.Equal(t, assignment.RoleId, scenario.firstRole)
assert.FileExists(t, filepath.Join(dataRoot, "assignments", assignment.Id+".json"))
list, err := s.ListRoleAssignments(scenario.userID)
assert.NoError(t, err)
assert.Equal(t, 1, len(list))
err = s.RemoveRoleAssignment(assignment.Id)
assert.NoError(t, err)
list, err = s.ListRoleAssignments(scenario.userID)
assert.NoError(t, err)
assert.Equal(t, 0, len(list))
err = s.RemoveRoleAssignment(assignment.Id)
merr := &os.PathError{}
assert.Equal(t, true, errors.As(err, &merr))
})
}
burnRoot()
}
@@ -0,0 +1,180 @@
// Package store implements the go-micro store interface
package store
import (
"fmt"
"io/ioutil"
"path/filepath"
"sync"
"github.com/gofrs/uuid"
"github.com/owncloud/ocis/extensions/settings/pkg/store/errortypes"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
var m = &sync.RWMutex{}
// 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) {
// FIXME: list requests should be ran against a cache, not FS
m.RLock()
defer m.RUnlock()
bundlesFolder := s.buildFolderPathForBundles(false)
bundleFiles, err := ioutil.ReadDir(bundlesFolder)
if err != nil {
return []*settingsmsg.Bundle{}, nil
}
records := make([]*settingsmsg.Bundle, 0, len(bundleFiles))
for _, bundleFile := range bundleFiles {
record := settingsmsg.Bundle{}
err = s.parseRecordFromFile(&record, filepath.Join(bundlesFolder, bundleFile.Name()))
if err != nil {
s.Logger.Warn().Msgf("error reading %v", bundleFile)
continue
}
if record.Type != bundleType {
continue
}
if len(bundleIDs) > 0 && !containsStr(record.Id, bundleIDs) {
continue
}
records = append(records, &record)
}
return records, nil
}
// containsStr checks if the strs slice contains str
func containsStr(str string, strs []string) bool {
for _, s := range strs {
if s == str {
return true
}
}
return false
}
// ReadBundle tries to find a bundle by the given id within the dataPath.
func (s Store) ReadBundle(bundleID string) (*settingsmsg.Bundle, error) {
m.RLock()
defer m.RUnlock()
filePath := s.buildFilePathForBundle(bundleID, false)
record := settingsmsg.Bundle{}
if err := s.parseRecordFromFile(&record, filePath); err != nil {
return nil, err
}
s.Logger.Debug().Msgf("read contents from file: %v", filePath)
return &record, nil
}
// ReadSetting tries to find a setting by the given id within the dataPath.
func (s Store) ReadSetting(settingID string) (*settingsmsg.Setting, error) {
m.RLock()
defer m.RUnlock()
bundles, err := s.ListBundles(settingsmsg.Bundle_TYPE_DEFAULT, []string{})
if err != nil {
return nil, err
}
for _, bundle := range bundles {
for _, setting := range bundle.Settings {
if setting.Id == settingID {
return setting, nil
}
}
}
return nil, fmt.Errorf("could not read setting: %v", settingID)
}
// WriteBundle writes the given record into a file within the dataPath.
func (s Store) WriteBundle(record *settingsmsg.Bundle) (*settingsmsg.Bundle, error) {
// FIXME: locking should happen on the file here, not globally.
m.Lock()
defer m.Unlock()
if record.Id == "" {
record.Id = uuid.Must(uuid.NewV4()).String()
}
filePath := s.buildFilePathForBundle(record.Id, true)
if err := s.writeRecordToFile(record, filePath); err != nil {
return nil, err
}
s.Logger.Debug().Msgf("request contents written to file: %v", filePath)
return record, nil
}
// AddSettingToBundle adds the given setting to the bundle with the given bundleID.
func (s Store) AddSettingToBundle(bundleID string, setting *settingsmsg.Setting) (*settingsmsg.Setting, error) {
bundle, err := s.ReadBundle(bundleID)
if err != nil {
if _, notFound := err.(errortypes.BundleNotFound); !notFound {
return nil, err
}
bundle = new(settingsmsg.Bundle)
bundle.Id = bundleID
bundle.Type = settingsmsg.Bundle_TYPE_DEFAULT
}
if setting.Id == "" {
setting.Id = uuid.Must(uuid.NewV4()).String()
}
setSetting(bundle, setting)
_, err = s.WriteBundle(bundle)
if err != nil {
return nil, err
}
return setting, nil
}
// RemoveSettingFromBundle removes the setting from the bundle with the given ids.
func (s Store) RemoveSettingFromBundle(bundleID string, settingID string) error {
bundle, err := s.ReadBundle(bundleID)
if err != nil {
return nil
}
if ok := removeSetting(bundle, settingID); ok {
if _, err := s.WriteBundle(bundle); err != nil {
return err
}
}
return nil
}
// indexOfSetting finds the index of the given setting within the given bundle.
// returns -1 if the setting was not found.
func indexOfSetting(bundle *settingsmsg.Bundle, settingID string) int {
for index := range bundle.Settings {
s := bundle.Settings[index]
if s.Id == settingID {
return index
}
}
return -1
}
// setSetting will append or overwrite the given setting within the given bundle
func setSetting(bundle *settingsmsg.Bundle, setting *settingsmsg.Setting) {
m.Lock()
defer m.Unlock()
index := indexOfSetting(bundle, setting.Id)
if index == -1 {
bundle.Settings = append(bundle.Settings, setting)
} else {
bundle.Settings[index] = setting
}
}
// removeSetting will remove the given setting from the given bundle
func removeSetting(bundle *settingsmsg.Bundle, settingID string) bool {
m.Lock()
defer m.Unlock()
index := indexOfSetting(bundle, settingID)
if index == -1 {
return false
}
bundle.Settings = append(bundle.Settings[:index], bundle.Settings[index+1:]...)
return true
}
@@ -0,0 +1,155 @@
package store
import (
"testing"
olog "github.com/owncloud/ocis/ocis-pkg/log"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
"github.com/stretchr/testify/assert"
)
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,
},
},
},
},
},
},
}
func TestBundles(t *testing.T) {
s := Store{
dataPath: dataRoot,
Logger: olog.NewLogger(
olog.Color(true),
olog.Pretty(true),
olog.Level("info"),
),
}
// write bundles
for i := range bundleScenarios {
index := i
t.Run(bundleScenarios[index].name, func(t *testing.T) {
filePath := s.buildFilePathForBundle(bundleScenarios[index].bundle.Id, true)
if err := s.writeRecordToFile(bundleScenarios[index].bundle, filePath); err != nil {
t.Error(err)
}
assert.FileExists(t, filePath)
})
}
// check that ListBundles only returns bundles with type DEFAULT
bundles, err := s.ListBundles(settingsmsg.Bundle_TYPE_DEFAULT, []string{})
if err != nil {
t.Error(err)
}
for i := range bundles {
assert.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})
if err != nil {
t.Error(err)
}
assert.Equal(t, 1, len(filteredBundles))
if len(filteredBundles) == 1 {
assert.Equal(t, bundle2, filteredBundles[0].Id)
}
// check that ListRoles only returns bundles with type ROLE
roles, err := s.ListBundles(settingsmsg.Bundle_TYPE_ROLE, []string{})
if err != nil {
t.Error(err)
}
for i := range roles {
assert.Equal(t, settingsmsg.Bundle_TYPE_ROLE, roles[i].Type)
}
burnRoot()
}
@@ -0,0 +1,59 @@
package store
import (
"io/ioutil"
"os"
"github.com/owncloud/ocis/extensions/settings/pkg/store/errortypes"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
// Unmarshal file into record
func (s Store) parseRecordFromFile(record proto.Message, filePath string) error {
_, err := os.Stat(filePath)
if err != nil {
return errortypes.BundleNotFound(err.Error())
}
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
b, err := ioutil.ReadAll(file)
if err != nil {
return err
}
if len(b) == 0 {
return errortypes.BundleNotFound(filePath)
}
if err := protojson.Unmarshal(b, record); err != nil {
return err
}
return nil
}
// Marshal record into file
func (s Store) writeRecordToFile(record proto.Message, filePath string) error {
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()
v, err := protojson.Marshal(record)
if err != nil {
return err
}
_, err = file.Write(v)
if err != nil {
return err
}
return nil
}
@@ -0,0 +1,65 @@
package store
import (
"os"
"path/filepath"
)
const folderNameBundles = "bundles"
const folderNameValues = "values"
const folderNameAssignments = "assignments"
// buildFolderPathForBundles builds the folder path for storing settings bundles. If mkdir is true, folders in the path will be created if necessary.
func (s Store) buildFolderPathForBundles(mkdir bool) string {
folderPath := filepath.Join(s.dataPath, folderNameBundles)
if mkdir {
s.ensureFolderExists(folderPath)
}
return folderPath
}
// buildFilePathForBundle builds a unique file name from the given params. If mkdir is true, folders in the path will be created if necessary.
func (s Store) buildFilePathForBundle(bundleID string, mkdir bool) string {
extensionFolder := s.buildFolderPathForBundles(mkdir)
return filepath.Join(extensionFolder, bundleID+".json")
}
// buildFolderPathForValues builds the folder path for storing settings values. If mkdir is true, folders in the path will be created if necessary.
func (s Store) buildFolderPathForValues(mkdir bool) string {
folderPath := filepath.Join(s.dataPath, folderNameValues)
if mkdir {
s.ensureFolderExists(folderPath)
}
return folderPath
}
// buildFilePathForValue builds a unique file name from the given params. If mkdir is true, folders in the path will be created if necessary.
func (s Store) buildFilePathForValue(valueID string, mkdir bool) string {
extensionFolder := s.buildFolderPathForValues(mkdir)
return filepath.Join(extensionFolder, valueID+".json")
}
// buildFolderPathForRoleAssignments builds the folder path for storing role assignments. If mkdir is true, folders in the path will be created if necessary.
func (s Store) buildFolderPathForRoleAssignments(mkdir bool) string {
roleAssignmentsFolder := filepath.Join(s.dataPath, folderNameAssignments)
if mkdir {
s.ensureFolderExists(roleAssignmentsFolder)
}
return roleAssignmentsFolder
}
// buildFilePathForRoleAssignment builds a unique file name from the given params. If mkdir is true, folders in the path will be created if necessary.
func (s Store) buildFilePathForRoleAssignment(assignmentID string, mkdir bool) string {
roleAssignmentsFolder := s.buildFolderPathForRoleAssignments(mkdir)
return filepath.Join(roleAssignmentsFolder, assignmentID+".json")
}
// ensureFolderExists checks if the given path is an existing folder and creates one if not existing
func (s Store) ensureFolderExists(path string) {
if _, err := os.Stat(path); os.IsNotExist(err) {
err = os.MkdirAll(path, 0700)
if err != nil {
s.Logger.Err(err).Msgf("Error creating folder %v", path)
}
}
}
@@ -0,0 +1,72 @@
package store
import (
"github.com/owncloud/ocis/extensions/settings/pkg/settings"
"github.com/owncloud/ocis/extensions/settings/pkg/util"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
// ListPermissionsByResource collects all permissions from the provided roleIDs that match the requested resource
func (s Store) ListPermissionsByResource(resource *settingsmsg.Resource, roleIDs []string) ([]*settingsmsg.Permission, error) {
records := make([]*settingsmsg.Permission, 0)
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
records = append(records, extractPermissionsByResource(resource, role)...)
}
return records, nil
}
// ReadPermissionByID finds the permission in the roles, specified by the provided roleIDs
func (s Store) ReadPermissionByID(permissionID string, roleIDs []string) (*settingsmsg.Permission, error) {
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
for _, permission := range role.Settings {
if permission.Id == permissionID {
if value, ok := permission.Value.(*settingsmsg.Setting_PermissionValue); ok {
return value.PermissionValue, nil
}
}
}
}
return nil, nil
}
// ReadPermissionByName finds the permission in the roles, specified by the provided roleIDs
func (s Store) ReadPermissionByName(name string, roleIDs []string) (*settingsmsg.Permission, error) {
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
for _, permission := range role.Settings {
if permission.Name == name {
if value, ok := permission.Value.(*settingsmsg.Setting_PermissionValue); ok {
return value.PermissionValue, nil
}
}
}
}
return nil, settings.ErrPermissionNotFound
}
// extractPermissionsByResource collects all permissions from the provided role that match the requested resource
func extractPermissionsByResource(resource *settingsmsg.Resource, role *settingsmsg.Bundle) []*settingsmsg.Permission {
permissions := make([]*settingsmsg.Permission, 0)
for _, setting := range role.Settings {
if value, ok := setting.Value.(*settingsmsg.Setting_PermissionValue); ok {
if util.IsResourceMatched(setting.Resource, resource) {
permissions = append(permissions, value.PermissionValue)
}
}
}
return permissions
}
@@ -0,0 +1 @@
checks = ["all", "-ST1003", "-ST1000", "-SA1019"]
@@ -0,0 +1,50 @@
// Package store implements the go-micro store interface
package store
import (
"os"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/extensions/settings/pkg/settings"
olog "github.com/owncloud/ocis/ocis-pkg/log"
)
var (
// Name is the default name for the settings store
Name = "ocis-settings"
managerName = "filesystem"
)
// Store interacts with the filesystem to manage settings information
type Store struct {
dataPath string
Logger olog.Logger
}
// New creates a new store
func New(cfg *config.Config) settings.Manager {
s := Store{
//Logger: olog.NewLogger(
// olog.Color(cfg.Log.Color),
// olog.Pretty(cfg.Log.Pretty),
// olog.Level(cfg.Log.Level),
// olog.File(cfg.Log.File),
//),
}
if _, err := os.Stat(cfg.DataPath); err != nil {
s.Logger.Info().Msgf("creating container on %v", cfg.DataPath)
err = os.MkdirAll(cfg.DataPath, 0700)
if err != nil {
s.Logger.Err(err).Msgf("providing container on %v", cfg.DataPath)
}
}
s.dataPath = cfg.DataPath
return &s
}
func init() {
settings.Registry[managerName] = New
}
@@ -0,0 +1,39 @@
package store
import (
"os"
"path/filepath"
)
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"
dataRoot = "/tmp/herecomesthesun"
)
func burnRoot() {
os.RemoveAll(filepath.Join(dataRoot, "values"))
os.RemoveAll(filepath.Join(dataRoot, "bundles"))
}
@@ -0,0 +1,109 @@
// Package store implements the go-micro store interface
package store
import (
"fmt"
"io/ioutil"
"path/filepath"
"github.com/gofrs/uuid"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
// ListValues reads all values that match the given bundleId and accountUUID.
// If the bundleId is empty, it's ignored for filtering.
// If the accountUUID is empty, only values with empty accountUUID are returned.
// If the accountUUID is not empty, values with an empty or with a matching accountUUID are returned.
func (s Store) ListValues(bundleID, accountUUID string) ([]*settingsmsg.Value, error) {
valuesFolder := s.buildFolderPathForValues(false)
valueFiles, err := ioutil.ReadDir(valuesFolder)
if err != nil {
return []*settingsmsg.Value{}, nil
}
records := make([]*settingsmsg.Value, 0, len(valueFiles))
for _, valueFile := range valueFiles {
record := settingsmsg.Value{}
err := s.parseRecordFromFile(&record, filepath.Join(valuesFolder, valueFile.Name()))
if err != nil {
s.Logger.Warn().Msgf("error reading %v", valueFile)
continue
}
if bundleID != "" && record.BundleId != bundleID {
continue
}
// if requested accountUUID empty -> fetch all system level values
if accountUUID == "" && record.AccountUuid != "" {
continue
}
// if requested accountUUID empty -> fetch all individual + all system level values
if accountUUID != "" && record.AccountUuid != "" && record.AccountUuid != accountUUID {
continue
}
records = append(records, &record)
}
return records, nil
}
// ReadValue tries to find a value by the given valueId within the dataPath
func (s Store) ReadValue(valueID string) (*settingsmsg.Value, error) {
filePath := s.buildFilePathForValue(valueID, false)
record := settingsmsg.Value{}
if err := s.parseRecordFromFile(&record, filePath); err != nil {
return nil, err
}
s.Logger.Debug().Msgf("read contents from file: %v", filePath)
return &record, nil
}
// ReadValueByUniqueIdentifiers tries to find a value given a set of unique identifiers
func (s Store) ReadValueByUniqueIdentifiers(accountUUID, settingID string) (*settingsmsg.Value, error) {
valuesFolder := s.buildFolderPathForValues(false)
files, err := ioutil.ReadDir(valuesFolder)
if err != nil {
return nil, err
}
for i := range files {
if !files[i].IsDir() {
r := settingsmsg.Value{}
s.Logger.Debug().Msgf("reading contents from file: %v", filepath.Join(valuesFolder, files[i].Name()))
if err := s.parseRecordFromFile(&r, filepath.Join(valuesFolder, files[i].Name())); err != nil {
s.Logger.Debug().Msgf("match found: %v", filepath.Join(valuesFolder, files[i].Name()))
return &settingsmsg.Value{}, nil
}
// if value saved without accountUUID, then it's a global value
if r.AccountUuid == "" && r.SettingId == settingID {
return &r, nil
}
// if value saved with accountUUID, then it's a user specific value
if r.AccountUuid == accountUUID && r.SettingId == settingID {
return &r, nil
}
}
}
return nil, fmt.Errorf("could not read value by settingID=%v and accountID=%v", settingID, accountUUID)
}
// WriteValue writes the given value into a file within the dataPath
func (s Store) WriteValue(value *settingsmsg.Value) (*settingsmsg.Value, error) {
s.Logger.Debug().Str("value", value.String()).Msg("writing value")
if value.Id == "" {
value.Id = uuid.Must(uuid.NewV4()).String()
}
// modify value depending on associated resource
if value.Resource.Type == settingsmsg.Resource_TYPE_SYSTEM {
value.AccountUuid = ""
}
// write the value
filePath := s.buildFilePathForValue(value.Id, true)
if err := s.writeRecordToFile(value, filePath); err != nil {
return nil, err
}
return value, nil
}
@@ -0,0 +1,70 @@
package store
import (
"testing"
olog "github.com/owncloud/ocis/ocis-pkg/log"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
"github.com/stretchr/testify/assert"
)
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: bundle1,
SettingId: setting2,
AccountUuid: accountUUID1,
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) {
s := Store{
dataPath: dataRoot,
Logger: olog.NewLogger(
olog.Color(true),
olog.Pretty(true),
olog.Level("info"),
),
}
for i := range valueScenarios {
index := i
t.Run(valueScenarios[index].name, func(t *testing.T) {
filePath := s.buildFilePathForValue(valueScenarios[index].value.Id, true)
if err := s.writeRecordToFile(valueScenarios[index].value, filePath); err != nil {
t.Error(err)
}
assert.FileExists(t, filePath)
})
}
burnRoot()
}
@@ -0,0 +1,111 @@
// Package store implements the go-micro store interface
package store
import (
"context"
"encoding/json"
"fmt"
"github.com/gofrs/uuid"
"github.com/owncloud/ocis/extensions/settings/pkg/store/defaults"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
// ListRoleAssignments loads and returns all role assignments matching the given assignment identifier.
func (s *Store) ListRoleAssignments(accountUUID string) ([]*settingsmsg.UserRoleAssignment, error) {
if s.mdc == nil || accountUUID == "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad" {
return defaultRoleAssignments(accountUUID), nil
}
s.Init()
ctx := context.TODO()
assIDs, err := s.mdc.ReadDir(ctx, accountPath(accountUUID))
if err != nil {
return nil, err
}
ass := make([]*settingsmsg.UserRoleAssignment, 0, len(assIDs))
for _, assID := range assIDs {
b, err := s.mdc.SimpleDownload(ctx, assignmentPath(accountUUID, assID))
if err != nil {
return nil, err
}
a := &settingsmsg.UserRoleAssignment{}
err = json.Unmarshal(b, a)
if err != nil {
return nil, err
}
ass = append(ass, a)
}
return ass, nil
}
// WriteRoleAssignment appends the given role assignment to the existing assignments of the respective account.
func (s *Store) WriteRoleAssignment(accountUUID, roleID string) (*settingsmsg.UserRoleAssignment, error) {
s.Init()
ctx := context.TODO()
// as per https://github.com/owncloud/product/issues/103 "Each user can have exactly one role"
_ = s.mdc.Delete(ctx, accountPath(accountUUID))
// TODO: How to differentiate between 'not found' and other errors?
err := s.mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID))
if err != nil {
return nil, err
}
ass := &settingsmsg.UserRoleAssignment{
Id: uuid.Must(uuid.NewV4()).String(),
AccountUuid: accountUUID,
RoleId: roleID,
}
b, err := json.Marshal(ass)
if err != nil {
return nil, err
}
return ass, s.mdc.SimpleUpload(ctx, assignmentPath(accountUUID, ass.Id), b)
}
// RemoveRoleAssignment deletes the given role assignment from the existing assignments of the respective account.
func (s *Store) RemoveRoleAssignment(assignmentID string) error {
s.Init()
ctx := context.TODO()
accounts, err := s.mdc.ReadDir(ctx, accountsFolderLocation)
if err != nil {
return err
}
// TODO: use indexer to avoid spamming Metadata service
for _, accID := range accounts {
assIDs, err := s.mdc.ReadDir(ctx, accountPath(accID))
if err != nil {
// TODO: error?
continue
}
for _, assID := range assIDs {
if assID == assignmentID {
return s.mdc.Delete(ctx, assignmentPath(accID, assID))
}
}
}
return fmt.Errorf("assignmentID '%s' not found", assignmentID)
}
func defaultRoleAssignments(accID string) []*settingsmsg.UserRoleAssignment {
var assmnts []*settingsmsg.UserRoleAssignment
for _, r := range defaults.DefaultRoleAssignments() {
if r.AccountUuid == accID {
assmnts = append(assmnts, r)
}
}
return assmnts
}
func accountPath(accountUUID string) string {
return fmt.Sprintf("%s/%s", accountsFolderLocation, accountUUID)
}
func assignmentPath(accountUUID string, assignmentID string) string {
return fmt.Sprintf("%s/%s/%s", accountsFolderLocation, accountUUID, assignmentID)
}
@@ -0,0 +1,189 @@
package store
import (
"log"
"sync"
"testing"
"github.com/owncloud/ocis/extensions/settings/pkg/config/defaults"
olog "github.com/owncloud/ocis/ocis-pkg/log"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
"github.com/stretchr/testify/require"
)
var (
einstein = "a4d07560-a670-4be9-8d60-9b547751a208"
//marie = "3c054db3-eec1-4ca4-b985-bc56dcf560cb"
s = &Store{
Logger: logger,
l: &sync.Mutex{},
cfg: defaults.DefaultConfig(),
}
logger = olog.NewLogger(
olog.Color(true),
olog.Pretty(true),
olog.Level("info"),
)
bundles = []*settingsmsg.Bundle{
{
Id: "f36db5e6-a03c-40df-8413-711c67e40b47",
Type: settingsmsg.Bundle_TYPE_ROLE,
DisplayName: "test role - reads | update",
Name: "TEST_ROLE",
Extension: "ocis-settings",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
},
Settings: []*settingsmsg.Setting{
{
Id: "updateID",
Name: "update",
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_UPDATE,
},
},
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
},
},
{
Id: "readID",
Name: "read",
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
},
},
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
},
},
},
},
{
Id: "44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
Type: settingsmsg.Bundle_TYPE_ROLE,
DisplayName: "another",
Name: "ANOTHER_TEST_ROLE",
Extension: "ocis-settings",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
},
Settings: []*settingsmsg.Setting{
{
Id: "readID",
Name: "read",
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
},
},
},
},
},
}
)
func init() {
_ = NewMDC(s)
setupRoles()
}
func setupRoles() {
for i := range bundles {
if _, err := s.WriteBundle(bundles[i]); err != nil {
log.Fatal("error initializing ", err)
}
}
}
func TestAssignmentUniqueness(t *testing.T) {
var scenarios = []struct {
name string
userID string
firstRole string
secondRole string
}{
{
"roles assignments",
einstein,
"f36db5e6-a03c-40df-8413-711c67e40b47",
"44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.name, func(t *testing.T) {
firstAssignment, err := s.WriteRoleAssignment(scenario.userID, scenario.firstRole)
require.NoError(t, err)
require.Equal(t, firstAssignment.RoleId, scenario.firstRole)
// TODO: check entry exists
list, err := s.ListRoleAssignments(scenario.userID)
require.NoError(t, err)
require.Equal(t, 1, len(list))
require.Equal(t, list[0].RoleId, scenario.firstRole)
// creating another assignment shouldn't add another entry, as we support max one role per user.
// assigning the second role should remove the old
secondAssignment, err := s.WriteRoleAssignment(scenario.userID, scenario.secondRole)
require.NoError(t, err)
require.Equal(t, secondAssignment.RoleId, scenario.secondRole)
list, err = s.ListRoleAssignments(scenario.userID)
require.NoError(t, err)
require.Equal(t, 1, len(list))
require.Equal(t, list[0].RoleId, scenario.secondRole)
})
}
}
func TestDeleteAssignment(t *testing.T) {
var scenarios = []struct {
name string
userID string
firstRole string
secondRole string
}{
{
"roles assignments",
einstein,
"f36db5e6-a03c-40df-8413-711c67e40b47",
"44f1a664-0a7f-461a-b0be-5b59e46bbc7a",
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.name, func(t *testing.T) {
assignment, err := s.WriteRoleAssignment(scenario.userID, scenario.firstRole)
require.NoError(t, err)
require.Equal(t, assignment.RoleId, scenario.firstRole)
// TODO: uncomment
// require.True(t, mdc.IDExists(assignment.RoleId))
list, err := s.ListRoleAssignments(scenario.userID)
require.NoError(t, err)
require.Equal(t, 1, len(list))
require.Equal(t, assignment.Id, list[0].Id)
err = s.RemoveRoleAssignment(assignment.Id)
require.NoError(t, err)
// TODO: uncomment
// require.False(t, mdc.IDExists(assignment.RoleId))
list, err = s.ListRoleAssignments(scenario.userID)
require.NoError(t, err)
require.Equal(t, 0, len(list))
err = s.RemoveRoleAssignment(assignment.Id)
require.Error(t, err)
// TODO: do we want a custom error message?
})
}
}
@@ -0,0 +1,146 @@
// Package store implements the go-micro store interface
package store
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gofrs/uuid"
"github.com/owncloud/ocis/extensions/settings/pkg/store/defaults"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
// ListBundles returns all bundles in the dataPath folder that match the given type.
func (s *Store) ListBundles(bundleType settingsmsg.Bundle_Type, bundleIDs []string) ([]*settingsmsg.Bundle, error) {
// TODO: this is needed for initialization - we need to find a better way to fix this
if s.mdc == nil && len(bundleIDs) == 1 {
return defaultBundle(bundleType, bundleIDs[0]), nil
}
s.Init()
ctx := context.TODO()
if len(bundleIDs) == 0 {
bIDs, err := s.mdc.ReadDir(ctx, bundleFolderLocation)
if err != nil {
return nil, err
}
bundleIDs = bIDs
}
var bundles []*settingsmsg.Bundle
for _, id := range bundleIDs {
b, err := s.mdc.SimpleDownload(ctx, bundlePath(id))
if err != nil {
return nil, err
}
bundle := &settingsmsg.Bundle{}
err = json.Unmarshal(b, bundle)
if err != nil {
return nil, err
}
if bundle.Type == bundleType {
bundles = append(bundles, bundle)
}
}
return bundles, nil
}
// ReadBundle tries to find a bundle by the given id from the metadata service
func (s *Store) ReadBundle(bundleID string) (*settingsmsg.Bundle, error) {
if s.mdc == nil {
return defaultBundle(settingsmsg.Bundle_TYPE_ROLE, bundleID)[0], nil
}
s.Init()
ctx := context.TODO()
b, err := s.mdc.SimpleDownload(ctx, bundlePath(bundleID))
if err != nil {
return nil, err
}
bundle := &settingsmsg.Bundle{}
return bundle, json.Unmarshal(b, bundle)
}
// ReadSetting tries to find a setting by the given id from the metadata service
func (s *Store) ReadSetting(settingID string) (*settingsmsg.Setting, error) {
s.Init()
ctx := context.TODO()
ids, err := s.mdc.ReadDir(ctx, bundleFolderLocation)
if err != nil {
return nil, err
}
// TODO: avoid spamming metadata service
for _, id := range ids {
b, err := s.ReadBundle(id)
if err != nil {
return nil, err
}
for _, setting := range b.Settings {
if setting.Id == settingID {
return setting, nil
}
}
}
return nil, fmt.Errorf("setting '%s' not found", settingID)
}
// WriteBundle sends the givens record to the metadataclient. returns `record` for legacy reasons
func (s *Store) WriteBundle(record *settingsmsg.Bundle) (*settingsmsg.Bundle, error) {
s.Init()
ctx := context.TODO()
b, err := json.Marshal(record)
if err != nil {
return nil, err
}
return record, s.mdc.SimpleUpload(ctx, bundlePath(record.Id), b)
}
// AddSettingToBundle adds the given setting to the bundle with the given bundleID.
func (s *Store) AddSettingToBundle(bundleID string, setting *settingsmsg.Setting) (*settingsmsg.Setting, error) {
s.Init()
b, err := s.ReadBundle(bundleID)
if err != nil {
// TODO: How to differentiate 'not found'?
b = new(settingsmsg.Bundle)
b.Id = bundleID
b.Type = settingsmsg.Bundle_TYPE_DEFAULT
}
if setting.Id == "" {
setting.Id = uuid.Must(uuid.NewV4()).String()
}
b.Settings = append(b.Settings, setting)
_, err = s.WriteBundle(b)
return setting, err
}
// RemoveSettingFromBundle removes the setting from the bundle with the given ids.
func (s *Store) RemoveSettingFromBundle(bundleID string, settingID string) error {
fmt.Println("RemoveSettingFromBundle not implemented")
return errors.New("not implemented")
}
func bundlePath(id string) string {
return fmt.Sprintf("%s/%s", bundleFolderLocation, id)
}
func defaultBundle(bundleType settingsmsg.Bundle_Type, bundleID string) []*settingsmsg.Bundle {
var bundles []*settingsmsg.Bundle
for _, b := range defaults.GenerateBundlesDefaultRoles() {
if b.Type == bundleType && b.Id == bundleID {
bundles = append(bundles, b)
}
}
return bundles
}
@@ -0,0 +1,208 @@
package store
import (
"testing"
"github.com/gofrs/uuid"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
"github.com/stretchr/testify/require"
)
var bundleScenarios = []struct {
name string
bundle *settingsmsg.Bundle
}{
{
name: "generic-test-file-resource",
bundle: &settingsmsg.Bundle{
Id: bundle1,
Type: settingsmsg.Bundle_TYPE_DEFAULT,
Extension: extension1,
DisplayName: "test1",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_FILE,
Id: "beep",
},
Settings: []*settingsmsg.Setting{
{
Id: setting1,
Description: "test-desc-1",
DisplayName: "test-displayname-1",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_FILE,
Id: "bleep",
},
Value: &settingsmsg.Setting_IntValue{
IntValue: &settingsmsg.Int{
Min: 0,
Max: 42,
},
},
},
},
},
},
{
name: "generic-test-system-resource",
bundle: &settingsmsg.Bundle{
Id: bundle2,
Type: settingsmsg.Bundle_TYPE_DEFAULT,
Extension: extension2,
DisplayName: "test1",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
{
Id: setting2,
Description: "test-desc-2",
DisplayName: "test-displayname-2",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Setting_IntValue{
IntValue: &settingsmsg.Int{
Min: 0,
Max: 42,
},
},
},
},
},
},
{
name: "generic-test-role-bundle",
bundle: &settingsmsg.Bundle{
Id: bundle3,
Type: settingsmsg.Bundle_TYPE_ROLE,
Extension: extension1,
DisplayName: "Role1",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
{
Id: setting3,
Description: "test-desc-3",
DisplayName: "test-displayname-3",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: setting1,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
},
},
},
},
}
var (
appendTestBundleID = uuid.Must(uuid.NewV4()).String()
appendTestSetting1 = &settingsmsg.Setting{
Id: "append-test-setting-1",
Description: "test-desc-3",
DisplayName: "test-displayname-3",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: setting1,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
}
appendTestSetting2 = &settingsmsg.Setting{
Id: "append-test-setting-2",
Description: "test-desc-3",
DisplayName: "test-displayname-3",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: setting1,
},
Value: &settingsmsg.Setting_PermissionValue{
PermissionValue: &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READ,
Constraint: settingsmsg.Permission_CONSTRAINT_OWN,
},
},
}
)
func TestBundles(t *testing.T) {
for i := range bundleScenarios {
b := bundleScenarios[i]
t.Run(b.name, func(t *testing.T) {
_, err := s.WriteBundle(b.bundle)
require.NoError(t, err)
bundle, err := s.ReadBundle(b.bundle.Id)
require.NoError(t, err)
require.Equal(t, b.bundle, bundle)
})
}
// check that ListBundles only returns bundles with type DEFAULT
bundles, err := s.ListBundles(settingsmsg.Bundle_TYPE_DEFAULT, []string{})
require.NoError(t, err)
for i := range bundles {
require.Equal(t, settingsmsg.Bundle_TYPE_DEFAULT, bundles[i].Type)
}
// check that ListBundles filtered by an id only returns that bundle
filteredBundles, err := s.ListBundles(settingsmsg.Bundle_TYPE_DEFAULT, []string{bundle2})
require.NoError(t, err)
require.Equal(t, 1, len(filteredBundles))
if len(filteredBundles) == 1 {
require.Equal(t, bundle2, filteredBundles[0].Id)
}
// check that ListRoles only returns bundles with type ROLE
roles, err := s.ListBundles(settingsmsg.Bundle_TYPE_ROLE, []string{})
require.NoError(t, err)
for i := range roles {
require.Equal(t, settingsmsg.Bundle_TYPE_ROLE, roles[i].Type)
}
// check that ReadSetting works
setting, err := s.ReadSetting(setting1)
require.NoError(t, err)
require.Equal(t, "test-desc-1", setting.Description) // could be tested better ;)
}
func TestAppendSetting(t *testing.T) {
//mdc := NewMDC()
//s := Store{
//Logger: olog.NewLogger(
//olog.Color(true),
//olog.Pretty(true),
//olog.Level("info"),
//),
//l: &sync.Mutex{},
//mdc: mdc,
//}
// appending to non existing bundle creates new
_, err := s.AddSettingToBundle(appendTestBundleID, appendTestSetting1)
require.NoError(t, err)
b, err := s.ReadBundle(appendTestBundleID)
require.NoError(t, err)
require.Len(t, b.Settings, 1)
_, err = s.AddSettingToBundle(appendTestBundleID, appendTestSetting2)
require.NoError(t, err)
b, err = s.ReadBundle(appendTestBundleID)
require.NoError(t, err)
require.Len(t, b.Settings, 2)
}
@@ -0,0 +1,123 @@
package store
import (
"context"
"path"
"strings"
"time"
"github.com/ReneKroon/ttlcache/v2"
)
var (
cachettl = 0
// these need to be global instances for now as the `Service` (and therefore the `Store`) are instantiated twice (for grpc and http)
// therefore caches need to cover both instances
dircache = initCache(cachettl)
filescache = initCache(cachettl)
)
// CachedMDC is cache for the metadataclient
type CachedMDC struct {
next MetadataClient
files *ttlcache.Cache
dirs *ttlcache.Cache
}
// SimpleDownload caches the answer from SimpleDownload or returns the cached one
func (c *CachedMDC) SimpleDownload(ctx context.Context, id string) ([]byte, error) {
if b, err := c.files.Get(id); err == nil {
return b.([]byte), nil
}
b, err := c.next.SimpleDownload(ctx, id)
if err != nil {
return nil, err
}
_ = c.files.Set(id, b)
return b, nil
}
// SimpleUpload caches the answer from SimpleUpload and invalidates the cache
func (c *CachedMDC) SimpleUpload(ctx context.Context, id string, content []byte) error {
b, err := c.files.Get(id)
if err == nil && string(b.([]byte)) == string(content) {
// no need to bug mdc
return nil
}
err = c.next.SimpleUpload(ctx, id, content)
if err != nil {
return err
}
// invalidate caches
_ = c.dirs.Remove(path.Dir(id))
_ = c.files.Set(id, content)
return nil
}
// Delete invalidates the cache when operation was successful
func (c *CachedMDC) Delete(ctx context.Context, id string) error {
if err := c.next.Delete(ctx, id); err != nil {
return err
}
// invalidate caches
_ = removePrefix(c.files, id)
_ = removePrefix(c.dirs, id)
return nil
}
// ReadDir caches the response from ReadDir or returnes the cached one
func (c *CachedMDC) ReadDir(ctx context.Context, id string) ([]string, error) {
i, err := c.dirs.Get(id)
if err == nil {
return i.([]string), nil
}
s, err := c.next.ReadDir(ctx, id)
if err != nil {
return nil, err
}
return s, c.dirs.Set(id, s)
}
// MakeDirIfNotExist invalidates the cache
func (c *CachedMDC) MakeDirIfNotExist(ctx context.Context, id string) error {
err := c.next.MakeDirIfNotExist(ctx, id)
if err != nil {
return err
}
// invalidate caches
_ = c.dirs.Remove(path.Dir(id))
return nil
}
// Init instantiates the caches
func (c *CachedMDC) Init(ctx context.Context, id string) error {
c.dirs = dircache
c.files = filescache
return c.next.Init(ctx, id)
}
func initCache(ttlSeconds int) *ttlcache.Cache {
cache := ttlcache.NewCache()
_ = cache.SetTTL(time.Duration(ttlSeconds) * time.Second)
cache.SkipTTLExtensionOnHit(true)
return cache
}
func removePrefix(cache *ttlcache.Cache, prefix string) error {
for _, k := range cache.GetKeys() {
if strings.HasPrefix(k, prefix) {
if err := cache.Remove(k); err != nil {
return err
}
}
}
return nil
}
@@ -0,0 +1,72 @@
package store
import (
"github.com/owncloud/ocis/extensions/settings/pkg/settings"
"github.com/owncloud/ocis/extensions/settings/pkg/util"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
// ListPermissionsByResource collects all permissions from the provided roleIDs that match the requested resource
func (s *Store) ListPermissionsByResource(resource *settingsmsg.Resource, roleIDs []string) ([]*settingsmsg.Permission, error) {
records := make([]*settingsmsg.Permission, 0)
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
records = append(records, extractPermissionsByResource(resource, role)...)
}
return records, nil
}
// ReadPermissionByID finds the permission in the roles, specified by the provided roleIDs
func (s *Store) ReadPermissionByID(permissionID string, roleIDs []string) (*settingsmsg.Permission, error) {
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
for _, permission := range role.Settings {
if permission.Id == permissionID {
if value, ok := permission.Value.(*settingsmsg.Setting_PermissionValue); ok {
return value.PermissionValue, nil
}
}
}
}
return nil, nil
}
// ReadPermissionByName finds the permission in the roles, specified by the provided roleIDs
func (s *Store) ReadPermissionByName(name string, roleIDs []string) (*settingsmsg.Permission, error) {
for _, roleID := range roleIDs {
role, err := s.ReadBundle(roleID)
if err != nil {
s.Logger.Debug().Str("roleID", roleID).Msg("role not found, skipping")
continue
}
for _, permission := range role.Settings {
if permission.Name == name {
if value, ok := permission.Value.(*settingsmsg.Setting_PermissionValue); ok {
return value.PermissionValue, nil
}
}
}
}
return nil, settings.ErrPermissionNotFound
}
// extractPermissionsByResource collects all permissions from the provided role that match the requested resource
func extractPermissionsByResource(resource *settingsmsg.Resource, role *settingsmsg.Bundle) []*settingsmsg.Permission {
permissions := make([]*settingsmsg.Permission, 0)
for _, setting := range role.Settings {
if value, ok := setting.Value.(*settingsmsg.Setting_PermissionValue); ok {
if util.IsResourceMatched(setting.Resource, resource) {
permissions = append(permissions, value.PermissionValue)
}
}
}
return permissions
}
@@ -0,0 +1,27 @@
package store
import (
"testing"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
"github.com/stretchr/testify/require"
)
func TestPermission(t *testing.T) {
// bunldes are initialized within init func
p, err := s.ReadPermissionByID("readID", []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
require.NoError(t, err)
require.Equal(t, settingsmsg.Permission_OPERATION_READ, p.Operation)
p, err = s.ReadPermissionByName("read", []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
require.NoError(t, err)
require.Equal(t, settingsmsg.Permission_OPERATION_READ, p.Operation)
pms, err := s.ListPermissionsByResource(&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
}, []string{"f36db5e6-a03c-40df-8413-711c67e40b47"})
require.NoError(t, err)
require.Len(t, pms, 1)
require.Equal(t, settingsmsg.Permission_OPERATION_READ, pms[0].Operation)
}
@@ -0,0 +1,155 @@
// Package store implements the go-micro store interface
package store
import (
"context"
"encoding/json"
"log"
"sync"
"github.com/cs3org/reva/v2/pkg/storage/utils/metadata"
"github.com/gofrs/uuid"
"github.com/owncloud/ocis/extensions/settings/pkg/config"
"github.com/owncloud/ocis/extensions/settings/pkg/settings"
"github.com/owncloud/ocis/extensions/settings/pkg/store/defaults"
olog "github.com/owncloud/ocis/ocis-pkg/log"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
var (
// Name is the default name for the settings store
Name = "ocis-settings"
managerName = "metadata"
settingsSpaceID = "f1bdd61a-da7c-49fc-8203-0558109d1b4f" // uuid.Must(uuid.NewV4()).String()
rootFolderLocation = "settings"
bundleFolderLocation = "settings/bundles"
accountsFolderLocation = "settings/accounts"
valuesFolderLocation = "settings/values"
)
// MetadataClient is the interface to talk to metadata service
type MetadataClient interface {
SimpleDownload(ctx context.Context, id string) ([]byte, error)
SimpleUpload(ctx context.Context, id string, content []byte) error
Delete(ctx context.Context, id string) error
ReadDir(ctx context.Context, id string) ([]string, error)
MakeDirIfNotExist(ctx context.Context, id string) error
Init(ctx context.Context, id string) error
}
// Store interacts with the filesystem to manage settings information
type Store struct {
Logger olog.Logger
mdc MetadataClient
cfg *config.Config
l *sync.Mutex
}
// Init initialize the store once, later calls are noops
func (s *Store) Init() {
if s.mdc != nil {
return
}
s.l.Lock()
defer s.l.Unlock()
if s.mdc != nil {
return
}
mdc := &CachedMDC{next: NewMetadataClient(s.cfg.Metadata)}
if err := s.initMetadataClient(mdc); err != nil {
s.Logger.Error().Err(err).Msg("error initializing metadata client")
}
}
// New creates a new store
func New(cfg *config.Config) settings.Manager {
s := Store{
Logger: olog.NewLogger(
olog.Color(cfg.Log.Color),
olog.Pretty(cfg.Log.Pretty),
olog.Level(cfg.Log.Level),
olog.File(cfg.Log.File),
),
cfg: cfg,
l: &sync.Mutex{},
}
return &s
}
// NewMetadataClient returns the MetadataClient
func NewMetadataClient(cfg config.Metadata) MetadataClient {
mdc, err := metadata.NewCS3Storage(cfg.GatewayAddress, cfg.StorageAddress, cfg.ServiceUserID, cfg.ServiceUserIDP, cfg.MachineAuthAPIKey)
if err != nil {
log.Fatal("error connecting to mdc:", err)
}
return mdc
}
// we need to lazy initialize the MetadataClient because metadata service might not be ready
func (s *Store) initMetadataClient(mdc MetadataClient) error {
ctx := context.TODO()
err := mdc.Init(ctx, settingsSpaceID)
if err != nil {
return err
}
for _, p := range []string{
rootFolderLocation,
accountsFolderLocation,
bundleFolderLocation,
valuesFolderLocation,
} {
err = mdc.MakeDirIfNotExist(ctx, p)
if err != nil {
return err
}
}
for _, p := range defaults.GenerateBundlesDefaultRoles() {
b, err := json.Marshal(p)
if err != nil {
return err
}
err = mdc.SimpleUpload(ctx, bundlePath(p.Id), b)
if err != nil {
return err
}
}
for _, p := range defaults.DefaultRoleAssignments() {
accountUUID := p.AccountUuid
roleID := p.RoleId
err = mdc.MakeDirIfNotExist(ctx, accountPath(accountUUID))
if err != nil {
return err
}
ass := &settingsmsg.UserRoleAssignment{
Id: uuid.Must(uuid.NewV4()).String(),
AccountUuid: accountUUID,
RoleId: roleID,
}
b, err := json.Marshal(ass)
if err != nil {
return err
}
err = mdc.SimpleUpload(ctx, assignmentPath(accountUUID, ass.Id), b)
if err != nil {
return err
}
}
s.mdc = mdc
return nil
}
func init() {
settings.Registry[managerName] = New
}
@@ -0,0 +1,110 @@
package store
import (
"context"
"strings"
"github.com/owncloud/ocis/extensions/settings/pkg/config/defaults"
)
const (
// account UUIDs
accountUUID1 = "c4572da7-6142-4383-8fc6-efde3d463036"
//accountUUID2 = "e11f9769-416a-427d-9441-41a0e51391d7"
//accountUUID3 = "633ecd77-1980-412a-8721-bf598a330bb4"
// extension names
extension1 = "test-extension-1"
extension2 = "test-extension-2"
// bundle ids
bundle1 = "2f06addf-4fd2-49d5-8f71-00fbd3a3ec47"
bundle2 = "2d745744-749c-4286-8e92-74a24d8331c5"
bundle3 = "d8fd27d1-c00b-4794-a658-416b756a72ff"
// setting ids
setting1 = "c7ebbc8b-d15a-4f2e-9d7d-d6a4cf858d1a"
setting2 = "3fd9a3d9-20b7-40d4-9294-b22bb5868c10"
setting3 = "24bb9535-3df4-42f1-a622-7c0562bec99f"
// value ids
value1 = "fd3b6221-dc13-4a22-824d-2480495f1cdb"
value2 = "2a0bd9b0-ca1d-491a-8c56-d2ddfd68ded8"
value3 = "b42702d2-5e4d-4d73-b133-e1f9e285355e"
)
// use "unit" or "integration" do define test type. You need a running ocis instance for integration tests
var testtype = "unit"
// MockedMetadataClient mocks the metadataservice inmemory
type MockedMetadataClient struct {
data map[string][]byte
}
// NewMDC instantiates a mocked MetadataClient
func NewMDC(s *Store) error {
var mdc MetadataClient
switch testtype {
case "unit":
mdc = &MockedMetadataClient{data: make(map[string][]byte)}
case "integration":
mdc = NewMetadataClient(defaults.DefaultConfig().Metadata)
}
return s.initMetadataClient(mdc)
}
// SimpleDownload returns nil if not found
func (m *MockedMetadataClient) SimpleDownload(_ context.Context, id string) ([]byte, error) {
return m.data[id], nil
}
// SimpleUpload can't error
func (m *MockedMetadataClient) SimpleUpload(_ context.Context, id string, content []byte) error {
m.data[id] = content
return nil
}
// Delete can't error either
func (m *MockedMetadataClient) Delete(_ context.Context, id string) error {
for k := range m.data {
if strings.HasPrefix(k, id) {
delete(m.data, k)
}
}
return nil
}
// ReadDir returns nil, nil if not found
func (m *MockedMetadataClient) ReadDir(_ context.Context, id string) ([]string, error) {
var out []string
for k := range m.data {
if strings.HasPrefix(k, id) {
dir := strings.TrimPrefix(k, id+"/")
// filter subfolders the lame way
s := strings.Trim(strings.SplitAfter(dir, "/")[0], "/")
out = append(out, s)
}
}
return out, nil
}
// MakeDirIfNotExist does nothing
func (*MockedMetadataClient) MakeDirIfNotExist(_ context.Context, _ string) error {
return nil
}
// Init does nothing
func (*MockedMetadataClient) Init(_ context.Context, _ string) error {
return nil
}
// IDExists is a helper to check if an id exists
func (m *MockedMetadataClient) IDExists(id string) bool {
_, ok := m.data[id]
return ok
}
// IDHasContent returns true if the value stored under id has the given content (converted to string)
func (m *MockedMetadataClient) IDHasContent(id string, content []byte) bool {
return string(m.data[id]) == string(content)
}
@@ -0,0 +1,94 @@
// Package store implements the go-micro store interface
package store
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gofrs/uuid"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
)
// ListValues reads all values that match the given bundleId and accountUUID.
// If the bundleId is empty, it's ignored for filtering.
// If the accountUUID is empty, only values with empty accountUUID are returned.
// If the accountUUID is not empty, values with an empty or with a matching accountUUID are returned.
func (s *Store) ListValues(bundleID, accountUUID string) ([]*settingsmsg.Value, error) {
s.Init()
ctx := context.TODO()
vIDs, err := s.mdc.ReadDir(ctx, valuesFolderLocation)
if err != nil {
return nil, err
}
// TODO: refine logic not to spam metadata service
var values []*settingsmsg.Value
for _, vid := range vIDs {
b, err := s.mdc.SimpleDownload(ctx, valuePath(vid))
if err != nil {
return nil, err
}
v := &settingsmsg.Value{}
err = json.Unmarshal(b, v)
if err != nil {
return nil, err
}
if bundleID != "" && v.BundleId != bundleID {
continue
}
if v.AccountUuid == "" {
values = append(values, v)
continue
}
if v.AccountUuid == accountUUID {
values = append(values, v)
continue
}
}
return values, nil
}
// ReadValue tries to find a value by the given valueId within the dataPath
func (s *Store) ReadValue(valueID string) (*settingsmsg.Value, error) {
s.Init()
ctx := context.TODO()
b, err := s.mdc.SimpleDownload(ctx, valuePath(valueID))
if err != nil {
return nil, err
}
val := &settingsmsg.Value{}
return val, json.Unmarshal(b, val)
}
// ReadValueByUniqueIdentifiers tries to find a value given a set of unique identifiers
func (s *Store) ReadValueByUniqueIdentifiers(accountUUID, settingID string) (*settingsmsg.Value, error) {
fmt.Println("ReadValueByUniqueIdentifiers not implemented")
return nil, errors.New("not implemented")
}
// WriteValue writes the given value into a file within the dataPath
func (s *Store) WriteValue(value *settingsmsg.Value) (*settingsmsg.Value, error) {
s.Init()
ctx := context.TODO()
if value.Id == "" {
value.Id = uuid.Must(uuid.NewV4()).String()
}
b, err := json.Marshal(value)
if err != nil {
return nil, err
}
return value, s.mdc.SimpleUpload(ctx, valuePath(value.Id), b)
}
func valuePath(id string) string {
return fmt.Sprintf("%s/%s", valuesFolderLocation, id)
}
@@ -0,0 +1,100 @@
package store
import (
"testing"
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/settings/v0"
"github.com/stretchr/testify/require"
)
var valueScenarios = []struct {
name string
value *settingsmsg.Value
}{
{
name: "generic-test-with-system-resource",
value: &settingsmsg.Value{
Id: value1,
BundleId: bundle1,
SettingId: setting1,
AccountUuid: accountUUID1,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Value: &settingsmsg.Value_StringValue{
StringValue: "lalala",
},
},
},
{
name: "generic-test-with-file-resource",
value: &settingsmsg.Value{
Id: value2,
BundleId: bundle2,
SettingId: setting2,
AccountUuid: accountUUID1,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_FILE,
Id: "adfba82d-919a-41c3-9cd1-5a3f83b2bf76",
},
Value: &settingsmsg.Value_StringValue{
StringValue: "tralala",
},
},
},
{
name: "value without accountUUID",
value: &settingsmsg.Value{
Id: value3,
BundleId: bundle3,
SettingId: setting2,
AccountUuid: "",
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_FILE,
Id: "adfba82d-919a-41c3-9cd1-5a3f83b2bf76",
},
Value: &settingsmsg.Value_StringValue{
StringValue: "tralala",
},
},
},
}
func TestValues(t *testing.T) {
for i := range valueScenarios {
index := i
t.Run(valueScenarios[index].name, func(t *testing.T) {
value := valueScenarios[index].value
v, err := s.WriteValue(value)
require.NoError(t, err)
require.Equal(t, value, v)
v, err = s.ReadValue(value.Id)
require.NoError(t, err)
require.Equal(t, value, v)
})
}
}
func TestListValues(t *testing.T) {
for _, v := range valueScenarios {
_, err := s.WriteValue(v.value)
require.NoError(t, err)
}
// empty accountid returns only values with empty accountud
vs, err := s.ListValues("", "")
require.NoError(t, err)
require.Len(t, vs, 1)
// filled accountid returns matching and empty accountUUID values
vs, err = s.ListValues("", accountUUID1)
require.NoError(t, err)
require.Len(t, vs, 3)
// filled bundleid only returns matching values
vs, err = s.ListValues(bundle3, accountUUID1)
require.NoError(t, err)
require.Len(t, vs, 1)
}
@@ -0,0 +1,7 @@
package store
import (
// init filesystem store
_ "github.com/owncloud/ocis/extensions/settings/pkg/store/filesystem"
_ "github.com/owncloud/ocis/extensions/settings/pkg/store/metadata"
)
@@ -0,0 +1,23 @@
package tracing
import (
"github.com/owncloud/ocis/extensions/settings/pkg/config"
pkgtrace "github.com/owncloud/ocis/ocis-pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
var (
// TraceProvider is the global trace provider for the settings service.
TraceProvider = trace.NewNoopTracerProvider()
)
func Configure(cfg *config.Config) error {
var err error
if cfg.Tracing.Enabled {
if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil {
return err
}
}
return nil
}
@@ -0,0 +1,18 @@
package util
import (
settingsmsg "github.com/owncloud/ocis/protogen/gen/ocis/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/owncloud/ocis/protogen/gen/ocis/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: "einstein",
},
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "marie",
},
false,
},
{
"same resource types with same ids match",
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "einstein",
},
&settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
Id: "einstein",
},
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: "einstein",
},
true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
assert.Equal(t, scenario.matched, IsResourceMatched(scenario.definition, scenario.example))
})
}
}
+5
View File
@@ -0,0 +1,5 @@
# backend
-r '^(cmd|pkg)/.*\.go$' -R '^node_modules/' -s -- sh -c 'make bin/ocis-settings-debug && bin/ocis-settings-debug --log-level debug server --debug-pprof --debug-zpages --asset-path assets/'
# frontend
-r '^ui/.*\.(vue|js)$' -R '^node_modules/' -- sh -c 'yarn build && make generate'
+52
View File
@@ -0,0 +1,52 @@
import vue from 'rollup-plugin-vue'
import { terser } from 'rollup-plugin-terser'
import replace from '@rollup/plugin-replace'
import filesize from 'rollup-plugin-filesize'
import resolve from 'rollup-plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import babel from 'rollup-plugin-babel'
import json from '@rollup/plugin-json'
import builtins from '@erquhart/rollup-plugin-node-builtins'
import globals from 'rollup-plugin-node-globals'
const production = !process.env.ROLLUP_WATCH
// We can't really do much about circular dependencies in node_modules
function onwarn (warning) {
if (warning.code !== 'CIRCULAR_DEPENDENCY') {
console.error(`(!) ${warning.message}`)
}
}
export default {
input: 'ui/app.js',
output: {
file: 'assets/settings.js',
format: 'amd',
sourcemap: !production
},
onwarn,
plugins: [
vue(),
replace({
'process.env.NODE_ENV': JSON.stringify('production')
}),
resolve({
mainFields: ['browser', 'jsnext', 'module', 'main'],
include: 'node_modules/**',
preferBuiltins: true
}),
babel({
exclude: 'node_modules/**',
runtimeHelpers: true
}),
commonjs({
include: 'node_modules/**'
}),
json(),
globals(),
builtins(),
production && terser(),
production && filesize()
]
}
+9
View File
@@ -0,0 +1,9 @@
package settings
import (
"embed"
)
//go:generate make generate
//go:embed assets/*
var Assets embed.FS
+44
View File
@@ -0,0 +1,44 @@
import 'regenerator-runtime/runtime'
import SettingsApp from './components/SettingsApp.vue'
import store from './store'
import translations from './../l10n/translations.json'
// just a dummy function to trick gettext tools
function $gettext(msg) {
return msg
}
const appInfo = {
name: $gettext('Settings'),
id: 'settings',
icon: 'settings-4',
isFileEditor: false
}
const routes = [
{
name: 'settings',
path: '/:extension?',
component: SettingsApp
}
]
const navItems = [
{
name: $gettext('Settings'),
icon: appInfo.icon,
route: {
name: 'settings',
path: `/${appInfo.id}/`
},
menu: 'user'
}
]
export default {
appInfo,
store,
routes,
navItems,
translations
}
@@ -0,0 +1,719 @@
/* eslint-disable */
import axios from 'axios'
import qs from 'qs'
let domain = ''
export const getDomain = () => {
return domain
}
export const setDomain = ($domain) => {
domain = $domain
}
export const request = (method, url, body, queryParameters, form, config) => {
method = method.toLowerCase()
let keys = Object.keys(queryParameters)
let queryUrl = url
if (keys.length > 0) {
queryUrl = url + '?' + qs.stringify(queryParameters)
}
// let queryUrl = url+(keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
if (body) {
return axios[method](queryUrl, body, config)
} else if (method === 'get') {
return axios[method](queryUrl, config)
} else {
return axios[method](queryUrl, qs.stringify(form), config)
}
}
/*==========================================================
*
==========================================================*/
/**
*
* request: RoleService_AssignRoleToUser
* url: RoleService_AssignRoleToUserURL
* method: RoleService_AssignRoleToUser_TYPE
* raw_url: RoleService_AssignRoleToUser_RAW_URL
* @param body -
*/
export const RoleService_AssignRoleToUser = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/assignments-add'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const RoleService_AssignRoleToUser_RAW_URL = function() {
return '/api/v0/settings/assignments-add'
}
export const RoleService_AssignRoleToUser_TYPE = function() {
return 'post'
}
export const RoleService_AssignRoleToUserURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/assignments-add'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: RoleService_ListRoleAssignments
* url: RoleService_ListRoleAssignmentsURL
* method: RoleService_ListRoleAssignments_TYPE
* raw_url: RoleService_ListRoleAssignments_RAW_URL
* @param body -
*/
export const RoleService_ListRoleAssignments = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/assignments-list'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const RoleService_ListRoleAssignments_RAW_URL = function() {
return '/api/v0/settings/assignments-list'
}
export const RoleService_ListRoleAssignments_TYPE = function() {
return 'post'
}
export const RoleService_ListRoleAssignmentsURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/assignments-list'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: RoleService_RemoveRoleFromUser
* url: RoleService_RemoveRoleFromUserURL
* method: RoleService_RemoveRoleFromUser_TYPE
* raw_url: RoleService_RemoveRoleFromUser_RAW_URL
* @param body -
*/
export const RoleService_RemoveRoleFromUser = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/assignments-remove'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const RoleService_RemoveRoleFromUser_RAW_URL = function() {
return '/api/v0/settings/assignments-remove'
}
export const RoleService_RemoveRoleFromUser_TYPE = function() {
return 'post'
}
export const RoleService_RemoveRoleFromUserURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/assignments-remove'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: BundleService_GetBundle
* url: BundleService_GetBundleURL
* method: BundleService_GetBundle_TYPE
* raw_url: BundleService_GetBundle_RAW_URL
* @param body -
*/
export const BundleService_GetBundle = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/bundle-get'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const BundleService_GetBundle_RAW_URL = function() {
return '/api/v0/settings/bundle-get'
}
export const BundleService_GetBundle_TYPE = function() {
return 'post'
}
export const BundleService_GetBundleURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/bundle-get'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: BundleService_SaveBundle
* url: BundleService_SaveBundleURL
* method: BundleService_SaveBundle_TYPE
* raw_url: BundleService_SaveBundle_RAW_URL
* @param body -
*/
export const BundleService_SaveBundle = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/bundle-save'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const BundleService_SaveBundle_RAW_URL = function() {
return '/api/v0/settings/bundle-save'
}
export const BundleService_SaveBundle_TYPE = function() {
return 'post'
}
export const BundleService_SaveBundleURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/bundle-save'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: BundleService_AddSettingToBundle
* url: BundleService_AddSettingToBundleURL
* method: BundleService_AddSettingToBundle_TYPE
* raw_url: BundleService_AddSettingToBundle_RAW_URL
* @param body -
*/
export const BundleService_AddSettingToBundle = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/bundles-add-setting'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const BundleService_AddSettingToBundle_RAW_URL = function() {
return '/api/v0/settings/bundles-add-setting'
}
export const BundleService_AddSettingToBundle_TYPE = function() {
return 'post'
}
export const BundleService_AddSettingToBundleURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/bundles-add-setting'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: BundleService_ListBundles
* url: BundleService_ListBundlesURL
* method: BundleService_ListBundles_TYPE
* raw_url: BundleService_ListBundles_RAW_URL
* @param body -
*/
export const BundleService_ListBundles = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/bundles-list'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const BundleService_ListBundles_RAW_URL = function() {
return '/api/v0/settings/bundles-list'
}
export const BundleService_ListBundles_TYPE = function() {
return 'post'
}
export const BundleService_ListBundlesURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/bundles-list'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: BundleService_RemoveSettingFromBundle
* url: BundleService_RemoveSettingFromBundleURL
* method: BundleService_RemoveSettingFromBundle_TYPE
* raw_url: BundleService_RemoveSettingFromBundle_RAW_URL
* @param body -
*/
export const BundleService_RemoveSettingFromBundle = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/bundles-remove-setting'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const BundleService_RemoveSettingFromBundle_RAW_URL = function() {
return '/api/v0/settings/bundles-remove-setting'
}
export const BundleService_RemoveSettingFromBundle_TYPE = function() {
return 'post'
}
export const BundleService_RemoveSettingFromBundleURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/bundles-remove-setting'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: PermissionService_GetPermissionByID
* url: PermissionService_GetPermissionByIDURL
* method: PermissionService_GetPermissionByID_TYPE
* raw_url: PermissionService_GetPermissionByID_RAW_URL
* @param body -
*/
export const PermissionService_GetPermissionByID = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/permissions-get-by-id'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const PermissionService_GetPermissionByID_RAW_URL = function() {
return '/api/v0/settings/permissions-get-by-id'
}
export const PermissionService_GetPermissionByID_TYPE = function() {
return 'post'
}
export const PermissionService_GetPermissionByIDURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/permissions-get-by-id'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: PermissionService_ListPermissionsByResource
* url: PermissionService_ListPermissionsByResourceURL
* method: PermissionService_ListPermissionsByResource_TYPE
* raw_url: PermissionService_ListPermissionsByResource_RAW_URL
* @param body -
*/
export const PermissionService_ListPermissionsByResource = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/permissions-list-by-resource'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const PermissionService_ListPermissionsByResource_RAW_URL = function() {
return '/api/v0/settings/permissions-list-by-resource'
}
export const PermissionService_ListPermissionsByResource_TYPE = function() {
return 'post'
}
export const PermissionService_ListPermissionsByResourceURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/permissions-list-by-resource'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: RoleService_ListRoles
* url: RoleService_ListRolesURL
* method: RoleService_ListRoles_TYPE
* raw_url: RoleService_ListRoles_RAW_URL
* @param body -
*/
export const RoleService_ListRoles = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/roles-list'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const RoleService_ListRoles_RAW_URL = function() {
return '/api/v0/settings/roles-list'
}
export const RoleService_ListRoles_TYPE = function() {
return 'post'
}
export const RoleService_ListRolesURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/roles-list'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: ValueService_GetValue
* url: ValueService_GetValueURL
* method: ValueService_GetValue_TYPE
* raw_url: ValueService_GetValue_RAW_URL
* @param body -
*/
export const ValueService_GetValue = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/values-get'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const ValueService_GetValue_RAW_URL = function() {
return '/api/v0/settings/values-get'
}
export const ValueService_GetValue_TYPE = function() {
return 'post'
}
export const ValueService_GetValueURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/values-get'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: ValueService_GetValueByUniqueIdentifiers
* url: ValueService_GetValueByUniqueIdentifiersURL
* method: ValueService_GetValueByUniqueIdentifiers_TYPE
* raw_url: ValueService_GetValueByUniqueIdentifiers_RAW_URL
* @param body -
*/
export const ValueService_GetValueByUniqueIdentifiers = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/values-get-by-unique-identifiers'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const ValueService_GetValueByUniqueIdentifiers_RAW_URL = function() {
return '/api/v0/settings/values-get-by-unique-identifiers'
}
export const ValueService_GetValueByUniqueIdentifiers_TYPE = function() {
return 'post'
}
export const ValueService_GetValueByUniqueIdentifiersURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/values-get-by-unique-identifiers'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: ValueService_ListValues
* url: ValueService_ListValuesURL
* method: ValueService_ListValues_TYPE
* raw_url: ValueService_ListValues_RAW_URL
* @param body -
*/
export const ValueService_ListValues = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/values-list'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const ValueService_ListValues_RAW_URL = function() {
return '/api/v0/settings/values-list'
}
export const ValueService_ListValues_TYPE = function() {
return 'post'
}
export const ValueService_ListValuesURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/values-list'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
/**
*
* request: ValueService_SaveValue
* url: ValueService_SaveValueURL
* method: ValueService_SaveValue_TYPE
* raw_url: ValueService_SaveValue_RAW_URL
* @param body -
*/
export const ValueService_SaveValue = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/settings/values-save'
let body
let queryParameters = {}
let form = {}
if (parameters['body'] !== undefined) {
body = parameters['body']
}
if (parameters['body'] === undefined) {
return Promise.reject(new Error('Missing required parameter: body'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('post', domain + path, body, queryParameters, form, config)
}
export const ValueService_SaveValue_RAW_URL = function() {
return '/api/v0/settings/values-save'
}
export const ValueService_SaveValue_TYPE = function() {
return 'post'
}
export const ValueService_SaveValueURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/settings/values-save'
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
})
}
let keys = Object.keys(queryParameters)
return domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : '')
}
@@ -0,0 +1,158 @@
<template>
<div class="oc-p">
<main class="oc-flex oc-flex-column" id="settings-app">
<template v-if="initialized">
<oc-alert v-if="extensions.length === 0" variation="primary" no-close>
<p class="oc-flex oc-flex-middle">
<oc-icon name="information" class="oc-mr-s" />
<translate>No settings available</translate>
</p>
</oc-alert>
<template v-else>
<template v-if="selectedExtensionName">
<div class="oc-flex oc-flex-between oc-flex-middle">
<h1 class="oc-page-title">
{{ selectedExtensionName }}
</h1>
</div>
<hr />
</template>
<template v-if="settingsValuesLoaded">
<settings-bundle
v-for="bundle in selectedBundles"
:key="'bundle-' + bundle.id"
:bundle="bundle"
class="oc-mt"
/>
</template>
<div class="oc-mt" v-else>
<oc-loader :aria-label="$gettext('Loading personal settings')" />
<oc-alert :aria-hidden="true" variation="primary" no-close>
<p v-translate>Loading personal settings...</p>
</oc-alert>
</div>
</template>
</template>
<oc-loader v-else />
</main>
</div>
</template>
<script>
import { mapActions, mapGetters, mapMutations } from 'vuex'
import SettingsBundle from './SettingsBundle.vue'
export default {
name: 'SettingsApp',
components: { SettingsBundle },
data () {
return {
loading: true,
selectedExtension: undefined
}
},
computed: {
...mapGetters(['settingsValuesLoaded', 'getNavItems']),
...mapGetters('Settings', [
'extensions',
'initialized',
'getBundlesByExtension'
]),
extensionRouteParam () {
return this.$route.params.extension
},
selectedExtensionName () {
return this.getExtensionName(this.selectedExtension)
},
selectedBundles () {
if (this.selectedExtension) {
return this.getBundlesByExtension(this.selectedExtension)
}
return []
}
},
methods: {
...mapActions('Settings', ['initialize']),
...mapMutations(['ADD_NAV_ITEM']),
resetSelectedExtension () {
if (this.extensions.length > 0) {
if (
this.extensionRouteParam &&
this.extensions.includes(this.extensionRouteParam)
) {
this.selectedExtension = this.extensionRouteParam
} else {
this.selectedExtension = this.extensions[0]
}
}
},
resetMenuItems () {
this.extensions.forEach((extension) => {
/*
* TODO:
* a) set up a map with possible extensions and icons?
* or b) let extensions register app info like displayName + icon?
* https://github.com/owncloud/ocis/settings/issues/27
*/
const navItem = {
name: this.getExtensionName(extension),
icon: this.getExtensionIcon(extension),
route: {
name: 'settings',
path: `/settings/${extension}`
},
menu: 'user'
}
this.ADD_NAV_ITEM({
extension: 'settings',
navItem
})
})
},
getExtensionName (extension) {
extension = extension || ''
switch (extension) {
case 'ocis-accounts':
return this.$gettext('Account')
case 'ocis-hello':
return this.$gettext('Hello')
default: {
const shortenedName = extension.replace('ocis-', '')
return shortenedName.charAt(0).toUpperCase() + shortenedName.slice(1)
}
}
},
getExtensionIcon (extension) {
extension = extension || ''
switch (extension) {
case 'ocis-accounts':
return 'team'
case 'ocis-hello':
return 'tag_faces'
default:
return 'application'
}
}
},
created () {
this.initialize()
},
watch: {
'$language.current': {
handler () {
this.resetMenuItems()
}
},
initialized: {
handler () {
this.resetMenuItems()
this.resetSelectedExtension()
},
immediate: true
},
extensionRouteParam () {
this.resetSelectedExtension()
}
}
}
</script>
@@ -0,0 +1,80 @@
<template>
<div class="oc-width-1-1 oc-width-2-3@m oc-width-1-2@l oc-width-1-3@xl">
<h2 class="oc-mb-s">
<translate>{{ bundle.displayName }}</translate>
</h2>
<oc-grid gutter="small">
<template>
<div
class="oc-width-1-1"
v-for="setting in bundle.settings"
:key="setting.id"
>
<label class="oc-label" :for="setting.id">{{
setting.displayName
}}</label>
<div
class="oc-position-relative"
:is="getSettingComponent(setting)"
:id="setting.id"
:bundle="bundle"
:setting="setting"
:persisted-value="getValue(setting)"
@onSave="onSaveValue"
/>
</div>
</template>
</oc-grid>
</div>
</template>
<script>
import assign from 'lodash-es/assign'
import { mapGetters, mapActions } from 'vuex'
import SettingBoolean from './settings/SettingBoolean.vue'
import SettingMultiChoice from './settings/SettingMultiChoice.vue'
import SettingNumber from './settings/SettingNumber.vue'
import SettingSingleChoice from './settings/SettingSingleChoice.vue'
import SettingString from './settings/SettingString.vue'
import SettingUnknown from './settings/SettingUnknown.vue'
export default {
name: 'SettingsBundle',
props: {
bundle: {
type: Object,
required: true
}
},
computed: mapGetters(['getSettingsValue']),
methods: {
...mapActions('Settings', ['saveValue']),
getSettingComponent (setting) {
return 'Setting' + setting.type[0].toUpperCase() + setting.type.slice(1)
},
getValue (setting) {
return this.getSettingsValue({ settingId: setting.id })
},
async onSaveValue ({ bundle, setting, payload }) {
payload = assign({}, payload, {
bundleId: bundle.id,
settingId: setting.id,
accountUuid: 'me',
resource: setting.resource
})
await this.saveValue({
bundle,
setting,
payload
})
}
},
components: {
SettingBoolean,
SettingMultiChoice,
SettingNumber,
SettingSingleChoice,
SettingString,
SettingUnknown
}
}
</script>
@@ -0,0 +1,55 @@
<template>
<div>
<oc-checkbox v-model="value" :label="setting.boolValue.label" @change="applyValue" />
</div>
</template>
<script>
import isNil from 'lodash-es/isNil'
export default {
name: 'SettingBoolean',
props: {
bundle: {
type: Object,
required: true
},
setting: {
type: Object,
required: true
},
persistedValue: {
type: Object,
required: false
}
},
data () {
return {
value: null
}
},
methods: {
async applyValue () {
const payload = {
boolValue: this.value
}
if (!isNil(this.persistedValue)) {
payload.id = this.persistedValue.id
}
await this.$emit('onSave', {
bundle: this.bundle,
setting: this.setting,
payload
})
// TODO: show a spinner while the request for saving the value is running!
}
},
mounted () {
if (!isNil(this.persistedValue)) {
this.value = this.persistedValue.boolValue
}
if (isNil(this.value) && !isNil(this.setting.boolValue.default)) {
this.value = this.setting.boolValue.default
}
}
}
</script>
@@ -0,0 +1,106 @@
<template>
<oc-select
v-model="selectedOptions"
:clearable="false"
:options="displayOptions"
@input="onSelectedOption"
multiple
/>
</template>
<script>
import isNil from 'lodash-es/isNil'
export default {
name: 'SettingMultiChoice',
props: {
bundle: {
type: Object,
required: true
},
setting: {
type: Object,
required: true
},
persistedValue: {
type: Object,
required: false
}
},
data () {
return {
selectedOptions: []
}
},
computed: {
displayOptions () {
return this.setting.multiChoiceValue.options.map(val => val.displayValue)
}
},
methods: {
async onSelectedOption () {
const values = []
if (!isNil(this.selectedOptions)) {
this.selectedOptions.forEach(displayValue => {
const option = this.setting.multiChoiceValue.options.find(val => val.displayValue === displayValue)
if (option.value.intValue) {
values.push({ intValue: option.value.intValue })
}
if (option.value.stringValue) {
values.push({ stringValue: option.value.stringValue })
}
})
}
const payload = {
listValue: {
values
}
}
if (!isNil(this.persistedValue)) {
payload.id = this.persistedValue.id
}
await this.$emit('onSave', {
bundle: this.bundle,
setting: this.setting,
payload
})
// TODO: show a spinner while the request for saving the value is running!
}
},
mounted () {
if (!isNil(this.persistedValue) && !isNil(this.persistedValue.listValue)) {
const selectedValues = []
if (this.persistedValue.listValue.values) {
this.persistedValue.listValue.values.forEach(value => {
if (value.intValue) {
selectedValues.push(value.intValue)
}
if (value.stringValue) {
selectedValues.push(value.stringValue)
}
})
}
if (selectedValues.length === 0) {
this.selectedOptions = []
} else {
this.selectedOptions = this.setting.multiChoiceValue.options.filter(option => {
if (option.value.intValue) {
return selectedValues.includes(option.value.intValue)
}
if (option.value.stringValue) {
return selectedValues.includes(option.value.stringValue)
}
return false
}).map(val => val.displayValue)
}
}
// TODO: load the settings value of the authenticated user and set it in `selectedOptions`
// if not set, yet, apply defaults from settings bundle definition
if (this.selectedOptions === null) {
this.selectedOptions = this.setting.multiChoiceValue.options
.filter(option => option.default)
.map(val => val.displayValue)
}
}
}
</script>
@@ -0,0 +1,97 @@
<template>
<oc-grid flex>
<div class="oc-width-expand">
<oc-text-input
type="number"
v-model="value"
v-bind="inputAttributes"
:placeholder="setting.intValue.placeholder"
:label="setting.description"
@keydown.enter="applyValue"
@keydown.esc="cancel"
/>
</div>
<div v-if="isChanged">
<oc-button @click="cancel" class="oc-ml-s">
<translate>Cancel</translate>
</oc-button>
<oc-button @click="applyValue" class="oc-ml-s" variation="primary">
<translate>Save</translate>
</oc-button>
</div>
</oc-grid>
</template>
<script>
import isNil from 'lodash-es/isNil'
export default {
name: 'SettingNumber',
props: {
bundle: {
type: Object,
required: true
},
setting: {
type: Object,
required: true
},
persistedValue: {
type: Object,
required: false
}
},
data () {
return {
initialValue: null,
value: null
}
},
computed: {
isChanged () {
return this.initialValue !== this.value
},
inputAttributes () {
const attributes = {}
if (!isNil(this.setting.intValue.min)) {
attributes.min = this.setting.intValue.min
}
if (!isNil(this.setting.intValue.max)) {
attributes.max = this.setting.intValue.max
}
if (!isNil(this.setting.intValue.step)) {
attributes.step = this.setting.intValue.step
}
return attributes
}
},
methods: {
cancel () {
this.value = this.initialValue
},
async applyValue () {
const payload = {
intValue: this.value
}
if (!isNil(this.persistedValue)) {
payload.id = this.persistedValue.id
}
await this.$emit('onSave', {
bundle: this.bundle,
setting: this.setting,
payload
})
// TODO: show a spinner while the request for saving the value is running!
this.initialValue = this.value
}
},
mounted () {
if (!isNil(this.persistedValue)) {
this.value = this.persistedValue.intValue
}
if (isNil(this.value) && !isNil(this.setting.intValue.default)) {
this.value = this.setting.intValue.default
}
this.initialValue = this.value
}
}
</script>
@@ -0,0 +1,92 @@
<template>
<div>
<oc-select
v-model="selectedOption"
:clearable="false"
:options="displayOptions"
@input="onSelectedOption"
/>
</div>
</template>
<script>
import isNil from 'lodash-es/isNil'
export default {
name: 'SettingSingleChoice',
props: {
bundle: {
type: Object,
required: true
},
setting: {
type: Object,
required: true
},
persistedValue: {
type: Object,
required: false
}
},
data () {
return {
selectedOption: null
}
},
computed: {
displayOptions () {
return this.setting.singleChoiceValue.options.map(val => val.displayValue)
}
},
methods: {
async onSelectedOption () {
const values = []
if (!isNil(this.selectedOption)) {
const option = this.setting.singleChoiceValue.options.find(val => val.displayValue === this.selectedOption)
if (option.value.intValue) {
values.push({ intValue: option.value.intValue })
}
if (option.value.stringValue) {
values.push({ stringValue: option.value.stringValue })
}
}
const payload = {
listValue: {
values
}
}
if (!isNil(this.persistedValue)) {
payload.id = this.persistedValue.id
}
await this.$emit('onSave', {
bundle: this.bundle,
setting: this.setting,
payload
})
// TODO: show a spinner while the request for saving the value is running!
}
},
mounted () {
if (!isNil(this.persistedValue) && !isNil(this.persistedValue.listValue)) {
const selected = this.persistedValue.listValue.values[0]
const filtered = this.setting.singleChoiceValue.options.filter(option => {
if (selected.intValue) {
return option.value.intValue === selected.intValue
} else {
return option.value.stringValue === selected.stringValue
}
})
if (filtered.length > 0) {
this.selectedOption = filtered[0].displayValue
}
}
// if not set, yet, apply default from settings bundle definition
if (isNil(this.selectedOption)) {
const defaults = this.setting.singleChoiceValue.options.filter(option => option.default)
if (defaults.length === 1) {
this.selectedOption = defaults[0].displayValue
}
}
}
}
</script>
@@ -0,0 +1,82 @@
<template>
<oc-grid flex>
<div class="oc-width-expand">
<oc-text-input
v-model="value"
:placeholder="setting.stringValue.placeholder"
:label="setting.description"
@keydown.enter="applyValue"
@keydown.esc="cancel"
/>
</div>
<div v-if="isChanged">
<oc-button @click="cancel" class="oc-ml-s">
<translate>Cancel</translate>
</oc-button>
<oc-button @click="applyValue" class="oc-ml-s" variation="primary">
<translate>Save</translate>
</oc-button>
</div>
</oc-grid>
</template>
<script>
import isNil from 'lodash-es/isNil'
export default {
name: 'SettingString',
props: {
bundle: {
type: Object,
required: true
},
setting: {
type: Object,
required: true
},
persistedValue: {
type: Object,
required: false
}
},
data () {
return {
initialValue: null,
value: null
}
},
computed: {
isChanged () {
return this.initialValue !== this.value
}
},
methods: {
async applyValue () {
const payload = {
stringValue: this.value
}
if (!isNil(this.persistedValue)) {
payload.id = this.persistedValue.id
}
await this.$emit('onSave', {
bundle: this.bundle,
setting: this.setting,
payload
})
// TODO: show a spinner while the request for saving the value is running!
this.initialValue = this.value
},
cancel () {
this.value = this.initialValue
}
},
mounted () {
if (!isNil(this.persistedValue)) {
this.value = this.persistedValue.stringValue
}
if (isNil(this.value) && !isNil(this.setting.stringValue.default)) {
this.value = this.setting.stringValue.default
}
this.initialValue = this.value
}
}
</script>
@@ -0,0 +1,25 @@
<template>
<div>
<translate :translate-params="{ type: setting.type }">Settings type not implemented: %{type}</translate>
</div>
</template>
<script>
export default {
name: 'SettingUnknown',
props: {
bundle: {
type: Object,
required: true
},
setting: {
type: Object,
required: true
},
persistedValue: {
type: Object,
required: false
}
}
}
</script>
+14
View File
@@ -0,0 +1,14 @@
/**
* This file contains strings that should be synced to transifex but not exist in the UI directly,
* moreover, they get loaded for example by API requests
*/
// just a dummy function to trick gettext tools
function $gettext (msg) {
return msg
}
// eslint-disable-next-line no-unused-vars
const dictionary = [
$gettext('Language')
]
+125
View File
@@ -0,0 +1,125 @@
import {
// eslint-disable-next-line camelcase
BundleService_ListBundles,
// eslint-disable-next-line camelcase
ValueService_SaveValue
} from '../client/settings'
import axios from 'axios'
import keyBy from 'lodash-es/keyBy'
const state = {
initialized: false,
bundles: {}
}
const getters = {
initialized: state => state.initialized,
extensions: state => {
return [...new Set(Object.values(state.bundles).map(bundle => bundle.extension))].sort()
},
getBundlesByExtension: state => extension => {
return Object.values(state.bundles)
.filter(bundle => bundle.extension === extension)
.sort((b1, b2) => {
return b1.name.localeCompare(b2.name)
})
},
getServerForJsClient: (state, getters, rootState, rootGetters) => rootGetters.configuration.server.replace(/\/$/, '')
}
const mutations = {
SET_INITIALIZED (state, value) {
state.initialized = value
},
SET_BUNDLES (state, bundles) {
state.bundles = keyBy(bundles, 'id')
}
}
const actions = {
async initialize ({ commit, dispatch }) {
await dispatch('fetchBundles')
commit('SET_INITIALIZED', true)
},
async fetchBundles ({ commit, dispatch, getters, rootGetters }) {
injectAuthToken(rootGetters)
try {
const response = await BundleService_ListBundles({
$domain: getters.getServerForJsClient,
body: {}
})
if (response.status === 201) {
// the settings markup has implicit typing. inject an explicit type variable here
const bundles = response.data.bundles
if (bundles) {
bundles.forEach(bundle => {
bundle.settings.forEach(setting => {
if (setting.intValue) {
setting.type = 'number'
} else if (setting.stringValue) {
setting.type = 'string'
} else if (setting.boolValue) {
setting.type = 'boolean'
} else if (setting.singleChoiceValue) {
setting.type = 'singleChoice'
} else if (setting.multiChoiceValue) {
setting.type = 'multiChoice'
} else {
setting.type = 'unknown'
}
})
})
commit('SET_BUNDLES', bundles)
} else {
commit('SET_BUNDLES', [])
}
}
} catch (err) {
dispatch('showMessage', {
title: 'Failed to fetch bundles.',
status: 'danger'
}, { root: true })
}
},
async saveValue ({ commit, dispatch, getters, rootGetters }, { setting, payload }) {
injectAuthToken(rootGetters)
try {
const response = await ValueService_SaveValue({
$domain: getters.getServerForJsClient,
body: {
value: payload
}
})
if (response.status === 201 && response.data.value) {
commit('SET_SETTINGS_VALUE', response.data.value, { root: true })
}
} catch (e) {
dispatch('showMessage', {
title: `Failed to save »${setting.displayName}«.`,
status: 'danger'
}, { root: true })
}
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
function injectAuthToken (rootGetters) {
axios.interceptors.request.use(config => {
if (typeof config.headers.Authorization === 'undefined') {
const token = rootGetters.user.token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
}
return config
})
}
@@ -0,0 +1,46 @@
Feature: Set user specific settings
As a user
I want to set user specific settings
So that I can customize my OCIS experience to my liking
Background:
Given these users have been created with default attributes and without skeleton files in the server:
| username |
| user1 |
| user2 |
And user "user1" has created folder "simple-folder" in the server
Scenario: Check the default settings
Given user "user1" has logged in using the webUI
And the user browses to the settings page
Then the setting "Language" should have value "English"
When the user browses to the files page
Then the files menu should be listed in language "English"
Scenario: changing the language (reactive and with page reload)
Given user "user1" has logged in using the webUI
And the user browses to the settings page
When the user changes the language to "Deutsch"
Then the setting "Language" should have value "Deutsch"
When the user browses to the files page
Then the files menu should be listed in language "Deutsch"
And the account menu should be listed in language "Deutsch"
And the files header should be displayed in language "Deutsch"
When the user reloads the current page of the webUI
Then the files menu should be listed in language "Deutsch"
And the account menu should be listed in language "Deutsch"
And the files header should be displayed in language "Deutsch"
When the user browses to the settings page
And the user changes the language to "English"
And the user browses to the files page
Then the files menu should be listed in language "English"
Scenario: changing the language only affects one user
Given user "user2" has logged in using the webUI
And the user browses to the settings page
When the user changes the language to "Español"
Then the setting "Language" should have value "Español"
When the user browses to the files page
Then the files menu should be listed in language "Español"
When the user re-logs in as "user1" using the webUI
Then the files menu should be listed in language "English"
@@ -0,0 +1,108 @@
const filesMenu = {
English: [
'Personal',
'Shares',
'Spaces\nbeta',
'Deleted files'
],
Deutsch: [
'Persönlich',
'Geteilt',
'Spaces\nbeta',
'Gelöschte Dateien'
],
Español: [
'Personal',
'Shares',
'Spaces\nbeta',
'Archivos borrados'
],
Français: [
'Personal',
'Shares',
'Spaces\nbeta',
'Fichiers supprimés'
]
}
const accountMenu = {
English: [
'N\nnull\nuser1@example.com',
'Settings',
'Log out',
'Personal storage (0.2% used)\n5.06 GB of 2.85 TB used'
],
Deutsch: [
'N\nnull\nuser1@example.com',
'Einstellungen',
'Abmelden',
'Persönlicher Speicher (0.2% benutzt)\n5.06 GB von 2.85 TB benutzt'
],
Español: [
'N\nnull\nuser1@example.com',
'Configuración',
'Salir',
'Personal storage (0.2% used)\n5.06 GB of 2.85 TB used'
],
Français: [
'N\nnull\nuser1@example.com',
'Settings',
'Se déconnecter',
'Personal storage (0.2% used)\n5.06 GB of 2.85 TB used'
]
}
const filesListHeaderMenu = {
English: [
'Name',
'Shares',
'Size',
'Modified',
'Actions'
],
Deutsch: [
'Name',
'Geteilt',
'Größe',
'Bearbeitet',
'Aktionen'
],
Español: [
'Nombre',
'Shares',
'Tamaño',
'Modificado',
'Acciones'
],
Français: [
'Nom',
'Shares',
'Taille',
'Modifié',
'Actions'
]
}
exports.getFilesMenuForLanguage = function (language) {
const menuList = filesMenu[language]
if (menuList === undefined) {
throw new Error(`Menu for language ${language} is not available`)
}
return menuList
}
exports.getUserMenuForLanguage = function (language) {
const menuList = accountMenu[language]
if (menuList === undefined) {
throw new Error(`Menu for language ${language} is not available`)
}
return menuList
}
exports.getFilesHeaderMenuForLanguage = function (language) {
const menuList = filesListHeaderMenu[language]
if (menuList === undefined) {
throw new Error(`Menu for language ${language} is not available`)
}
return menuList
}
@@ -0,0 +1,79 @@
module.exports = {
commands: {
getMenuList: async function () {
const menu = []
await this.isVisible('@openNavigationBtn', (res) => {
if (res.value) {
this.click('@openNavigationBtn')
}
})
await this.waitForElementVisible('@fileSidebarNavItem')
await this.api
.elements('@fileSidebarNavItem', result => {
result.value.map(item => {
this.api.elementIdText(item.ELEMENT, res => {
menu.push(res.value)
})
})
})
return menu
},
getUserMenu: async function () {
const menu = []
await this
.waitForElementVisible('@userMenuBtn')
.click('@userMenuBtn')
.waitForElementVisible('@userMenuContainer')
await this.api
.elements('@userMenuItem', result => {
result.value.map(item => {
this.api.elementIdText(item.ELEMENT, res => {
menu.push(res.value)
})
})
})
await this
.click('@userMenuBtn')
.waitForElementNotPresent('@userMenuContainer')
return menu
},
getFileHeaderItems: async function () {
const menu = []
await this.waitForElementVisible('@fileTableHeaderItems')
await this.api
.elements('@fileTableHeaderItems', result => {
result.value.map(item => {
this.api.elementIdText(item.ELEMENT, res => {
menu.push(res.value)
})
})
})
return menu
}
},
elements: {
pageHeader: {
selector: '.oc-page-title'
},
fileSidebarNavItem: {
selector: '.oc-sidebar-nav-item'
},
openNavigationBtn: {
selector: '.oc-app-navigation-toggle'
},
userMenuBtn: {
selector: '#_userMenuButton'
},
userMenuItem: {
selector: '#account-info-container li'
},
userMenuContainer: {
selector: '#account-info-container'
},
fileTableHeaderItems: {
selector: '//*[@id="files-personal-table"]//th[not(.//div)]',
locateStrategy: 'xpath'
}
}
}
@@ -0,0 +1,82 @@
const { client } = require('nightwatch-api')
module.exports = {
url: function () {
return this.api.launchUrl + '/settings'
},
commands: {
navigateAndWaitTillLoaded: async function () {
const url = this.url()
await await this.navigate(url)
while (true) {
let found = false
await this.waitForElementVisible('@pageHeader', 2000, 500, false)
await this.api
.elements('@pageHeader', result => {
if (result.value.length) {
found = true
}
})
if (found) {
break
}
await client.refresh()
}
return this.waitForElementVisible('@pageHeader')
},
getSettingsValue: async function (key) {
let output
switch (key) {
case 'Language':
let elemfound = true
// Language value is set to empty at beginning
// In that case just return false
await this.api.element('@languageValue', result => {
if (result.status < 0) {
elemfound = false
}
})
if (!elemfound) {
output = false
break
}
await this.waitForElementVisible('@languageValue')
.getText('@languageValue', (result) => {
output = result.value
})
break
default:
throw new Error('failed to find the setting')
}
return output
},
changeSettings: async function (key, value) {
switch (key) {
case 'Language':
await this
.waitForElementVisible('@languageInput')
.setValue('@languageInput', value + '\n')
break
default:
throw new Error('failed to find the setting')
}
}
},
elements: {
pageHeader: {
selector: '.oc-page-title'
},
languageValue: {
selector: "//label[.='Language']/..//span[@class='vs__selected']",
locateStrategy: 'xpath'
},
languageInput: {
selector: "//label[.='Language']/..//input",
locateStrategy: 'xpath'
}
}
}
@@ -0,0 +1,40 @@
const assert = require('assert')
const { client } = require('nightwatch-api')
const { Given, When, Then } = require('@cucumber/cucumber')
const languageHelper = require('../helpers/language')
Given('the user browses to the settings page', function () {
return client.page.settingsPage().navigateAndWaitTillLoaded()
})
Then('the setting {string} should have value {string}', async function (setting, result) {
const actual = await client.page.settingsPage().getSettingsValue(setting)
assert.strictEqual(actual, result, 'The setting value doesnt matches to ' + result)
})
Then('the setting {string} should not have any value', async function (setting) {
const actual = await client.page.settingsPage().getSettingsValue(setting)
assert.strictEqual(actual, false, 'The setting value was expected not to be present but was')
})
When('the user changes the language to {string}', async function (value) {
await client.page.settingsPage().changeSettings('Language', value)
})
Then('the files menu should be listed in language {string}', async function (language) {
const menu = await client.page.filesPageSettingsContext().getMenuList()
const expected = languageHelper.getFilesMenuForLanguage(language)
assert.deepStrictEqual(menu, expected, 'the menu list were not same')
})
Then('the account menu should be listed in language {string}', async function (language) {
const menu = await client.page.filesPageSettingsContext().getUserMenu()
const expected = languageHelper.getUserMenuForLanguage(language)
assert.deepStrictEqual(menu, expected, 'the menu list were not same')
})
Then('the files header should be displayed in language {string}', async function (language) {
const items = await client.page.filesPageSettingsContext().getFileHeaderItems()
const expected = languageHelper.getFilesHeaderMenuForLanguage(language)
assert.deepStrictEqual(items, expected, 'the menu list were not same')
})
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
if [ -z "$WEB_PATH" ]
then
echo "WEB_PATH env variable is not set, cannot find files for tests infrastructure"
exit 1
fi
if [ -z "$WEB_UI_CONFIG" ]
then
echo "WEB_UI_CONFIG env variable is not set, cannot find web config file"
exit 1
fi
if [ -z "$1" ]
then
echo "Features path not given, exiting test run"
exit 1
fi
trap clean_up SIGHUP SIGINT SIGTERM
if [ -z "$TEST_INFRA_DIRECTORY" ]
then
cleanup=true
testFolder=$(mktemp -d -p .)
printf "creating folder $testFolder for Test infrastructure setup\n\n"
export TEST_INFRA_DIRECTORY=$(realpath $testFolder)
fi
clean_up() {
if $cleanup
then
if [ -d "$testFolder" ]; then
printf "\n\n\n\nDeleting folder $testFolder Test infrastructure setup..."
rm -rf "$testFolder"
fi
fi
}
trap clean_up SIGHUP SIGINT SIGTERM EXIT
cp -r $(ls -d "$WEB_PATH"/tests/acceptance/* | grep -v 'node_modules') "$testFolder"
export SERVER_HOST=${SERVER_HOST:-https://localhost:9200}
export BACKEND_HOST=${BACKEND_HOST:-https://localhost:9200}
export TEST_TAGS=${TEST_TAGS:-"not @skip"}
yarn run acceptance-tests "$1"
status=$?
exit $status