Remove "accounts" service

This commit is contained in:
Ralf Haferkamp
2022-05-04 15:48:48 +02:00
committed by Ralf Haferkamp
parent 5ba1b8f2c1
commit d25aa7b20f
123 changed files with 80 additions and 28807 deletions

View File

@@ -43,7 +43,6 @@ DEFAULT_NODEJS_VERSION = "14"
config = {
"modules": [
# if you add a module here please also add it to the root level Makefile
"extensions/accounts",
"extensions/app-provider",
"extensions/app-registry",
"extensions/audit",
@@ -96,10 +95,6 @@ config = {
"skipExceptParts": [],
"earlyFail": True,
},
"accountsUITests": {
"skip": True,
"earlyFail": True,
},
"settingsUITests": {
"skip": True,
"earlyFail": True,
@@ -303,9 +298,6 @@ def testPipelines(ctx):
if "skip" not in config["uiTests"] or not config["uiTests"]["skip"]:
pipelines += uiTests(ctx)
if "skip" not in config["accountsUITests"] or not config["accountsUITests"]["skip"]:
pipelines.append(accountsUITests(ctx))
if "skip" not in config["settingsUITests"] or not config["settingsUITests"]["skip"]:
pipelines.append(settingsUITests(ctx))
@@ -753,70 +745,6 @@ def uiTestPipeline(ctx, filterTags, early_fail, runPart = 1, numberOfParts = 1,
},
}
def accountsUITests(ctx, storage = "ocis", accounts_hash_difficulty = 4):
early_fail = config["accountsUITests"]["earlyFail"] if "earlyFail" in config["accountsUITests"] else False
return {
"kind": "pipeline",
"type": "docker",
"name": "accountsUITests",
"platform": {
"os": "linux",
"arch": "amd64",
},
"steps": skipIfUnchanged(ctx, "acceptance-tests") + restoreBuildArtifactCache(ctx, "ocis-binary-amd64", "ocis/bin/ocis") +
ocisServer(storage, accounts_hash_difficulty, [stepVolumeOC10Tests]) + waitForSeleniumService() + waitForMiddlewareService() + [
{
"name": "WebUIAcceptanceTests",
"image": OC_CI_NODEJS % DEFAULT_NODEJS_VERSION,
"environment": {
"SERVER_HOST": "https://ocis-server:9200",
"BACKEND_HOST": "https://ocis-server:9200",
"RUN_ON_OCIS": "true",
"OCIS_REVA_DATA_ROOT": "/srv/app/tmp/ocis/owncloud/data",
"WEB_UI_CONFIG": "/drone/src/tests/config/drone/ocis-config.json",
"TEST_TAGS": "not @skipOnOCIS and not @skip",
"LOCAL_UPLOAD_DIR": "/uploads",
"NODE_TLS_REJECT_UNAUTHORIZED": 0,
"WEB_PATH": "/srv/app/web",
"FEATURE_PATH": "/drone/src/extensions/accounts/ui/tests/acceptance/features",
"MIDDLEWARE_HOST": "http://middleware:3000",
},
"commands": [
". /drone/src/.drone.env",
# we need to have Web around for some general step definitions (eg. how to log in)
"git clone -b $WEB_BRANCH --single-branch --no-tags https://github.com/owncloud/web.git /srv/app/web",
"cd /srv/app/web",
"git checkout $WEB_COMMITID",
# TODO: settings/package.json has all the acceptance test dependencies
# they shouldn't be needed since we could also use them from web:/tests/acceptance/package.json
"cd /drone/src/extensions/accounts",
"yarn install --immutable",
"make test-acceptance-webui",
],
"volumes": [stepVolumeOC10Tests] +
[{
"name": "uploads",
"path": "/uploads",
}],
},
] + failEarly(ctx, early_fail),
"services": selenium() + middlewareService(),
"volumes": [stepVolumeOC10Tests] +
[{
"name": "uploads",
"temp": {},
}],
"depends_on": getPipelineNames([buildOcisBinaryForTesting(ctx)]),
"trigger": {
"ref": [
"refs/heads/master",
"refs/tags/v*",
"refs/pull/**",
],
},
}
def settingsUITests(ctx, storage = "ocis", accounts_hash_difficulty = 4):
early_fail = config["settingsUITests"]["earlyFail"] if "earlyFail" in config["settingsUITests"] else False

2
.vscode/launch.json vendored
View File

@@ -22,7 +22,7 @@
// demo users
"IDM_CREATE_DEMO_USERS": "true",
// OCIS_RUN_EXTENSIONS allows to start a subset of extensions even in the supervised mode
//"OCIS_RUN_EXTENSIONS": "settings,storage-system,graph,graph-explorer,idp,idm,ocs,store,thumbnails,web,webdav,frontend,gateway,users,groups,auth-basic,auth-bearer,storage-authmachine,storage-users,storage-shares,storage-publiclink,app-provider,sharing,accounts,proxy,ocdav",
//"OCIS_RUN_EXTENSIONS": "settings,storage-system,graph,graph-explorer,idp,idm,ocs,store,thumbnails,web,webdav,frontend,gateway,users,groups,auth-basic,auth-bearer,storage-authmachine,storage-users,storage-shares,storage-publiclink,app-provider,sharing,proxy,ocdav",
/*
* Keep secrets and passwords in one block to allow easy uncommenting

View File

@@ -16,7 +16,6 @@ L10N_MODULES := $(shell find . -path '*.tx*' -name 'config' | sed 's|/[^/]*$$||'
# if you add a module here please also add it to the .drone.star file
OCIS_MODULES = \
extensions/accounts \
extensions/app-provider \
extensions/app-registry \
extensions/audit \

View File

@@ -1 +0,0 @@
grpc.md

View File

@@ -1,20 +0,0 @@
---
title: Accounts
date: 2018-05-02T00:00:00+00:00
weight: 20
geekdocRepo: https://github.com/owncloud/ocis
geekdocEditPath: edit/master/docs/extensions/accounts
geekdocFilePath: _index.md
geekdocCollapseSection: true
---
## Abstract
oCIS needs to be able to identify users. Without a non reassignable and persistent account ID share metadata cannot be reliably persisted. `accounts` allows exchanging oidc claims for a uuid. Using a uuid allows users to change the login, mail or even openid connect provider without breaking any persisted metadata that might have been attached to it.
- persists accounts
- uses graph api properties
- ldap can be synced using the onpremise* attributes
## Table of Contents
{{< toc-tree >}}

View File

@@ -1,15 +0,0 @@
---
title: Service Configuration
date: 2018-05-02T00:00:00+00:00
weight: 20
geekdocRepo: https://github.com/owncloud/ocis
geekdocEditPath: edit/master/docs/extensions/accounts
geekdocFilePath: configuration.md
geekdocCollapseSection: true
---
## Example YAML Config
{{< include file="extensions/_includes/accounts-config-example.yaml" language="yaml" >}}
{{< include file="extensions/_includes/accounts_configvars.md" >}}

View File

@@ -1,21 +0,0 @@
---
title: "Releasing"
weight: 70
geekdocRepo: https://github.com/owncloud/ocis
geekdocEditPath: edit/master/docs/extensions/accounts
geekdocFilePath: releasing.md
---
{{< toc >}}
## Requirements
You need a working installation of [the Go programming language](https://golang.org/), [the Node runtime](https://nodejs.org/) and [the Yarn package manager](https://yarnpkg.com/) installed to build the assets for a working release.
## Releasing
The accounts service doesn't have a dedicated release process. Simply commit your changes, make sure linting and unit tests pass locally and open a pull request.
### Package Hierarchy
- [ocis](https://github.com/owncloud/ocis)
- [ocis-accounts](https://github.com/owncloud/ocis/tree/master/accounts)

View File

@@ -1,85 +0,0 @@
---
title: "Tests"
weight: 80
geekdocRepo: https://github.com/owncloud/ocis
geekdocEditPath: edit/master/docs/extensions/accounts
geekdocFilePath: tests.md
---
{{< toc >}}
## Requirements
You need a working installation of [the Go programming language](https://golang.org/), [the Node runtime](https://nodejs.org/) and [the Yarn package manager](https://yarnpkg.com/) installed to run the acceptance tests. You may also want to use [Docker](https://www.docker.com/) to start the necessary services in their respective containers.
## Acceptance Tests
Make sure you've cloned the [web frontend repo](https://github.com/owncloud/web/) and the [infinite scale repo](https://github.com/owncloud/ocis/) next to each other. If your file/folder structure is different, you'll have to change the paths below accordingly.
{{< hint info >}}
For now, an IDP configuration file gets generated once and will fail upon changing the oCIS url as done below. To avoid any clashes, remove this file before starting the tests:
```bash
rm ~/.ocis/idp/identifier-registration.yaml
```
{{< /hint >}}
### In the web repo
#### **Optional:** Build web to test local changes
Install dependencies and bundle the frontend with a watcher by running
```bash
yarn && yarn build:w
```
If you skip the step above, the currently bundled frontend from the oCIS binary will be used.
#### Dockerized acceptance test services
Start the necessary acceptance test services by using Docker (Compose):
```bash
docker compose up selenium middleware-ocis vnc
```
### In the oCIS repo
#### **Optional:** Build accounts UI to test local changes
Navigate into the accounts service via `cd ../accounts/` and install dependencies and build the bundled accounts UI with a watcher by running
```bash
yarn && yarn watch
```
#### Start oCIS from binary
Navigate into the oCIS directory inside the oCIS repository and build the oCIS binary by running
```bash
make clean build
```
Then, start oCIS from the binary via
```bash
./bin/ocis init
OCIS_URL=https://host.docker.internal:9200 OCIS_INSECURE=true PROXY_ENABLE_BASIC_AUTH=true WEB_UI_CONFIG=../../web/dev/docker/ocis.web.config.json ./bin/ocis server
```
If you've built the web bundle locally in its repository, you also need to reference the bundle output in the above command: `WEB_ASSET_PATH=../../web/dist`
If you've built the accounts UI bundle locally, you also need to reference the bundle output in the above command: `ACCOUNTS_ASSET_PATH=../accounts/assets/`
#### Run accounts acceptance tests
If you want visual feedback on the test run, visit http://host.docker.internal:6080/ in your browser and connect to the VNC client.
Navigate into the accounts service via `cd ../accounts/` and start the acceptance tests by running
```bash
SERVER_HOST=https://host.docker.internal:9200 BACKEND_HOST=https://host.docker.internal:9200 RUN_ON_OCIS=true NODE_TLS_REJECT_UNAUTHORIZED=0 WEB_PATH=../../web WEB_UI_CONFIG=../../web/tests/drone/config-ocis.json MIDDLEWARE_HOST=http://host.docker.internal:3000 ./ui/tests/run-acceptance-test.sh ./ui/tests/acceptance/features/
```

View File

@@ -34,7 +34,7 @@ We also suggest to use the last port in your extensions' range as a debug/metric
| 9130-9134 | [konnectd](https://github.com/owncloud/ocis/tree/master/konnectd) |
| 9135-9139 | [graph-explorer](https://github.com/owncloud/ocis/tree/master/graph-explorer) |
| 9140-9179 | [reva/storage](https://github.com/owncloud/ocis/tree/master/storage) |
| 9180-9184 | [accounts](https://github.com/owncloud/ocis/tree/master/accounts) |
| 9180-9184 | FREE (formerly used by accounts) |
| 9185-9189 | [thumbnails](https://github.com/owncloud/ocis/tree/master/thumbnails) |
| 9190-9194 | [settings](https://github.com/owncloud/ocis/tree/master/settings) |
| 9195-9199 | [store](https://github.com/owncloud/ocis/tree/master/store) |

View File

@@ -38,9 +38,6 @@ For now, the storage service uses these ports to preconfigure those services:
| 9165 | storage app-provider debug |
| 9178 | storage public link |
| 9179 | storage public link data |
| 9180 | accounts grpc |
| 9181 | accounts http |
| 9182 | accounts debug |
| 9215 | storage meta grpc |
| 9216 | storage meta http |
| 9217 | storage meta debug |

View File

@@ -61,13 +61,6 @@ proxy:
pretty: false
color: false
level: info
accounts:
http:
addr: localhost:2222
log:
level: debug
color: false
pretty: false
log:
pretty: true
color: true
@@ -80,21 +73,12 @@ http:
addr: localhost:3333
```
_accounts.yaml_
```yaml
http:
addr: localhost:4444
```
Note that the extension files will overwrite values from the main `ocis.yaml`, causing `ocis server` to run with the following configuration:
```yaml
proxy:
http:
addr: localhost:3333
accounts:
http:
addr: localhost:4444
log:
pretty: true
color: true
@@ -109,9 +93,6 @@ The logging configuration if defined in the main ocis.yaml is inherited by all e
proxy:
http:
addr: localhost:5555
accounts:
http:
addr: localhost:4444
log:
pretty: true
color: true

View File

@@ -1,2 +0,0 @@
*
!bin/

View File

@@ -1,17 +0,0 @@
{
"env": {
"browser": true,
"es6": true,
"amd": true
},
"extends": [
"standard",
"plugin:vue/essential"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
}
}

View File

@@ -1,17 +0,0 @@
# 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.*

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +0,0 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.1.0.cjs
enableScripts: false
enableTelemetry: false

View File

@@ -1,65 +0,0 @@
SHELL := bash
NAME := accounts
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

View File

@@ -1,9 +0,0 @@
package accounts
import (
"embed"
)
//go:generate make generate
//go:embed assets/*
var Assets embed.FS

View File

View File

@@ -1,25 +0,0 @@
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
}
}

View File

@@ -1,14 +0,0 @@
package main
import (
"os"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/command"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config/defaults"
)
func main() {
if err := command.Execute(defaults.DefaultConfig()); err != nil {
os.Exit(1)
}
}

View File

@@ -1,19 +0,0 @@
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 Accounts" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9180
ENTRYPOINT ["/usr/bin/ocis-accounts"]
CMD ["server"]
COPY bin/ocis-accounts /usr/bin/ocis-accounts

View File

@@ -1,19 +0,0 @@
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 Accounts" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9180
ENTRYPOINT ["/usr/bin/ocis-accounts"]
CMD ["server"]
COPY bin/ocis-accounts /usr/bin/ocis-accounts

View File

@@ -1,19 +0,0 @@
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 Accounts" \
org.label-schema.vendor="ownCloud GmbH" \
org.label-schema.schema-version="1.0"
EXPOSE 9180
ENTRYPOINT ["/usr/bin/ocis-accounts"]
CMD ["server"]
COPY bin/ocis-accounts /usr/bin/ocis-accounts

View File

@@ -1,22 +0,0 @@
image: owncloud/ocis-accounts:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: owncloud/ocis-accounts:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: owncloud/ocis-accounts:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
variant: v8
os: linux
- image: owncloud/ocis-accounts:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
variant: v6
os: linux

View File

@@ -1,9 +0,0 @@
[main]
host = https://www.transifex.com
[owncloud.ocis-accounts]
file_filter = locale/<lang>/LC_MESSAGES/app.po
minimum_perc = 0
source_file = template.pot
source_lang = en
type = PO

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +0,0 @@
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

View File

@@ -1,90 +0,0 @@
{
"name": "ocis-accounts",
"version": "0.0.0",
"private": true,
"description": "",
"homepage": "https://github.com/owncloud/ocis-accounts#readme",
"bugs": {
"url": "https://github.com/owncloud/ocis/issues",
"email": "support@owncloud.com"
},
"repository": "https://github.com/owncloud/ocis-accounts.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/accounts.swagger.json --moduleName accounts --destination ui/client/accounts/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-accounts'",
"licenses:csv": "license-checker-rseidelsohn --relativeLicensePath --csv --out ../third-party-licenses/node/accounts/third-party-licenses.csv",
"licenses:save": "license-checker-rseidelsohn --relativeLicensePath --out /dev/null --files ../third-party-licenses/node/accounts/third-party-licenses"
},
"browserslist": [
"> 1%",
"not dead"
],
"dependencies": {
"axios": "^0.21.4",
"core-js": "^3.17.3",
"debounce": "^1.2.1",
"validator": "^13.1.1",
"vuex": "^3.5.1"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-export-default-from": "^7.7.4",
"@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.13.12",
"@babel/register": "^7.14.5",
"@cucumber/cucumber": "^7.3.1",
"@cucumber/pretty-formatter": "^1.0.0-alpha.1",
"@erquhart/rollup-plugin-node-builtins": "^2.1.5",
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-json": "^4.0.1",
"@rollup/plugin-replace": "^2.3.0",
"archiver": "^5.3.0",
"chromedriver": "^93.0.1",
"cross-env": "^7.0.3",
"easygettext": "^2.17.0",
"eslint": "7.24.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.17.3",
"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": "^9.0.1",
"join-path": "^1.1.1",
"ldapjs": "^2.2.3",
"license-checker-rseidelsohn": "^3.1.0",
"nightwatch": "1.7.11",
"nightwatch-api": "3.0.2",
"nightwatch-vrt": "^0.2.10",
"node-fetch": "^2.6.1",
"qs": "^6.10.1",
"rimraf": "^3.0.0",
"rollup": "^2.55.1",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-eslint": "^7.0.0",
"rollup-plugin-filesize": "^9.1.0",
"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.4",
"swagger-vue-generator": "^1.0.6",
"url-search-params-polyfill": "^8.1.0",
"vue-template-compiler": "^2.6.11",
"xml-js": "^1.6.11"
},
"peerDependencies": {
"owncloud-design-system": "^12.2.2"
},
"packageManager": "yarn@3.1.0"
}

View File

@@ -1,50 +0,0 @@
package assets
import (
"net/http"
"github.com/owncloud/ocis/v2/extensions/accounts"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/assetsfs"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// New returns a new http filesystem to serve assets.
func New(opts ...Option) http.FileSystem {
options := newOptions(opts...)
return assetsfs.New(accounts.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
}
}

View File

@@ -1,60 +0,0 @@
package command
import (
"fmt"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/go-micro/plugins/v4/client/grpc"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/flagset"
"github.com/urfave/cli/v2"
)
// AddAccount command creates a new account
func AddAccount(cfg *config.Config) *cli.Command {
a := &accountsmsg.Account{
PasswordProfile: &accountsmsg.PasswordProfile{},
}
return &cli.Command{
Name: "add",
Usage: "create a new account",
Category: "account management",
Aliases: []string{"create", "a"},
Flags: flagset.AddAccountWithConfig(cfg, a),
Before: func(c *cli.Context) error {
// Write value of username to the flags beneath, as preferred name
// and on-premises-sam-account-name is probably confusing for users.
if username := c.String("username"); username != "" {
if !c.IsSet("on-premises-sam-account-name") {
if err := c.Set("on-premises-sam-account-name", username); err != nil {
return err
}
}
if !c.IsSet("preferred-name") {
if err := c.Set("preferred-name", username); err != nil {
return err
}
}
}
return nil
},
Action: func(c *cli.Context) error {
accSvcID := cfg.GRPC.Namespace + "." + cfg.Service.Name
accSvc := accountssvc.NewAccountsService(accSvcID, grpc.NewClient())
_, err := accSvc.CreateAccount(c.Context, &accountssvc.CreateAccountRequest{
Account: a,
})
if err != nil {
fmt.Println(fmt.Errorf("could not create account %w", err))
return err
}
return nil
}}
}

View File

@@ -1,57 +0,0 @@
package command
import (
"fmt"
"net/http"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config/parser"
"github.com/owncloud/ocis/v2/extensions/accounts/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",
Category: "info",
Before: func(c *cli.Context) error {
err := parser.ParseConfig(cfg)
if err != nil {
fmt.Printf("%v", err)
}
return err
},
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
},
}
}

View File

@@ -1,81 +0,0 @@
package command
import (
"fmt"
"os"
"strconv"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/flagset"
"github.com/go-micro/plugins/v4/client/grpc"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/urfave/cli/v2"
)
// InspectAccount command shows detailed information about a specific account.
func InspectAccount(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "inspect",
Usage: "show detailed data on an existing account",
Category: "account management",
ArgsUsage: "id",
Flags: flagset.InspectAccountWithConfig(cfg),
Action: func(c *cli.Context) error {
accServiceID := cfg.GRPC.Namespace + "." + cfg.Service.Name
if c.NArg() != 1 {
fmt.Println("Please provide a user-id")
os.Exit(1)
}
uid := c.Args().First()
accSvc := accountssvc.NewAccountsService(accServiceID, grpc.NewClient())
acc, err := accSvc.GetAccount(c.Context, &accountssvc.GetAccountRequest{
Id: uid,
})
if err != nil {
fmt.Println(fmt.Errorf("could not view account %w", err))
return err
}
buildAccountInspectTable(acc).Render()
return nil
}}
}
func buildAccountInspectTable(acc *accountsmsg.Account) *tw.Table {
table := tw.NewWriter(os.Stdout)
table.SetAutoMergeCells(true)
table.AppendBulk([][]string{
{"ID", acc.Id},
{"Mail", acc.Mail},
{"DisplayName", acc.DisplayName},
{"PreferredName", acc.PreferredName},
{"AccountEnabled", strconv.FormatBool(acc.AccountEnabled)},
{"CreationType", acc.CreationType},
{"CreatedDateTime", acc.CreatedDateTime.String()},
{"Description", acc.Description},
{"ExternalUserState", acc.ExternalUserState},
{"UidNumber", fmt.Sprintf("%+d", acc.UidNumber)},
{"GidNumber", fmt.Sprintf("%+d", acc.GidNumber)},
{"IsResourceAccount", strconv.FormatBool(acc.IsResourceAccount)},
{"OnPremisesDistinguishedName", acc.OnPremisesDistinguishedName},
{"OnPremisesDomainName", acc.OnPremisesDomainName},
{"OnPremisesImmutableId", acc.OnPremisesImmutableId},
{"OnPremisesSamAccountName", acc.OnPremisesSamAccountName},
{"OnPremisesSecurityIdentifier", acc.OnPremisesSecurityIdentifier},
{"OnPremisesUserPrincipalName", acc.OnPremisesUserPrincipalName},
{"RefreshTokenValidFromDateTime", acc.RefreshTokensValidFromDateTime.String()},
})
// Merged cell with group memberships
for k := range acc.MemberOf {
table.Append([]string{"MemberOf", acc.MemberOf[k].DisplayName})
}
return table
}

View File

@@ -1,55 +0,0 @@
package command
import (
"fmt"
"os"
"strconv"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/flagset"
"github.com/go-micro/plugins/v4/client/grpc"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/urfave/cli/v2"
)
// ListAccounts command lists all accounts
func ListAccounts(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "list",
Usage: "list existing accounts",
Category: "account management",
Aliases: []string{"ls"},
Flags: flagset.ListAccountsWithConfig(cfg),
Action: func(c *cli.Context) error {
accSvcID := cfg.GRPC.Namespace + "." + cfg.Service.Name
accSvc := accountssvc.NewAccountsService(accSvcID, grpc.NewClient())
resp, err := accSvc.ListAccounts(c.Context, &accountssvc.ListAccountsRequest{})
if err != nil {
fmt.Println(fmt.Errorf("could not list accounts %w", err))
return err
}
buildAccountsListTable(resp.Accounts).Render()
return nil
}}
}
// buildAccountsListTable creates an ascii table for printing on the cli
func buildAccountsListTable(accs []*accountsmsg.Account) *tw.Table {
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Id", "DisplayName", "Mail", "AccountEnabled"})
table.SetAutoFormatHeaders(false)
for _, acc := range accs {
table.Append([]string{
acc.Id,
acc.DisplayName,
acc.Mail,
strconv.FormatBool(acc.AccountEnabled)})
}
return table
}

View File

@@ -1,36 +0,0 @@
package command
import (
"context"
"fmt"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/go-micro/plugins/v4/client/grpc"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/urfave/cli/v2"
merrors "go-micro.dev/v4/errors"
)
// RebuildIndex rebuilds the entire configured index.
func RebuildIndex(cdf *config.Config) *cli.Command {
return &cli.Command{
Name: "rebuildIndex",
Usage: "rebuilds the service's index, i.e. deleting and then re-adding all existing documents",
Category: "account management",
Aliases: []string{"rebuild", "ri"},
Action: func(ctx *cli.Context) error {
idxSvcID := "com.owncloud.api.accounts"
idxSvc := accountssvc.NewIndexService(idxSvcID, grpc.NewClient())
_, err := idxSvc.RebuildIndex(context.Background(), &accountssvc.RebuildIndexRequest{})
if err != nil {
fmt.Println(merrors.FromError(err).Detail)
return err
}
fmt.Println("index rebuilt successfully")
return nil
},
}
}

View File

@@ -1,43 +0,0 @@
package command
import (
"fmt"
"os"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/flagset"
"github.com/go-micro/plugins/v4/client/grpc"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/urfave/cli/v2"
)
// RemoveAccount command deletes an existing account.
func RemoveAccount(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "remove",
Usage: "removes an existing account",
Category: "account management",
ArgsUsage: "id",
Aliases: []string{"rm"},
Flags: flagset.RemoveAccountWithConfig(cfg),
Action: func(c *cli.Context) error {
accServiceID := cfg.GRPC.Namespace + "." + cfg.Service.Name
if c.NArg() != 1 {
fmt.Println("Please provide a user-id")
os.Exit(1)
}
uid := c.Args().First()
accSvc := accountssvc.NewAccountsService(accServiceID, grpc.NewClient())
_, err := accSvc.DeleteAccount(c.Context, &accountssvc.DeleteAccountRequest{Id: uid})
if err != nil {
fmt.Println(fmt.Errorf("could not delete account %w", err))
return err
}
return nil
}}
}

View File

@@ -1,70 +0,0 @@
package command
import (
"context"
"os"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/clihelper"
ociscfg "github.com/owncloud/ocis/v2/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
AddAccount(cfg),
UpdateAccount(cfg),
ListAccounts(cfg),
InspectAccount(cfg),
RemoveAccount(cfg),
RebuildIndex(cfg),
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the ocis-accounts command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "accounts",
Usage: "Provide accounts and groups 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 accounts command to be embedded and supervised by a suture supervisor tree.
type SutureService struct {
cfg *config.Config
}
// NewSutureService creates a new accounts.SutureService
func NewSutureService(cfg *ociscfg.Config) suture.Service {
cfg.Accounts.Commons = cfg.Commons
return SutureService{
cfg: cfg.Accounts,
}
}
func (s SutureService) Serve(ctx context.Context) error {
s.cfg.Context = ctx
if err := Execute(s.cfg); err != nil {
return err
}
return nil
}

View File

@@ -1,110 +0,0 @@
package command
import (
"context"
"fmt"
"os"
"github.com/oklog/run"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config/parser"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/logging"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/metrics"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/server/debug"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/server/grpc"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/server/http"
svc "github.com/owncloud/ocis/v2/extensions/accounts/pkg/service/v0"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/tracing"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
"github.com/urfave/cli/v2"
)
// Server is the entry point 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 {
err := parser.ParseConfig(cfg)
if err != nil {
fmt.Printf("%v", err)
os.Exit(1)
}
return err
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
err := tracing.Configure(cfg)
if err != nil {
return err
}
gr := run.Group{}
ctx, cancel := defineContext(cfg)
mtrcs := metrics.New()
defer cancel()
mtrcs.BuildInfo.WithLabelValues(version.String).Set(1)
handler, err := svc.New(svc.Logger(logger), svc.Config(cfg))
if err != nil {
logger.Error().Err(err).Msg("handler init")
return err
}
httpServer := http.Server(
http.Config(cfg),
http.Logger(logger),
http.Name(cfg.Service.Name),
http.Context(ctx),
http.Metrics(mtrcs),
http.Handler(handler),
)
gr.Add(httpServer.Run, func(_ error) {
logger.Info().Str("server", "http").Msg("shutting down server")
cancel()
})
grpcServer := grpc.Server(
grpc.Config(cfg),
grpc.Logger(logger),
grpc.Name(cfg.Service.Name),
grpc.Context(ctx),
grpc.Metrics(mtrcs),
grpc.Handler(handler),
)
gr.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
}
gr.Add(debugServer.ListenAndServe, func(_ error) {
_ = debugServer.Shutdown(ctx)
cancel()
})
return gr.Run()
},
}
}
// defineContext sets the context for the extension. If there is a context configured it will create a new child from it,
// if not, it will create a root context that can be cancelled.
func defineContext(cfg *config.Config) (context.Context, context.CancelFunc) {
return func() (context.Context, context.CancelFunc) {
if cfg.Context == nil {
return context.WithCancel(context.Background())
}
return context.WithCancel(cfg.Context)
}()
}

View File

@@ -1,91 +0,0 @@
package command
import (
"errors"
"fmt"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/flagset"
"github.com/go-micro/plugins/v4/client/grpc"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/urfave/cli/v2"
"google.golang.org/genproto/protobuf/field_mask"
)
// UpdateAccount command for modifying accounts including password policies
func UpdateAccount(cfg *config.Config) *cli.Command {
a := &accountsmsg.Account{
PasswordProfile: &accountsmsg.PasswordProfile{},
}
return &cli.Command{
Name: "update",
Usage: "Make changes to an existing account",
Category: "account management",
ArgsUsage: "id",
Flags: flagset.UpdateAccountWithConfig(cfg, a),
Before: func(c *cli.Context) error {
if len(c.StringSlice("password_policies")) > 0 {
a.PasswordProfile.PasswordPolicies = c.StringSlice("password_policies")
}
if c.NArg() != 1 {
return errors.New("missing account-id")
}
if c.NumFlags() == 0 {
return errors.New("missing attribute-flags for update")
}
return nil
},
Action: func(c *cli.Context) error {
a.Id = c.Args().First()
accSvcID := cfg.GRPC.Namespace + "." + cfg.Service.Name
accSvc := accountssvc.NewAccountsService(accSvcID, grpc.NewClient())
_, err := accSvc.UpdateAccount(c.Context, &accountssvc.UpdateAccountRequest{
Account: a,
UpdateMask: buildAccUpdateMask(c.FlagNames()),
})
if err != nil {
fmt.Println(fmt.Errorf("could not update account %w", err))
return err
}
return nil
}}
}
// buildAccUpdateMask by mapping passed update flags to account fieldNames.
//
// The UpdateMask is passed with the update-request to the server so that
// only the modified values are transferred.
func buildAccUpdateMask(setFlags []string) *field_mask.FieldMask {
var flagToPath = map[string]string{
"enabled": "AccountEnabled",
"displayname": "DisplayName",
"preferred-name": "PreferredName",
"uidnumber": "UidNumber",
"gidnumber": "GidNumber",
"mail": "Mail",
"description": "Description",
"password": "PasswordProfile.Password",
"password-policies": "PasswordProfile.PasswordPolicies",
"force-password-change": "PasswordProfile.ForceChangePasswordNextSignIn",
"force-password-change-mfa": "PasswordProfile.ForceChangePasswordNextSignInWithMfa",
"on-premises-sam-account-name": "OnPremisesSamAccountName",
}
updatedPaths := make([]string, 0)
for _, v := range setFlags {
if _, ok := flagToPath[v]; ok {
updatedPaths = append(updatedPaths, flagToPath[v])
}
}
return &field_mask.FieldMask{Paths: updatedPaths}
}

View File

@@ -1,50 +0,0 @@
package command
import (
"fmt"
"os"
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis/v2/extensions/accounts/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
},
}
}

View File

@@ -1,80 +0,0 @@
package config
import (
"context"
"github.com/owncloud/ocis/v2/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"`
TokenManager *TokenManager `yaml:"token_manager"`
Asset Asset `yaml:"asset"`
Repo Repo `yaml:"repo"`
Index Index `yaml:"index"`
ServiceUser ServiceUser `yaml:"service_user"`
HashDifficulty int `yaml:"hash_difficulty" env:"ACCOUNTS_HASH_DIFFICULTY" desc:"The hash difficulty makes sure that validating a password takes at least a certain amount of time."`
DemoUsersAndGroups bool `yaml:"demo_users_and_groups" env:"ACCOUNTS_DEMO_USERS_AND_GROUPS" desc:"If this flag is set the service will setup the demo users and groups."`
Context context.Context `yaml:"-"`
}
// Asset defines the available asset configuration.
type Asset struct {
Path string `yaml:"path" env:"ACCOUNTS_ASSET_PATH" desc:"The path to the ui assets."`
}
// Repo defines which storage implementation is to be used.
type Repo struct {
Backend string `yaml:"backend" env:"ACCOUNTS_STORAGE_BACKEND" desc:"Defines which storage implementation is to be used"`
Disk Disk `yaml:"disk"`
CS3 CS3 `yaml:"cs3"`
}
// Disk is the local disk implementation of the storage.
type Disk struct {
Path string `yaml:"path" env:"ACCOUNTS_STORAGE_DISK_PATH" desc:"The path where the accounts data is stored."`
}
// CS3 is the cs3 implementation of the storage.
type CS3 struct {
ProviderAddr string `yaml:"provider_addr" env:"ACCOUNTS_STORAGE_CS3_PROVIDER_ADDR" desc:"The address to the storage provider."`
}
// ServiceUser defines the user required for EOS.
type ServiceUser struct {
UUID string `yaml:"uuid" env:"ACCOUNTS_SERVICE_USER_UUID" desc:"The id of the accounts service user."`
Username string `yaml:"username" env:"ACCOUNTS_SERVICE_USER_USERNAME" desc:"The username of the accounts service user."`
UID int64 `yaml:"uid" env:"ACCOUNTS_SERVICE_USER_UID" desc:"The uid of the accounts service user."`
GID int64 `yaml:"gid" env:"ACCOUNTS_SERVICE_USER_GID" desc:"The gid of the accounts service user."`
}
// Index defines config for indexes.
type Index struct {
UID UIDBound `yaml:"uid"`
GID GIDBound `yaml:"gid"`
}
// GIDBound defines a lower and upper bound.
type GIDBound struct {
Lower int64 `yaml:"lower" env:"ACCOUNTS_GID_INDEX_LOWER_BOUND" desc:"The lowest possible gid value for the indexer."`
Upper int64 `yaml:"upper" env:"ACCOUNTS_GID_INDEX_UPPER_BOUND" desc:"The highest possible gid value for the indexer."`
}
// UIDBound defines a lower and upper bound.
type UIDBound struct {
Lower int64 `yaml:"lower" env:"ACCOUNTS_UID_INDEX_LOWER_BOUND" desc:"The lowest possible uid value for the indexer."`
Upper int64 `yaml:"upper" env:"ACCOUNTS_UID_INDEX_UPPER_BOUND" desc:"The highest possible uid value for the indexer."`
}

View File

@@ -1,9 +0,0 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `yaml:"addr" env:"ACCOUNTS_DEBUG_ADDR"`
Token string `yaml:"token" env:"ACCOUNTS_DEBUG_TOKEN"`
Pprof bool `yaml:"pprof" env:"ACCOUNTS_DEBUG_PPROF"`
Zpages bool `yaml:"zpages" env:"ACCOUNTS_DEBUG_ZPAGES"`
}

View File

@@ -1,115 +0,0 @@
package defaults
import (
"path"
"strings"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
)
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9182",
Token: "",
Pprof: false,
Zpages: false,
},
HTTP: config.HTTP{
Addr: "127.0.0.1:9181",
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:9180",
Namespace: "com.owncloud.api",
},
Service: config.Service{
Name: "accounts",
},
Asset: config.Asset{},
HashDifficulty: 11,
DemoUsersAndGroups: false,
Repo: config.Repo{
Backend: "CS3",
Disk: config.Disk{
Path: path.Join(defaults.BaseDataPath(), "accounts"),
},
CS3: config.CS3{
ProviderAddr: "localhost:9215",
},
},
Index: config.Index{
UID: config.UIDBound{
Lower: 0,
Upper: 1000,
},
GID: config.GIDBound{
Lower: 0,
Upper: 1000,
},
},
ServiceUser: config.ServiceUser{
UUID: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad",
Username: "95cb8724-03b2-11eb-a0a6-c33ef8ef53ad",
UID: 0,
GID: 0,
},
}
}
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{}
}
if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
cfg.TokenManager = &config.TokenManager{
JWTSecret: cfg.Commons.TokenManager.JWTSecret,
}
} else if cfg.TokenManager == nil {
cfg.TokenManager = &config.TokenManager{}
}
}
func Sanitize(cfg *config.Config) {
// sanitize config
if cfg.HTTP.Root != "/" {
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
}
cfg.Repo.Backend = strings.ToLower(cfg.Repo.Backend)
}

View File

@@ -1,7 +0,0 @@
package config
// GRPC defines the available grpc configuration.
type GRPC struct {
Addr string `yaml:"addr" env:"ACCOUNTS_GRPC_ADDR" desc:"The address of the grpc service."`
Namespace string `yaml:"-"`
}

View File

@@ -1,18 +0,0 @@
package config
// HTTP defines the available http configuration.
type HTTP struct {
Addr string `yaml:"addr" env:"ACCOUNTS_HTTP_ADDR" desc:"The address of the http service."`
Namespace string `yaml:"-"`
Root string `yaml:"root" env:"ACCOUNTS_HTTP_ROOT" desc:"The root path of the http service."`
CacheTTL int `yaml:"cache_ttl" env:"ACCOUNTS_CACHE_TTL" desc:"The cache time for the static assets."`
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"`
}

View File

@@ -1,9 +0,0 @@
package config
// Log defines the available log configuration.
type Log struct {
Level string `yaml:"level" env:"OCIS_LOG_LEVEL;ACCOUNTS_LOG_LEVEL" desc:"The log level."`
Pretty bool `yaml:"pretty" env:"OCIS_LOG_PRETTY;ACCOUNTS_LOG_PRETTY" desc:"Activates pretty log output."`
Color bool `yaml:"color" env:"OCIS_LOG_COLOR;ACCOUNTS_LOG_COLOR" desc:"Activates colorized log output."`
File string `yaml:"file" env:"OCIS_LOG_FILE;ACCOUNTS_LOG_FILE" desc:"The target log file."`
}

View File

@@ -1,41 +0,0 @@
package parser
import (
"errors"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
defaults "github.com/owncloud/ocis/v2/extensions/accounts/pkg/config/defaults"
ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode"
)
// ParseConfig loads 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 Validate(cfg)
}
func Validate(cfg *config.Config) error {
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
return nil
}

View File

@@ -1,6 +0,0 @@
package config
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"OCIS_JWT_SECRET;ACCOUNTS_JWT_SECRET"`
}

View File

@@ -1,6 +0,0 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
}

View File

@@ -1,9 +0,0 @@
package config
// Tracing defines the available tracing configuration.
type Tracing struct {
Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;ACCOUNTS_TRACING_ENABLED" desc:"Activates tracing."`
Type string `yaml:"type" env:"OCIS_TRACING_TYPE;ACCOUNTS_TRACING_TYPE"`
Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;ACCOUNTS_TRACING_ENDPOINT" desc:"The endpoint to the tracing collector."`
Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;ACCOUNTS_TRACING_COLLECTOR"`
}

View File

@@ -1,241 +0,0 @@
package flagset
import (
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/flags"
"github.com/urfave/cli/v2"
)
// UpdateAccountWithConfig applies update command flags to cfg
func UpdateAccountWithConfig(cfg *config.Config, a *accountsmsg.Account) []cli.Flag {
if a.PasswordProfile == nil {
a.PasswordProfile = &accountsmsg.PasswordProfile{}
}
return []cli.Flag{
&cli.StringFlag{
Name: "grpc-namespace",
Value: flags.OverrideDefaultString(cfg.GRPC.Namespace, "com.owncloud.api"),
Usage: "Set the base namespace for the grpc namespace",
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
Destination: &cfg.GRPC.Namespace,
},
&cli.StringFlag{
Name: "name",
Value: flags.OverrideDefaultString(cfg.Service.Name, "accounts"),
Usage: "service name",
EnvVars: []string{"ACCOUNTS_NAME"},
Destination: &cfg.Service.Name,
},
&cli.BoolFlag{
Name: "enabled",
Usage: "Enable the account",
Destination: &a.AccountEnabled,
},
&cli.StringFlag{
Name: "displayname",
Usage: "Set the displayname for the account",
Destination: &a.DisplayName,
},
&cli.StringFlag{
Name: "preferred-name",
Usage: "Set the preferred-name for the account",
Destination: &a.PreferredName,
},
&cli.StringFlag{
Name: "on-premises-sam-account-name",
Usage: "Set the on-premises-sam-account-name",
Destination: &a.OnPremisesSamAccountName,
},
&cli.Int64Flag{
Name: "uidnumber",
Usage: "Set the uidnumber for the account",
Destination: &a.UidNumber,
},
&cli.Int64Flag{
Name: "gidnumber",
Usage: "Set the gidnumber for the account",
Destination: &a.GidNumber,
},
&cli.StringFlag{
Name: "mail",
Usage: "Set the mail for the account",
Destination: &a.Mail,
},
&cli.StringFlag{
Name: "description",
Usage: "Set the description for the account",
Destination: &a.Description,
},
&cli.StringFlag{
Name: "password",
Usage: "Set the password for the account",
Destination: &a.PasswordProfile.Password,
// TODO read password from ENV?
},
&cli.StringSliceFlag{
Name: "password-policies",
Usage: "Possible policies: DisableStrongPassword, DisablePasswordExpiration",
},
&cli.BoolFlag{
Name: "force-password-change",
Usage: "Force password change on next sign-in",
Destination: &a.PasswordProfile.ForceChangePasswordNextSignIn,
},
&cli.BoolFlag{
Name: "force-password-change-mfa",
Usage: "Force password change on next sign-in with mfa",
Destination: &a.PasswordProfile.ForceChangePasswordNextSignInWithMfa,
},
}
}
// AddAccountWithConfig applies create command flags to cfg
func AddAccountWithConfig(cfg *config.Config, a *accountsmsg.Account) []cli.Flag {
if a.PasswordProfile == nil {
a.PasswordProfile = &accountsmsg.PasswordProfile{}
}
return []cli.Flag{
&cli.StringFlag{
Name: "grpc-namespace",
Value: flags.OverrideDefaultString(cfg.GRPC.Namespace, "com.owncloud.api"),
Usage: "Set the base namespace for the grpc namespace",
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
Destination: &cfg.GRPC.Namespace,
},
&cli.StringFlag{
Name: "name",
Value: flags.OverrideDefaultString(cfg.Service.Name, "accounts"),
Usage: "service name",
EnvVars: []string{"ACCOUNTS_NAME"},
Destination: &cfg.Service.Name,
},
&cli.BoolFlag{
Name: "enabled",
Usage: "Enable the account",
Destination: &a.AccountEnabled,
},
&cli.StringFlag{
Name: "displayname",
Usage: "Set the displayname for the account",
Destination: &a.DisplayName,
},
&cli.StringFlag{
Name: "username",
Usage: "Username will be written to preferred-name and on_premises_sam_account_name",
},
&cli.StringFlag{
Name: "preferred-name",
Usage: "Set the preferred-name for the account",
Destination: &a.PreferredName,
},
&cli.StringFlag{
Name: "on-premises-sam-account-name",
Usage: "Set the on-premises-sam-account-name",
Destination: &a.OnPremisesSamAccountName,
},
&cli.Int64Flag{
Name: "uidnumber",
Usage: "Set the uidnumber for the account",
Destination: &a.UidNumber,
},
&cli.Int64Flag{
Name: "gidnumber",
Usage: "Set the gidnumber for the account",
Destination: &a.GidNumber,
},
&cli.StringFlag{
Name: "mail",
Usage: "Set the mail for the account",
Destination: &a.Mail,
},
&cli.StringFlag{
Name: "description",
Usage: "Set the description for the account",
Destination: &a.Description,
},
&cli.StringFlag{
Name: "password",
Usage: "Set the password for the account",
Destination: &a.PasswordProfile.Password,
// TODO read password from ENV?
},
&cli.StringSliceFlag{
Name: "password-policies",
Usage: "Possible policies: DisableStrongPassword, DisablePasswordExpiration",
},
&cli.BoolFlag{
Name: "force-password-change",
Usage: "Force password change on next sign-in",
Destination: &a.PasswordProfile.ForceChangePasswordNextSignIn,
},
&cli.BoolFlag{
Name: "force-password-change-mfa",
Usage: "Force password change on next sign-in with mfa",
Destination: &a.PasswordProfile.ForceChangePasswordNextSignInWithMfa,
},
}
}
// ListAccountsWithConfig applies list command flags to cfg
func ListAccountsWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "grpc-namespace",
Value: flags.OverrideDefaultString(cfg.GRPC.Namespace, "com.owncloud.api"),
Usage: "Set the base namespace for the grpc namespace",
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
Destination: &cfg.GRPC.Namespace,
},
&cli.StringFlag{
Name: "name",
Value: flags.OverrideDefaultString(cfg.Service.Name, "accounts"),
Usage: "service name",
EnvVars: []string{"ACCOUNTS_NAME"},
Destination: &cfg.Service.Name,
},
}
}
// RemoveAccountWithConfig applies remove command flags to cfg
func RemoveAccountWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "grpc-namespace",
Value: flags.OverrideDefaultString(cfg.GRPC.Namespace, "com.owncloud.api"),
Usage: "Set the base namespace for the grpc namespace",
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
Destination: &cfg.GRPC.Namespace,
},
&cli.StringFlag{
Name: "name",
Value: flags.OverrideDefaultString(cfg.Service.Name, "accounts"),
Usage: "service name",
EnvVars: []string{"ACCOUNTS_NAME"},
Destination: &cfg.Service.Name,
},
}
}
// InspectAccountWithConfig applies inspect command flags to cfg
func InspectAccountWithConfig(cfg *config.Config) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "grpc-namespace",
Value: flags.OverrideDefaultString(cfg.GRPC.Namespace, "com.owncloud.api"),
Usage: "Set the base namespace for the grpc namespace",
EnvVars: []string{"ACCOUNTS_GRPC_NAMESPACE"},
Destination: &cfg.GRPC.Namespace,
},
&cli.StringFlag{
Name: "name",
Value: flags.OverrideDefaultString(cfg.Service.Name, "accounts"),
Usage: "service name",
EnvVars: []string{"ACCOUNTS_NAME"},
Destination: &cfg.Service.Name,
},
}
}

View File

@@ -1,17 +0,0 @@
package logging
import (
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/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),
)
}

View File

@@ -1,33 +0,0 @@
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 = "accounts"
)
// 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{
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "build_info",
Help: "Build information",
}, []string{"version"}),
}
_ = prometheus.Register(m.BuildInfo)
// TODO: implement metrics
return m
}

View File

@@ -1,50 +0,0 @@
package debug
import (
"context"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/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
}
}

View File

@@ -1,63 +0,0 @@
package debug
import (
"io"
"net/http"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/service/debug"
"github.com/owncloud/ocis/v2/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)
}
}
}

View File

@@ -1,85 +0,0 @@
package grpc
import (
"context"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/metrics"
svc "github.com/owncloud/ocis/v2/extensions/accounts/pkg/service/v0"
"github.com/owncloud/ocis/v2/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
Handler *svc.Service
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Name provides a name for the service.
func Name(val string) Option {
return func(o *Options) {
o.Name = val
}
}
// 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
}
}
// 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...)
}
}
// Handler provides a function to set the handler option.
func Handler(val *svc.Service) Option {
return func(o *Options) {
o.Handler = val
}
}

View File

@@ -1,36 +0,0 @@
package grpc
import (
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
)
// Server initializes a new go-micro service ready to run
func Server(opts ...Option) grpc.Service {
options := newOptions(opts...)
handler := options.Handler
service := grpc.NewService(
grpc.Name(options.Config.Service.Name),
grpc.Context(options.Context),
grpc.Address(options.Config.GRPC.Addr),
grpc.Namespace(options.Config.GRPC.Namespace),
grpc.Logger(options.Logger),
grpc.Flags(options.Flags...),
grpc.Version(version.String),
)
if err := accountssvc.RegisterAccountsServiceHandler(service.Server(), handler); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register service handler")
}
if err := accountssvc.RegisterGroupsServiceHandler(service.Server(), handler); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register groups handler")
}
if err := accountssvc.RegisterIndexServiceHandler(service.Server(), handler); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register index handler")
}
return service
}

View File

@@ -1,85 +0,0 @@
package http
import (
"context"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/metrics"
svc "github.com/owncloud/ocis/v2/extensions/accounts/pkg/service/v0"
"github.com/owncloud/ocis/v2/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
Handler *svc.Service
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Name provides a name for the service.
func Name(val string) Option {
return func(o *Options) {
o.Name = val
}
}
// 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
}
}
// 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...)
}
}
// Handler provides a function to set the handler option.
func Handler(val *svc.Service) Option {
return func(o *Options) {
o.Handler = val
}
}

View File

@@ -1,80 +0,0 @@
package http
import (
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/assets"
"github.com/owncloud/ocis/v2/ocis-pkg/account"
"github.com/owncloud/ocis/v2/ocis-pkg/cors"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
"github.com/owncloud/ocis/v2/ocis-pkg/service/http"
"github.com/owncloud/ocis/v2/ocis-pkg/version"
"go-micro.dev/v4"
)
// Server initializes the http service and server.
func Server(opts ...Option) http.Service {
options := newOptions(opts...)
handler := options.Handler
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...),
)
mux := chi.NewMux()
mux.Use(chimiddleware.RealIP)
mux.Use(chimiddleware.RequestID)
mux.Use(middleware.TraceContext)
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) {
accountssvc.RegisterAccountsServiceWeb(r, handler)
accountssvc.RegisterGroupsServiceWeb(r, handler)
})
err := micro.RegisterHandler(service.Server(), mux)
if err != nil {
options.Logger.Fatal().Err(err).Msg("failed to register the handler")
}
return service
}

View File

@@ -1,864 +0,0 @@
package service
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"path"
"regexp"
"strconv"
"time"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/attribute"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/gofrs/uuid"
"github.com/golang/protobuf/ptypes/empty"
fieldmask_utils "github.com/mennanov/fieldmask-utils"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/storage"
accTracing "github.com/owncloud/ocis/v2/extensions/accounts/pkg/tracing"
settings_svc "github.com/owncloud/ocis/v2/extensions/settings/pkg/service/v0"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
"github.com/owncloud/ocis/v2/ocis-pkg/sync"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/rs/zerolog"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
"golang.org/x/crypto/bcrypt"
"google.golang.org/genproto/protobuf/field_mask"
p "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
// passwordValidCache caches basic auth password validations
var passwordValidCache = sync.NewCache(1024)
// passwordValidCacheExpiration defines the entry lifetime
const passwordValidCacheExpiration = 10 * time.Minute
// an auth request is currently hardcoded and has to match this regex
// login eq \"teddy\" and password eq \"F&1!b90t111!\"
var authQuery = regexp.MustCompile(`^login eq '(.*)' and password eq '(.*)'$`) // TODO how is ' escaped in the password?
func (s Service) expandMemberOf(a *accountsmsg.Account) {
if a == nil {
return
}
expanded := []*accountsmsg.Group{}
for i := range a.MemberOf {
g := &accountsmsg.Group{}
// TODO resolve by name, when a create or update is issued they may not have an id? fall back to searching the group id in the index?
if err := s.repo.LoadGroup(context.Background(), a.MemberOf[i].Id, g); err == nil {
g.Members = nil // always hide members when expanding
expanded = append(expanded, g)
} else {
// log errors but continue execution for now
s.log.Error().Err(err).Str("id", a.MemberOf[i].Id).Msg("could not load group")
}
}
a.MemberOf = expanded
}
func (s Service) hasAccountManagementPermissions(ctx context.Context) bool {
// get roles from context
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
}
// check if permission is present in roles of the authenticated account
return s.RoleManager.FindPermissionByID(ctx, roleIDs, AccountManagementPermissionID) != nil
}
func (s Service) hasSelfManagementPermissions(ctx context.Context) bool {
// get roles from context
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
if !ok {
return false
}
// check if permission is present in roles of the authenticated account
return s.RoleManager.FindPermissionByID(ctx, roleIDs, SelfManagementPermissionID) != nil
}
// ListAccounts implements the AccountsServiceHandler interface
// the query contains account properties
func (s Service) ListAccounts(ctx context.Context, in *accountssvc.ListAccountsRequest, out *accountssvc.ListAccountsResponse) (err error) {
var span trace.Span
ctx, span = accTracing.TraceProvider.Tracer("accounts").Start(ctx, "Accounts.ListAccounts")
defer span.End()
span.SetAttributes(
attribute.KeyValue{Key: "page_size", Value: attribute.Int64Value(int64(in.PageSize))},
attribute.KeyValue{Key: "page_token", Value: attribute.StringValue(in.PageToken)},
)
hasSelf := s.hasSelfManagementPermissions(ctx)
hasManagement := s.hasAccountManagementPermissions(ctx)
if !hasSelf && !hasManagement {
return merrors.Forbidden(s.id, "no permission for ListAccounts")
}
onlySelf := hasSelf && !hasManagement
match, authRequest := getAuthQueryMatch(in.Query)
if authRequest {
password := match[2]
if len(password) == 0 {
return merrors.Unauthorized(s.id, "account not found or invalid credentials")
}
ids, err := s.index.FindBy(&accountsmsg.Account{}, "OnPremisesSamAccountName", match[1])
if err != nil || len(ids) > 1 {
return merrors.Unauthorized(s.id, "account not found or invalid credentials")
}
if len(ids) == 0 {
ids, err = s.index.FindBy(&accountsmsg.Account{}, "Mail", match[1])
if err != nil || len(ids) != 1 {
return merrors.Unauthorized(s.id, "account not found or invalid credentials")
}
}
a := &accountsmsg.Account{}
err = s.repo.LoadAccount(ctx, ids[0], a)
if err != nil || a.PasswordProfile == nil || len(a.PasswordProfile.Password) == 0 {
return merrors.Unauthorized(s.id, "account not found or invalid credentials")
}
// isPasswordValid uses bcrypt.CompareHashAndPassword which is slow by design.
// if every request that matches authQuery regex needs to do this step over and over again,
// this is secure but also slow. In this implementation we keep it same secure but increase the speed.
//
// flow:
// - request comes in
// - it creates a sha256 based on found account PasswordProfile.LastPasswordChangeDateTime and requested password (v)
// - it checks if the cache already contains an entry that matches found account Id // account PasswordProfile.LastPasswordChangeDateTime (k)
// - if no entry exists it runs the bcrypt.CompareHashAndPassword as before and if everything is ok it stores the
// result by the (k) as key and (v) as value. If not it errors
// - if a entry is found it checks if the given value matches (v). If it doesnt match, the cache entry gets removed
// and it errors.
{
var suspicious bool
kh := sha256.New()
mustWrite(kh, []byte(a.Id))
k := hex.EncodeToString(kh.Sum([]byte(a.PasswordProfile.LastPasswordChangeDateTime.String())))
vh := sha256.New()
mustWrite(vh, []byte(a.PasswordProfile.Password))
v := vh.Sum([]byte(password))
e := passwordValidCache.Load(k)
if e == nil {
suspicious = !isPasswordValid(s.log, a.PasswordProfile.Password, password)
} else if !bytes.Equal(e.V.([]byte), v) {
suspicious = true
}
if suspicious {
passwordValidCache.Delete(k)
return merrors.Unauthorized(s.id, "account not found or invalid credentials")
}
if e == nil {
passwordValidCache.Store(k, v, time.Now().Add(passwordValidCacheExpiration))
}
}
a.PasswordProfile.Password = ""
out.Accounts = []*accountsmsg.Account{a}
return nil
}
if onlySelf {
// limit list to own account id
if aid, ok := metadata.Get(ctx, middleware.AccountID); ok {
in.Query = "id eq '" + aid + "'"
} else {
return merrors.InternalServerError(s.id, "account id not in context")
}
}
if in.Query == "" {
err = s.repo.LoadAccounts(ctx, &out.Accounts)
if err != nil {
s.log.Err(err).Msg("failed to load all accounts from storage")
return merrors.InternalServerError(s.id, "failed to load all accounts")
}
for i := range out.Accounts {
a := out.Accounts[i]
// TODO add groups only if requested
// if in.FieldMask ...
s.expandMemberOf(a)
if a.PasswordProfile != nil {
a.PasswordProfile.Password = ""
}
}
return nil
}
searchResults, err := s.findAccountsByQuery(ctx, in.Query)
out.Accounts = make([]*accountsmsg.Account, 0, len(searchResults))
for _, hit := range searchResults {
a := &accountsmsg.Account{}
if hit == s.Config.ServiceUser.UUID {
acc := s.getInMemoryServiceUser()
a = &acc
} else if err = s.repo.LoadAccount(ctx, hit, a); err != nil {
s.log.Error().Err(err).Str("account", hit).Msg("could not load account, skipping")
continue
}
s.debugLogAccount(a).Msg("found account")
// TODO add groups if requested
// if in.FieldMask ...
s.expandMemberOf(a)
// remove password before returning
if a.PasswordProfile != nil {
a.PasswordProfile.Password = ""
}
out.Accounts = append(out.Accounts, a)
}
return
}
func (s Service) findAccountsByQuery(ctx context.Context, query string) ([]string, error) {
return s.index.Query(ctx, &accountsmsg.Account{}, query)
}
// GetAccount implements the AccountsServiceHandler interface
func (s Service) GetAccount(ctx context.Context, in *accountssvc.GetAccountRequest, out *accountsmsg.Account) (err error) {
var span trace.Span
ctx, span = accTracing.TraceProvider.Tracer("accounts").Start(ctx, "Accounts.GetAccount")
defer span.End()
span.SetAttributes(
attribute.KeyValue{Key: "account_id", Value: attribute.StringValue(in.Id)},
)
hasSelf := s.hasSelfManagementPermissions(ctx)
hasManagement := s.hasAccountManagementPermissions(ctx)
if !hasSelf && !hasManagement {
return merrors.Forbidden(s.id, "no permission for GetAccount")
}
onlySelf := hasSelf && !hasManagement
var id string
if id, err = cleanupID(in.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
if onlySelf {
// limit get to own account id
if aid, ok := metadata.Get(ctx, middleware.AccountID); ok {
if id != aid {
return merrors.Forbidden(s.id, "no permission for GetAccount of another user")
}
} else {
return merrors.InternalServerError(s.id, "account id not in context")
}
}
if err = s.repo.LoadAccount(ctx, id, out); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "account not found: %v", err.Error())
}
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
return merrors.InternalServerError(s.id, "could not load account: %v", err.Error())
}
s.debugLogAccount(out).Msg("found account")
// TODO add groups if requested
// if in.FieldMask ...
s.expandMemberOf(out)
// remove password
if out.PasswordProfile != nil {
out.PasswordProfile.Password = ""
}
return
}
// CreateAccount implements the AccountsServiceHandler interface
func (s Service) CreateAccount(ctx context.Context, in *accountssvc.CreateAccountRequest, out *accountsmsg.Account) (err error) {
var span trace.Span
ctx, span = accTracing.TraceProvider.Tracer("accounts").Start(ctx, "Accounts.CreateAccount")
defer span.End()
span.SetAttributes(
attribute.KeyValue{Key: "account", Value: attribute.StringValue(in.Account.String())},
)
if !s.hasAccountManagementPermissions(ctx) {
return merrors.Forbidden(s.id, "no permission for CreateAccount")
}
var id string
if in.Account == nil {
return merrors.InternalServerError(s.id, "invalid account: empty")
}
p.Merge(out, in.Account)
if out.Id == "" {
out.Id = uuid.Must(uuid.NewV4()).String()
}
if err = validateAccount(s.id, out); err != nil {
return err
}
if id, err = cleanupID(out.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
exists, err := s.accountExists(ctx, out.PreferredName, out.Mail, out.Id)
if err != nil {
return merrors.InternalServerError(s.id, "could not check if account exists: %v", err.Error())
}
if exists {
return merrors.Conflict(s.id, "account already exists")
}
if out.PasswordProfile != nil {
if out.PasswordProfile.Password != "" {
// encrypt password
hashed, err := bcrypt.GenerateFromPassword([]byte(in.Account.PasswordProfile.Password), s.Config.HashDifficulty)
if err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not hash password")
return merrors.InternalServerError(s.id, "could not hash password: %v", err.Error())
}
out.PasswordProfile.Password = string(hashed)
in.Account.PasswordProfile.Password = ""
}
if err := passwordPoliciesValid(out.PasswordProfile.PasswordPolicies); err != nil {
return merrors.BadRequest(s.id, "%s", err)
}
}
// extract group id
// TODO groups should be ignored during create, use groups.AddMember? return error?
// write and index account - note: don't do anything else in between!
if err = s.repo.WriteAccount(ctx, out); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not persist new account")
s.debugLogAccount(out).Msg("could not persist new account")
return merrors.InternalServerError(s.id, "could not persist new account: %v", err.Error())
}
indexResults, err := s.index.Add(out)
if err != nil {
s.rollbackCreateAccount(ctx, out)
return merrors.Conflict(s.id, "Account already exists %v", err.Error())
}
s.log.Debug().Interface("account", out).Msg("account after indexing")
for _, r := range indexResults {
if r.Field == "UidNumber" {
id, err := strconv.Atoi(path.Base(r.Value))
if err != nil {
s.rollbackCreateAccount(ctx, out)
return err
}
out.UidNumber = int64(id)
break
}
}
if out.GidNumber == 0 {
out.GidNumber = userDefaultGID
}
r := accountssvc.ListGroupsResponse{}
err = s.ListGroups(ctx, &accountssvc.ListGroupsRequest{}, &r)
if err != nil {
// rollback account creation
return err
}
for _, group := range r.Groups {
if group.GidNumber == out.GidNumber {
out.MemberOf = append(out.MemberOf, group)
}
}
//acc.MemberOf = append(acc.MemberOf, &group)
if err := s.repo.WriteAccount(context.Background(), out); err != nil {
return err
}
if out.PasswordProfile != nil {
out.PasswordProfile.Password = ""
}
// TODO: assign user role to all new users for now, as create Account request does not have any role field
if s.RoleService == nil {
return merrors.InternalServerError(s.id, "could not assign role to account: roleService not configured")
}
if _, err = s.RoleService.AssignRoleToUser(ctx, &settingssvc.AssignRoleToUserRequest{
AccountUuid: out.Id,
RoleId: settings_svc.BundleUUIDRoleUser,
}); err != nil {
return merrors.InternalServerError(s.id, "could not assign role to account: %v", err.Error())
}
return
}
// rollbackCreateAccount tries to rollback changes made by `CreateAccount` if parts of it failed.
func (s Service) rollbackCreateAccount(ctx context.Context, acc *accountsmsg.Account) {
err := s.index.Delete(acc)
if err != nil {
s.log.Err(err).Msg("failed to rollback account from indices")
}
err = s.repo.DeleteAccount(ctx, acc.Id)
if err != nil {
s.log.Err(err).Msg("failed to rollback account from repo")
}
}
// UpdateAccount implements the AccountsServiceHandler interface
// read only fields are ignored
// TODO how can we unset specific values? using the update mask
func (s Service) UpdateAccount(ctx context.Context, in *accountssvc.UpdateAccountRequest, out *accountsmsg.Account) (err error) {
var span trace.Span
ctx, span = accTracing.TraceProvider.Tracer("accounts").Start(ctx, "Accounts.UpdateAccount")
defer span.End()
span.SetAttributes(
attribute.KeyValue{Key: "account", Value: attribute.StringValue(in.Account.String())},
)
hasSelf := s.hasSelfManagementPermissions(ctx)
hasManagement := s.hasAccountManagementPermissions(ctx)
if !hasSelf && !hasManagement {
return merrors.Forbidden(s.id, "no permission for UpdateAccount")
}
onlySelf := hasSelf && !hasManagement
var id string
if in.Account == nil {
return merrors.BadRequest(s.id, "account missing")
}
if in.Account.Id == "" {
return merrors.BadRequest(s.id, "account id missing")
}
if id, err = cleanupID(in.Account.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
if onlySelf {
// limit update to own account id
if aid, ok := metadata.Get(ctx, middleware.AccountID); ok {
if id != aid {
return merrors.Forbidden(s.id, "no permission to UpdateAccount of another user")
}
} else {
return merrors.InternalServerError(s.id, "account id not in context")
}
}
if err = s.repo.LoadAccount(ctx, id, out); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "account not found: %v", err.Error())
}
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
return merrors.InternalServerError(s.id, "could not load account: %v", err.Error())
}
t := time.Now()
tsnow := &timestamppb.Timestamp{
Seconds: t.Unix(),
Nanos: int32(t.Nanosecond()),
}
var validMask fieldmask_utils.FieldFilterContainer
if onlySelf {
if validMask, err = validateUpdate(in.UpdateMask, selfUpdatableAccountPaths); err != nil {
return merrors.BadRequest(s.id, "%s", err)
}
} else {
if validMask, err = validateUpdate(in.UpdateMask, updatableAccountPaths); err != nil {
return merrors.BadRequest(s.id, "%s", err)
}
}
if _, exists := validMask.Filter("PreferredName"); exists {
if err = validateAccountPreferredName(s.id, in.Account); err != nil {
return err
}
}
if _, exists := validMask.Filter("OnPremisesSamAccountName"); exists {
if err = validateAccountOnPremisesSamAccountName(s.id, in.Account); err != nil {
return err
}
}
if _, exists := validMask.Filter("Mail"); exists {
if in.Account.Mail != "" {
if err = validateAccountEmail(s.id, in.Account); err != nil {
return err
}
}
}
if err := fieldmask_utils.StructToStruct(validMask, in.Account, out); err != nil {
return merrors.InternalServerError(s.id, "%s", err)
}
if in.Account.PasswordProfile != nil {
if out.PasswordProfile == nil {
out.PasswordProfile = &accountsmsg.PasswordProfile{}
}
if in.Account.PasswordProfile.Password != "" {
// encrypt password
hashed, err := bcrypt.GenerateFromPassword([]byte(in.Account.PasswordProfile.Password), s.Config.HashDifficulty)
if err != nil {
in.Account.PasswordProfile.Password = ""
s.log.Error().Err(err).Str("id", id).Msg("could not hash password")
return merrors.InternalServerError(s.id, "could not hash password: %v", err.Error())
}
out.PasswordProfile.Password = string(hashed)
in.Account.PasswordProfile.Password = ""
}
if err := passwordPoliciesValid(in.Account.PasswordProfile.PasswordPolicies); err != nil {
return merrors.BadRequest(s.id, "%s", err)
}
// lastPasswordChangeDateTime calculated, see password
out.PasswordProfile.LastPasswordChangeDateTime = tsnow
}
// out.RefreshTokensValidFromDateTime TODO use to invalidate all existing sessions
// out.SignInSessionsValidFromDateTime TODO use to invalidate all existing sessions
// ... TODO on prem for sync
if out.ExternalUserState != in.Account.ExternalUserState {
out.ExternalUserState = in.Account.ExternalUserState
out.ExternalUserStateChangeDateTime = tsnow
}
// We need to reload the old account state to be able to compute the update
old := &accountsmsg.Account{}
if err = s.repo.LoadAccount(ctx, id, old); err != nil {
s.log.Error().Err(err).Str("id", out.Id).Msg("could not load old account representation during update, maybe the account got deleted meanwhile?")
return merrors.InternalServerError(s.id, "could not load current account for update: %v", err.Error())
}
if err = s.repo.WriteAccount(ctx, out); err != nil {
s.log.Error().Err(err).Str("id", out.Id).Msg("could not persist updated account")
return merrors.InternalServerError(s.id, "could not persist updated account: %v", err.Error())
}
if err = s.index.Update(old, out); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not index new account")
return merrors.InternalServerError(s.id, "could not index updated account: %v", err.Error())
}
// remove password
if out.PasswordProfile != nil {
out.PasswordProfile.Password = ""
}
return
}
// whitelist of all paths/fields which can be updated by users themselves
var selfUpdatableAccountPaths = map[string]struct{}{
"DisplayName": {},
"Description": {},
"Mail": {}, // read only?,
"PasswordProfile.Password": {},
}
// whitelist of all paths/fields which can be updated by clients
var updatableAccountPaths = map[string]struct{}{
"AccountEnabled": {},
"IsResourceAccount": {},
"Identities": {},
"DisplayName": {},
"PreferredName": {},
"UidNumber": {},
"GidNumber": {},
"Description": {},
"Mail": {}, // read only?,
"PasswordProfile.Password": {},
"PasswordProfile.PasswordPolicies": {},
"PasswordProfile.ForceChangePasswordNextSignIn": {},
"PasswordProfile.ForceChangePasswordNextSignInWithMfa": {},
"OnPremisesSyncEnabled": {},
"OnPremisesSamAccountName": {},
}
// DeleteAccount implements the AccountsServiceHandler interface
func (s Service) DeleteAccount(ctx context.Context, in *accountssvc.DeleteAccountRequest, out *empty.Empty) (err error) {
var span trace.Span
ctx, span = accTracing.TraceProvider.Tracer("accounts").Start(ctx, "Accounts.DeleteAccount")
defer span.End()
if !s.hasAccountManagementPermissions(ctx) {
return merrors.Forbidden(s.id, "no permission for DeleteAccount")
}
var id string
if id, err = cleanupID(in.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
a := &accountsmsg.Account{}
if err = s.repo.LoadAccount(ctx, id, a); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "account not found: %v", err.Error())
}
s.log.Error().Err(err).Str("id", id).Msg("could not load account")
return merrors.InternalServerError(s.id, "could not load account: %v", err.Error())
}
// delete member relationship in groups
for i := range a.MemberOf {
err = s.RemoveMember(ctx, &accountssvc.RemoveMemberRequest{
GroupId: a.MemberOf[i].Id,
AccountId: id,
}, a.MemberOf[i])
if err != nil {
s.log.Error().Err(err).Str("accountid", id).Str("groupid", a.MemberOf[i].Id).Msg("could not remove group member, skipping")
}
}
if err = s.repo.DeleteAccount(ctx, id); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "account not found: %v", err.Error())
}
s.log.Error().Err(err).Str("id", id).Str("accountId", id).Msg("could not remove account")
return merrors.InternalServerError(s.id, "could not remove account: %v", err.Error())
}
if err = s.index.Delete(a); err != nil {
s.log.Error().Err(err).Str("id", id).Str("accountId", id).Msg("could not remove account from index")
return merrors.InternalServerError(s.id, "could not remove account from index: %v", err.Error())
}
s.log.Info().Str("id", id).Msg("deleted account")
return
}
func validateAccount(serviceID string, a *accountsmsg.Account) error {
if err := validateAccountPreferredName(serviceID, a); err != nil {
return err
}
if err := validateAccountOnPremisesSamAccountName(serviceID, a); err != nil {
return err
}
if err := validateAccountEmail(serviceID, a); err != nil {
return err
}
return nil
}
func validateAccountPreferredName(serviceID string, a *accountsmsg.Account) error {
if !isValidUsername(a.PreferredName) {
return merrors.BadRequest(serviceID, "preferred_name '%s' must be at least the local part of an email", a.PreferredName)
}
return nil
}
func validateAccountOnPremisesSamAccountName(serviceID string, a *accountsmsg.Account) error {
if !isValidUsername(a.OnPremisesSamAccountName) {
return merrors.BadRequest(serviceID, "on_premises_sam_account_name '%s' must be at least the local part of an email", a.OnPremisesSamAccountName)
}
return nil
}
func validateAccountEmail(serviceID string, a *accountsmsg.Account) error {
if !isValidEmail(a.Mail) {
return merrors.BadRequest(serviceID, "mail '%s' must be a valid email", a.Mail)
}
return nil
}
// We want to allow email addresses as usernames so they show up when using them in ACLs on storages that allow integration with our glauth LDAP service
// so we are adding a few restrictions from https://stackoverflow.com/questions/6949667/what-are-the-real-rules-for-linux-usernames-on-centos-6-and-rhel-6
// names should not start with numbers
var usernameRegex = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]*(@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)*$")
func isValidUsername(e string) bool {
if len(e) < 1 && len(e) > 254 {
return false
}
return usernameRegex.MatchString(e)
}
// regex from https://www.w3.org/TR/2016/REC-html51-20161101/sec-forms.html#valid-e-mail-address
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
func isValidEmail(e string) bool {
if len(e) < 3 && len(e) > 254 {
return false
}
return emailRegex.MatchString(e)
}
const (
policyDisableStrongPassword = "DisableStrongPassword"
policyDisablePasswordExpiration = "DisablePasswordExpiration"
)
func passwordPoliciesValid(policies []string) error {
for _, v := range policies {
if v != policyDisableStrongPassword && v != policyDisablePasswordExpiration {
return fmt.Errorf("invalid password-policy %s", v)
}
}
return nil
}
// validateUpdate takes a update field-mask and validates it against a whitelist of updatable paths.
// Returns a FieldFilter on success which can be passed to the fieldmask_utils..StructToStruct. An error is returned
// if the mask tries to update no whitelisted fields.
//
// Given an empty or nil mask we assume that the client wants to update all whitelisted fields.
//
func validateUpdate(mask *field_mask.FieldMask, updatablePaths map[string]struct{}) (fieldmask_utils.FieldFilterContainer, error) {
nop := func(s string) string { return s }
// Assume that the client wants to update all updatable path if
// no field-mask is given, so we create a mask with all paths
if mask == nil || len(mask.Paths) == 0 {
paths := make([]string, 0, len(updatablePaths))
for fieldName := range updatablePaths {
paths = append(paths, fieldName)
}
return fieldmask_utils.MaskFromPaths(paths, nop)
}
// Check that only allowed fields are updated
for _, v := range mask.Paths {
if _, ok := updatablePaths[v]; !ok {
return nil, fmt.Errorf("can not update field %s, either unknown or readonly", v)
}
}
return fieldmask_utils.MaskFromPaths(mask.Paths, nop)
}
// debugLogAccount returns a debug-log event with detailed account-info, and filtered password data
func (s Service) debugLogAccount(a *accountsmsg.Account) *zerolog.Event {
return s.log.Debug().Fields(map[string]interface{}{
"Id": a.Id,
"Mail": a.Mail,
"DisplayName": a.DisplayName,
"AccountEnabled": a.AccountEnabled,
"IsResourceAccount": a.IsResourceAccount,
"Identities": a.Identities,
"PreferredName": a.PreferredName,
"UidNumber": a.UidNumber,
"GidNumber": a.GidNumber,
"Description": a.Description,
"OnPremisesSyncEnabled": a.OnPremisesSyncEnabled,
"OnPremisesSamAccountName": a.OnPremisesSamAccountName,
"OnPremisesUserPrincipalName": a.OnPremisesUserPrincipalName,
"OnPremisesSecurityIdentifier": a.OnPremisesSecurityIdentifier,
"OnPremisesDistinguishedName": a.OnPremisesDistinguishedName,
"OnPremisesLastSyncDateTime": a.OnPremisesLastSyncDateTime,
"MemberOf": a.MemberOf,
"CreatedDateTime": a.CreatedDateTime,
"DeletedDateTime": a.DeletedDateTime,
})
}
func (s Service) accountExists(ctx context.Context, username, mail, id string) (exists bool, err error) {
var ids []string
ids, err = s.index.FindBy(&accountsmsg.Account{}, "preferred_name", username)
if err != nil {
return false, err
}
if len(ids) > 0 {
return true, nil
}
ids, err = s.index.FindBy(&accountsmsg.Account{}, "on_premises_sam_account_name", username)
if err != nil {
return false, err
}
if len(ids) > 0 {
return true, nil
}
ids, err = s.index.FindBy(&accountsmsg.Account{}, "mail", mail)
if err != nil {
return false, err
}
if len(ids) > 0 {
return true, nil
}
a := &accountsmsg.Account{}
err = s.repo.LoadAccount(ctx, id, a)
if err == nil {
return true, nil
}
if !storage.IsNotFoundErr(err) {
return true, err
}
return false, nil
}
func getAuthQueryMatch(query string) (match []string, authRequest bool) {
match = authQuery.FindStringSubmatch(query)
return match, len(match) == 3
}
func isPasswordValid(logger log.Logger, hash string, pwd string) (ok bool) {
defer func() {
if r := recover(); r != nil {
logger.Error().Err(fmt.Errorf("%s", r)).Str("hash", hash).Msg("password lib panicked")
}
}()
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pwd)) == nil
}
func mustWrite(w io.Writer, val []byte) {
if _, err := w.Write(val); err != nil {
panic(err)
}
}

View File

@@ -1,369 +0,0 @@
package service
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"testing"
"time"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/golang/protobuf/ptypes/empty"
config "github.com/owncloud/ocis/v2/extensions/accounts/pkg/config/defaults"
ssvc "github.com/owncloud/ocis/v2/extensions/settings/pkg/service/v0"
olog "github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/stretchr/testify/assert"
"go-micro.dev/v4/client"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
)
const dataPath = "/tmp/ocis-accounts-tests"
var (
roleServiceMock settingssvc.RoleService
s *Service
)
func init() {
cfg := config.DefaultConfig()
cfg.Repo.Backend = "disk"
cfg.Repo.Disk.Path = dataPath
logger := olog.NewLogger(olog.Color(true), olog.Pretty(true))
roleServiceMock = buildRoleServiceMock()
roleManager := roles.NewManager(
roles.Logger(logger),
roles.RoleService(roleServiceMock),
roles.CacheTTL(time.Hour),
roles.CacheSize(1024),
)
s, _ = New(
Logger(logger),
Config(cfg),
RoleService(roleServiceMock),
RoleManager(&roleManager),
)
}
func setup() (teardown func()) {
return func() {
if err := os.RemoveAll(dataPath); err != nil {
log.Printf("could not delete data root: %s", dataPath)
} else {
log.Println("data root deleted")
}
}
}
// TestPermissionsListAccounts checks permission handling on ListAccounts
func TestPermissionsListAccounts(t *testing.T) {
var scenarios = []struct {
name string
roleIDs []string
query string
permissionError error
}{
// TODO: remove this test when https://github.com/owncloud/ocis/v2/accounts/pull/111 is merged
// replace with two tests:
// 1: "ListAccounts fails with 403 when roleIDs don't exist in context"
// 2: "ListAccounts fails with 403 when ('no admin role in context' AND 'empty query')"
{
"ListAccounts succeeds when no roleIDs in context",
nil,
"",
nil,
},
{
"ListAccounts fails when no admin roleID in context",
[]string{ssvc.BundleUUIDRoleUser, ssvc.BundleUUIDRoleGuest},
"",
merrors.Forbidden(s.id, "no permission for ListAccounts"),
},
{
"ListAccounts succeeds when admin roleID in context",
[]string{ssvc.BundleUUIDRoleAdmin},
"",
nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
teardown := setup()
defer teardown()
ctx := buildTestCtx(t, scenario.roleIDs)
request := &accountssvc.ListAccountsRequest{
Query: scenario.query,
}
response := &accountssvc.ListAccountsResponse{}
err := s.ListAccounts(ctx, request, response)
if scenario.permissionError != nil {
assert.Equal(t, scenario.permissionError, err)
} else if err != nil {
// we are only checking permissions here, so just check that the error code is not 403
merr := merrors.FromError(err)
assert.NotEqual(t, http.StatusForbidden, merr.GetCode())
}
})
}
}
// TestPermissionsGetAccount checks permission handling on GetAccount
// TODO: remove this test function entirely, when https://github.com/owncloud/ocis/v2/accounts/pull/111 is merged. GetAccount will not have permission checks for the time being.
func TestPermissionsGetAccount(t *testing.T) {
var scenarios = []struct {
name string
roleIDs []string
permissionError error
}{
{
"GetAccount succeeds when no role IDs in context",
nil,
nil,
},
{
"GetAccount fails when no admin roleID in context",
[]string{ssvc.BundleUUIDRoleUser, ssvc.BundleUUIDRoleGuest},
merrors.Forbidden(s.id, "no permission for GetAccount"),
},
{
"GetAccount succeeds when admin roleID in context",
[]string{ssvc.BundleUUIDRoleAdmin},
nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
teardown := setup()
defer teardown()
ctx := buildTestCtx(t, scenario.roleIDs)
request := &accountssvc.GetAccountRequest{}
response := &accountsmsg.Account{}
err := s.GetAccount(ctx, request, response)
if scenario.permissionError != nil {
assert.Equal(t, scenario.permissionError, err)
} else if err != nil {
// we are only checking permissions here, so just check that the error code is not 403
merr := merrors.FromError(err)
assert.NotEqual(t, http.StatusForbidden, merr.GetCode())
}
})
}
}
// TestPermissionsCreateAccount checks permission handling on CreateAccount
func TestPermissionsCreateAccount(t *testing.T) {
var scenarios = []struct {
name string
roleIDs []string
permissionError error
}{
// TODO: remove this test when https://github.com/owncloud/ocis/v2/accounts/pull/111 is merged
// replace with two tests:
// 1: "CreateAccount fails with 403 when roleIDs don't exist in context"
// 2: "CreateAccount fails with 403 when no admin role in context"
{
"CreateAccount succeeds when no role IDs in context",
nil,
nil,
},
{
"CreateAccount fails when no admin roleID in context",
[]string{ssvc.BundleUUIDRoleUser, ssvc.BundleUUIDRoleGuest},
merrors.Forbidden(s.id, "no permission for CreateAccount"),
},
{
"CreateAccount succeeds when admin roleID in context",
[]string{ssvc.BundleUUIDRoleAdmin},
nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
teardown := setup()
defer teardown()
ctx := buildTestCtx(t, scenario.roleIDs)
request := &accountssvc.CreateAccountRequest{}
response := &accountsmsg.Account{}
err := s.CreateAccount(ctx, request, response)
if scenario.permissionError != nil {
assert.Equal(t, scenario.permissionError, err)
} else if err != nil {
// we are only checking permissions here, so just check that the error code is not 403
merr := merrors.FromError(err)
assert.NotEqual(t, http.StatusForbidden, merr.GetCode())
}
})
}
}
// TestPermissionsUpdateAccount checks permission handling on UpdateAccount
func TestPermissionsUpdateAccount(t *testing.T) {
var scenarios = []struct {
name string
roleIDs []string
permissionError error
}{
// TODO: remove this test when https://github.com/owncloud/ocis/v2/accounts/pull/111 is merged
// replace with two tests:
// 1: "UpdateAccount fails with 403 when roleIDs don't exist in context"
// 2: "UpdateAccount fails with 403 when no admin role in context"
{
"UpdateAccount succeeds when no role IDs in context",
nil,
nil,
},
{
"UpdateAccount fails when no admin roleID in context",
[]string{ssvc.BundleUUIDRoleUser, ssvc.BundleUUIDRoleGuest},
merrors.Forbidden(s.id, "no permission for UpdateAccount"),
},
{
"UpdateAccount succeeds when admin roleID in context",
[]string{ssvc.BundleUUIDRoleAdmin},
nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
teardown := setup()
defer teardown()
ctx := buildTestCtx(t, scenario.roleIDs)
request := &accountssvc.UpdateAccountRequest{}
response := &accountsmsg.Account{}
err := s.UpdateAccount(ctx, request, response)
if scenario.permissionError != nil {
assert.Equal(t, scenario.permissionError, err)
} else if err != nil {
// we are only checking permissions here, so just check that the error code is not 403
merr := merrors.FromError(err)
assert.NotEqual(t, http.StatusForbidden, merr.GetCode())
}
})
}
}
// TestPermissionsDeleteAccount checks permission handling on DeleteAccount
func TestPermissionsDeleteAccount(t *testing.T) {
var scenarios = []struct {
name string
roleIDs []string
permissionError error
}{
// TODO: remove this test when https://github.com/owncloud/ocis/v2/accounts/pull/111 is merged
// replace with two tests:
// 1: "DeleteAccount fails with 403 when roleIDs don't exist in context"
// 2: "DeleteAccount fails with 403 when no admin role in context"
{
"DeleteAccount succeeds when no role IDs in context",
nil,
nil,
},
{
"DeleteAccount fails when no admin roleID in context",
[]string{ssvc.BundleUUIDRoleUser, ssvc.BundleUUIDRoleGuest},
merrors.Forbidden(s.id, "no permission for DeleteAccount"),
},
{
"DeleteAccount succeeds when admin roleID in context",
[]string{ssvc.BundleUUIDRoleAdmin},
nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
teardown := setup()
defer teardown()
ctx := buildTestCtx(t, scenario.roleIDs)
request := &accountssvc.DeleteAccountRequest{}
response := &empty.Empty{}
err := s.DeleteAccount(ctx, request, response)
if scenario.permissionError != nil {
assert.Equal(t, scenario.permissionError, err)
} else if err != nil {
// we are only checking permissions here, so just check that the error code is not 403
merr := merrors.FromError(err)
assert.NotEqual(t, http.StatusForbidden, merr.GetCode())
}
})
}
}
func buildTestCtx(t *testing.T, roleIDs []string) context.Context {
ctx := context.Background()
if roleIDs != nil {
roleIDs, err := json.Marshal(roleIDs)
assert.NoError(t, err)
ctx = metadata.Set(ctx, middleware.RoleIDs, string(roleIDs))
}
return ctx
}
func buildRoleServiceMock() settingssvc.RoleService {
defaultRoles := map[string]*settingsmsg.Bundle{
ssvc.BundleUUIDRoleAdmin: {
Id: ssvc.BundleUUIDRoleAdmin,
Type: settingsmsg.Bundle_TYPE_ROLE,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{
{
Id: AccountManagementPermissionID,
},
},
},
ssvc.BundleUUIDRoleUser: {
Id: ssvc.BundleUUIDRoleUser,
Type: settingsmsg.Bundle_TYPE_ROLE,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{},
},
ssvc.BundleUUIDRoleGuest: {
Id: ssvc.BundleUUIDRoleGuest,
Type: settingsmsg.Bundle_TYPE_ROLE,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SYSTEM,
},
Settings: []*settingsmsg.Setting{},
},
}
return settingssvc.MockRoleService{
ListRolesFunc: func(ctx context.Context, req *settingssvc.ListBundlesRequest, opts ...client.CallOption) (res *settingssvc.ListBundlesResponse, err error) {
payload := make([]*settingsmsg.Bundle, 0)
for _, roleID := range req.BundleIds {
if defaultRoles[roleID] != nil {
payload = append(payload, defaultRoles[roleID])
}
}
return &settingssvc.ListBundlesResponse{
Bundles: payload,
}, nil
},
AssignRoleToUserFunc: func(ctx context.Context, req *settingssvc.AssignRoleToUserRequest, opts ...client.CallOption) (res *settingssvc.AssignRoleToUserResponse, err error) {
// mock can be empty. function is called during service start. actual role assignments not needed for the tests.
return &settingssvc.AssignRoleToUserResponse{
Assignment: &settingsmsg.UserRoleAssignment{},
}, nil
},
}
}

View File

@@ -1,384 +0,0 @@
package service
import (
"context"
"path"
"strconv"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/gofrs/uuid"
"github.com/golang/protobuf/ptypes/empty"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/storage"
merrors "go-micro.dev/v4/errors"
p "google.golang.org/protobuf/proto"
)
func (s Service) expandMembers(g *accountsmsg.Group) {
if g == nil {
return
}
expanded := []*accountsmsg.Account{}
for i := range g.Members {
// TODO resolve by name, when a create or update is issued they may not have an id? fall back to searching the group id in the index?
a := &accountsmsg.Account{}
if err := s.repo.LoadAccount(context.Background(), g.Members[i].Id, a); err == nil {
expanded = append(expanded, a)
} else {
// log errors but con/var/tmp/ocis-accounts-store-408341811tinue execution for now
s.log.Error().Err(err).Str("id", g.Members[i].Id).Msg("could not load account")
}
}
g.Members = expanded
}
// deflateMembers replaces the users of a group with an instance that only contains the id
func (s Service) deflateMembers(g *accountsmsg.Group) {
if g == nil {
return
}
deflated := []*accountsmsg.Account{}
for i := range g.Members {
if g.Members[i].Id != "" {
deflated = append(deflated, &accountsmsg.Account{Id: g.Members[i].Id})
} else {
// TODO fetch and use an id when group only has a name but no id
s.log.Error().Str("id", g.Id).Interface("account", g.Members[i]).Msg("resolving members by name is not implemented yet")
}
}
g.Members = deflated
}
// ListGroups implements the GroupsServiceHandler interface
func (s Service) ListGroups(ctx context.Context, in *accountssvc.ListGroupsRequest, out *accountssvc.ListGroupsResponse) (err error) {
if in.Query == "" {
err = s.repo.LoadGroups(ctx, &out.Groups)
if err != nil {
s.log.Err(err).Msg("failed to load all groups from storage")
return merrors.InternalServerError(s.id, "failed to load all groups")
}
for i := range out.Groups {
a := out.Groups[i]
// TODO add accounts only if requested
// if in.FieldMask ...
s.expandMembers(a)
}
return nil
}
searchResults, err := s.findGroupsByQuery(ctx, in.Query)
out.Groups = make([]*accountsmsg.Group, 0, len(searchResults))
for _, hit := range searchResults {
g := &accountsmsg.Group{}
if err = s.repo.LoadGroup(ctx, hit, g); err != nil {
s.log.Error().Err(err).Str("group", hit).Msg("could not load group, skipping")
continue
}
s.log.Debug().Interface("group", g).Msg("found group")
// TODO add accounts if requested
// if in.FieldMask ...
s.expandMembers(g)
out.Groups = append(out.Groups, g)
}
return
}
func (s Service) findGroupsByQuery(ctx context.Context, query string) ([]string, error) {
return s.index.Query(ctx, &accountsmsg.Group{}, query)
}
// GetGroup implements the GroupsServiceHandler interface
func (s Service) GetGroup(c context.Context, in *accountssvc.GetGroupRequest, out *accountsmsg.Group) (err error) {
var id string
if id, err = cleanupID(in.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
}
if err = s.repo.LoadGroup(c, id, out); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "group not found: %v", err.Error())
}
s.log.Error().Err(err).Str("id", id).Msg("could not load group")
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
}
s.log.Debug().Interface("group", out).Msg("found group")
// TODO only add accounts if requested
// if in.FieldMask ...
s.expandMembers(out)
return
}
// CreateGroup implements the GroupsServiceHandler interface
func (s Service) CreateGroup(c context.Context, in *accountssvc.CreateGroupRequest, out *accountsmsg.Group) (err error) {
if in.Group == nil {
return merrors.InternalServerError(s.id, "invalid group: empty")
}
p.Merge(out, in.Group)
if out.Id == "" {
out.Id = uuid.Must(uuid.NewV4()).String()
}
if _, err = cleanupID(out.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
s.deflateMembers(out)
if err = s.repo.WriteGroup(c, out); err != nil {
s.log.Error().Err(err).Interface("group", out).Msg("could not persist new group")
return merrors.InternalServerError(s.id, "could not persist new group: %v", err.Error())
}
indexResults, err := s.index.Add(out)
if err != nil {
s.rollbackCreateGroup(c, out)
return merrors.InternalServerError(s.id, "could not index new group: %v", err.Error())
}
for _, r := range indexResults {
if r.Field == "GidNumber" {
gid, err := strconv.Atoi(path.Base(r.Value))
if err != nil {
s.rollbackCreateGroup(c, out)
return err
}
out.GidNumber = int64(gid)
return s.repo.WriteGroup(context.Background(), out)
}
}
return
}
// rollbackCreateGroup tries to rollback changes made by `CreateGroup` if parts of it failed.
func (s Service) rollbackCreateGroup(ctx context.Context, group *accountsmsg.Group) {
err := s.index.Delete(group)
if err != nil {
s.log.Err(err).Msg("failed to rollback group from indices")
}
err = s.repo.DeleteGroup(ctx, group.Id)
if err != nil {
s.log.Err(err).Msg("failed to rollback group from repo")
}
}
// UpdateGroup implements the GroupsServiceHandler interface
func (s Service) UpdateGroup(c context.Context, in *accountssvc.UpdateGroupRequest, out *accountsmsg.Group) (err error) {
return merrors.InternalServerError(s.id, "not implemented")
}
// DeleteGroup implements the GroupsServiceHandler interface
func (s Service) DeleteGroup(c context.Context, in *accountssvc.DeleteGroupRequest, out *empty.Empty) (err error) {
var id string
if id, err = cleanupID(in.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
}
g := &accountsmsg.Group{}
if err = s.repo.LoadGroup(c, id, g); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "group not found: %v", err.Error())
}
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
}
// delete memberof relationship in users
for i := range g.Members {
err = s.RemoveMember(c, &accountssvc.RemoveMemberRequest{
AccountId: g.Members[i].Id,
GroupId: id,
}, g)
if err != nil {
s.log.Error().Err(err).Str("groupid", id).Str("accountid", g.Members[i].Id).Msg("could not remove account memberof, skipping")
}
}
if err = s.repo.DeleteGroup(c, id); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "group not found: %v", err.Error())
}
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
}
if err = s.index.Delete(g); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not remove group from index")
return merrors.InternalServerError(s.id, "could not remove group from index: %v", err.Error())
}
s.log.Info().Str("id", id).Msg("deleted group")
return
}
// AddMember implements the GroupsServiceHandler interface
func (s Service) AddMember(c context.Context, in *accountssvc.AddMemberRequest, out *accountsmsg.Group) (err error) {
// cleanup ids
var groupID string
if groupID, err = cleanupID(in.GroupId); err != nil {
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
}
var accountID string
if accountID, err = cleanupID(in.AccountId); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
// load structs
a := &accountsmsg.Account{}
if err = s.repo.LoadAccount(c, accountID, a); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "group not found: %v", err.Error())
}
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
}
g := &accountsmsg.Group{}
if err = s.repo.LoadGroup(c, groupID, g); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "could not load group: %v", err.Error())
}
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
}
// check if we need to add the account to the group
alreadyRelated := false
for i := range g.Members {
if g.Members[i].Id == a.Id {
alreadyRelated = true
}
}
aref := &accountsmsg.Account{
Id: a.Id,
}
if !alreadyRelated {
g.Members = append(g.Members, aref)
}
// check if we need to add the group to the account
alreadyRelated = false
for i := range a.MemberOf {
if a.MemberOf[i].Id == g.Id {
alreadyRelated = true
break
}
}
// only store the reference to prevent recursion when marshaling json
gref := &accountsmsg.Group{
Id: g.Id,
}
if !alreadyRelated {
a.MemberOf = append(a.MemberOf, gref)
}
if err = s.repo.WriteAccount(c, a); err != nil {
return merrors.InternalServerError(s.id, "could not persist account: %v", err.Error())
}
if err = s.repo.WriteGroup(c, g); err != nil {
return merrors.InternalServerError(s.id, "could not persist group: %v", err.Error())
}
// FIXME update index!
// TODO rollback changes when only one of them failed?
// TODO store relation in another file?
// TODO return error if they are already related?
return nil
}
// RemoveMember implements the GroupsServiceHandler interface
func (s Service) RemoveMember(c context.Context, in *accountssvc.RemoveMemberRequest, out *accountsmsg.Group) (err error) {
// cleanup ids
var groupID string
if groupID, err = cleanupID(in.GroupId); err != nil {
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
}
var accountID string
if accountID, err = cleanupID(in.AccountId); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
// load structs
a := &accountsmsg.Account{}
if err = s.repo.LoadAccount(c, accountID, a); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "could not load account: %v", err.Error())
}
s.log.Error().Err(err).Str("id", accountID).Msg("could not load account")
return merrors.InternalServerError(s.id, "could not load account: %v", err.Error())
}
g := &accountsmsg.Group{}
if err = s.repo.LoadGroup(c, groupID, g); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "could not load group: %v", err.Error())
}
s.log.Error().Err(err).Str("id", groupID).Msg("could not load group")
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
}
//remove the account from the group if it exists
newMembers := []*accountsmsg.Account{}
for i := range g.Members {
if g.Members[i].Id != a.Id {
newMembers = append(newMembers, g.Members[i])
}
}
g.Members = newMembers
// remove the group from the account if it exists
newGroups := []*accountsmsg.Group{}
for i := range a.MemberOf {
if a.MemberOf[i].Id != g.Id {
newGroups = append(newGroups, a.MemberOf[i])
}
}
a.MemberOf = newGroups
if err = s.repo.WriteAccount(c, a); err != nil {
s.log.Error().Err(err).Interface("account", a).Msg("could not persist account")
return merrors.InternalServerError(s.id, "could not persist account: %v", err.Error())
}
if err = s.repo.WriteGroup(c, g); err != nil {
s.log.Error().Err(err).Interface("group", g).Msg("could not persist group")
return merrors.InternalServerError(s.id, "could not persist group: %v", err.Error())
}
// FIXME update index!
// TODO rollback changes when only one of them failed?
// TODO store relation in another file?
// TODO return error if they are not related?
return nil
}
// ListMembers implements the GroupsServiceHandler interface
func (s Service) ListMembers(c context.Context, in *accountssvc.ListMembersRequest, out *accountssvc.ListMembersResponse) (err error) {
// cleanup ids
var groupID string
if groupID, err = cleanupID(in.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
}
g := &accountsmsg.Group{}
if err = s.repo.LoadGroup(c, groupID, g); err != nil {
if storage.IsNotFoundErr(err) {
return merrors.NotFound(s.id, "group not found: %v", err.Error())
}
s.log.Error().Err(err).Str("id", groupID).Msg("could not load group")
return merrors.InternalServerError(s.id, "could not load group: %v", err.Error())
}
// TODO only expand accounts if requested
// if in.FieldMask ...
s.expandMembers(g)
out.Members = g.Members
return
}

View File

@@ -1,110 +0,0 @@
package service
import (
"context"
"fmt"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/storage"
"github.com/owncloud/ocis/v2/ocis-pkg/indexer"
"github.com/owncloud/ocis/v2/ocis-pkg/indexer/config"
"github.com/owncloud/ocis/v2/ocis-pkg/indexer/option"
)
// RebuildIndex deletes all indices (in memory and on storage) and rebuilds them from scratch.
func (s Service) RebuildIndex(ctx context.Context, request *accountssvc.RebuildIndexRequest, response *accountssvc.RebuildIndexResponse) error {
if err := s.index.Reset(); err != nil {
return fmt.Errorf("failed to delete index containers: %w", err)
}
c, err := configFromSvc(s.Config)
if err != nil {
return err
}
if err := recreateContainers(s.index, c); err != nil {
return fmt.Errorf("failed to recreate index containers: %w", err)
}
if err := reindexDocuments(ctx, s.repo, s.index); err != nil {
return fmt.Errorf("failed to reindex documents: %w", err)
}
return nil
}
// recreateContainers adds all indices to the indexer that we have for this service.
func recreateContainers(idx *indexer.Indexer, cfg *config.Config) error {
// Accounts
if err := idx.AddIndex(&accountsmsg.Account{}, "Id", "Id", "accounts", "non_unique", nil, true); err != nil {
return err
}
if err := idx.AddIndex(&accountsmsg.Account{}, "DisplayName", "Id", "accounts", "non_unique", nil, true); err != nil {
return err
}
if err := idx.AddIndex(&accountsmsg.Account{}, "Mail", "Id", "accounts", "unique", nil, true); err != nil {
return err
}
if err := idx.AddIndex(&accountsmsg.Account{}, "OnPremisesSamAccountName", "Id", "accounts", "unique", nil, true); err != nil {
return err
}
if err := idx.AddIndex(&accountsmsg.Account{}, "PreferredName", "Id", "accounts", "unique", nil, true); err != nil {
return err
}
if err := idx.AddIndex(&accountsmsg.Account{}, "UidNumber", "Id", "accounts", "autoincrement", &option.Bound{
Lower: cfg.Index.UID.Lower,
Upper: cfg.Index.UID.Upper,
}, false); err != nil {
return err
}
// Groups
if err := idx.AddIndex(&accountsmsg.Group{}, "OnPremisesSamAccountName", "Id", "groups", "unique", nil, false); err != nil {
return err
}
if err := idx.AddIndex(&accountsmsg.Group{}, "DisplayName", "Id", "groups", "non_unique", nil, false); err != nil {
return err
}
if err := idx.AddIndex(&accountsmsg.Group{}, "GidNumber", "Id", "groups", "autoincrement", &option.Bound{
Lower: cfg.Index.GID.Lower,
Upper: cfg.Index.GID.Upper,
}, false); err != nil {
return err
}
return nil
}
// reindexDocuments loads all existing documents and adds them to the index.
func reindexDocuments(ctx context.Context, repo storage.Repo, index *indexer.Indexer) error {
accounts := make([]*accountsmsg.Account, 0)
if err := repo.LoadAccounts(ctx, &accounts); err != nil {
return err
}
for i := range accounts {
_, err := index.Add(accounts[i])
if err != nil {
return err
}
}
groups := make([]*accountsmsg.Group, 0)
if err := repo.LoadGroups(ctx, &groups); err != nil {
return err
}
for i := range groups {
_, err := index.Add(groups[i])
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,57 +0,0 @@
package service
import (
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
)
// 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
RoleService settingssvc.RoleService
RoleManager *roles.Manager
}
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
}
}
// RoleService provides a function to set the RoleService option.
func RoleService(val settingssvc.RoleService) Option {
return func(o *Options) {
o.RoleService = val
}
}
// RoleManager provides a function to set the RoleManager option.
func RoleManager(val *roles.Manager) Option {
return func(o *Options) {
o.RoleManager = val
}
}

View File

@@ -1,105 +0,0 @@
package service
import (
"context"
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
ssvc "github.com/owncloud/ocis/v2/extensions/settings/pkg/service/v0"
olog "github.com/owncloud/ocis/v2/ocis-pkg/log"
settingsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/settings/v0"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
)
const (
// 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"
)
// RegisterPermissions registers permissions for account management and group management with the settings service.
func RegisterPermissions(l *olog.Logger) {
service := settingssvc.NewBundleService("com.owncloud.api.settings", grpc.DefaultClient)
permissionRequests := generateAccountManagementPermissionsRequests()
for i := range permissionRequests {
res, err := service.AddSettingToBundle(context.Background(), &permissionRequests[i])
bundleID := permissionRequests[i].BundleId
if err != nil {
l.Err(err).Str("bundle", bundleID).Str("setting", permissionRequests[i].Setting.Id).Msg("error adding permission to bundle")
} else {
l.Info().Str("bundle", bundleID).Str("setting", res.Setting.Id).Msg("successfully added permission to bundle")
}
}
}
func generateAccountManagementPermissionsRequests() []settingssvc.AddSettingToBundleRequest {
return []settingssvc.AddSettingToBundleRequest{
{
BundleId: ssvc.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: ssvc.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: ssvc.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,
},
},
},
},
}
}

View File

@@ -1,508 +0,0 @@
package service
import (
"context"
"path"
"path/filepath"
"strconv"
"strings"
"time"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
"github.com/pkg/errors"
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/storage"
"github.com/owncloud/ocis/v2/ocis-pkg/indexer"
idxcfg "github.com/owncloud/ocis/v2/ocis-pkg/indexer/config"
idxerrs "github.com/owncloud/ocis/v2/ocis-pkg/indexer/errors"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
oreg "github.com/owncloud/ocis/v2/ocis-pkg/registry"
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
)
// userDefaultGID is the default integer representing the "users" group.
const userDefaultGID = 30000
// New returns a new instance of Service
func New(opts ...Option) (s *Service, err error) {
options := newOptions(opts...)
logger := options.Logger
cfg := options.Config
roleService := options.RoleService
if roleService == nil {
roleService = settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient)
}
roleManager := options.RoleManager
if roleManager == nil {
m := roles.NewManager(
roles.CacheSize(1024),
roles.CacheTTL(time.Hour*24*7),
roles.Logger(options.Logger),
roles.RoleService(roleService),
)
roleManager = &m
}
storage, err := createMetadataStorage(cfg, logger)
if err != nil {
return nil, errors.Wrap(err, "could not create metadata storage")
}
s = &Service{
id: cfg.GRPC.Namespace + "." + cfg.Service.Name,
log: logger,
Config: cfg,
RoleService: roleService,
RoleManager: roleManager,
repo: storage,
}
r := oreg.GetRegistry()
if cfg.Repo.Backend == "cs3" {
if _, err := r.GetService("com.owncloud.storage.metadata"); err != nil {
logger.Error().Err(err).Msg("index: storage-system service not present")
return nil, err
}
}
// we want to wait anyway. If it depends on a reva service it could be the case that the entry on the registry
// happens prior to the reva service being up and running
time.Sleep(500 * time.Millisecond)
if s.index, err = s.buildIndex(); err != nil {
return nil, err
}
if err = s.createDefaultAccounts(cfg.DemoUsersAndGroups); err != nil {
return nil, err
}
if err = s.createDefaultGroups(cfg.DemoUsersAndGroups); err != nil {
return nil, err
}
s.serviceUserToIndex()
return
}
// serviceUserToIndex temporarily adds a service user to the index, which is supposed to be removed before the lock on the handler function is released
func (s Service) serviceUserToIndex() {
if s.Config.ServiceUser.Username != "" && s.Config.ServiceUser.UUID != "" {
_, err := s.index.Add(s.getInMemoryServiceUser())
if err != nil {
s.log.Logger.Err(err).Msg("service user was configured but failed to be added to the index")
}
}
}
func (s Service) getInMemoryServiceUser() accountsmsg.Account {
return accountsmsg.Account{
AccountEnabled: true,
Id: s.Config.ServiceUser.UUID,
PreferredName: s.Config.ServiceUser.Username,
OnPremisesSamAccountName: s.Config.ServiceUser.Username,
DisplayName: s.Config.ServiceUser.Username,
UidNumber: s.Config.ServiceUser.UID,
GidNumber: s.Config.ServiceUser.GID,
}
}
func (s Service) buildIndex() (*indexer.Indexer, error) {
var indexcfg *idxcfg.Config
indexcfg, err := configFromSvc(s.Config)
if err != nil {
return nil, err
}
idx := indexer.CreateIndexer(indexcfg)
if err := recreateContainers(idx, indexcfg); err != nil {
return nil, err
}
return idx, nil
}
// configFromSvc creates an index config out of a service configuration. This intermediate step exists
// because the index config was mapped after the service config.
func configFromSvc(cfg *config.Config) (*idxcfg.Config, error) {
c := idxcfg.New()
if cfg.Log == nil {
cfg.Log = &config.Log{}
}
defer func(cfg *config.Config) {
l := log.NewLogger(log.Color(cfg.Log.Color), log.Pretty(cfg.Log.Pretty), log.Level(cfg.Log.Level))
if r := recover(); r != nil {
l.Error().
Str("panic", "recovered from panic while parsing index config from service configuration").
Interface("svc_config", cfg).
Msg("recovered from panic")
}
}(cfg)
switch cfg.Repo.Backend {
case "disk":
c.Repo = idxcfg.Repo{
Backend: cfg.Repo.Backend,
Disk: idxcfg.Disk{
Path: cfg.Repo.Disk.Path,
},
}
case "cs3":
c.Repo = idxcfg.Repo{
Backend: cfg.Repo.Backend,
CS3: idxcfg.CS3{
ProviderAddr: cfg.Repo.CS3.ProviderAddr,
JWTSecret: cfg.TokenManager.JWTSecret,
},
}
default:
return nil, errors.New("index backend " + cfg.Repo.Backend + " is not supported")
}
if (config.Index{}) != cfg.Index {
c.Index = idxcfg.Index{
UID: idxcfg.Bound{
Lower: cfg.Index.UID.Lower,
},
GID: idxcfg.Bound{
Lower: cfg.Index.GID.Lower,
},
}
}
if (config.ServiceUser{}) != cfg.ServiceUser {
c.ServiceUser = cfg.ServiceUser
}
return c, nil
}
func (s Service) createDefaultAccounts(withDemoAccounts bool) (err error) {
accounts := []accountsmsg.Account{
{
Id: "4c510ada-c86b-4815-8820-42cdf82c3d51",
PreferredName: "einstein",
OnPremisesSamAccountName: "einstein",
Mail: "einstein@example.org",
DisplayName: "Albert Einstein",
UidNumber: 20000,
GidNumber: 30000,
PasswordProfile: &accountsmsg.PasswordProfile{
Password: "$2a$04$L.Rkpa0/nOhF3SsFo.QY9uzjMG8zB9a8dZP./LZBCDgsiuI8w10Em",
},
AccountEnabled: true,
MemberOf: []*accountsmsg.Group{
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
{Id: "6040aa17-9c64-4fef-9bd0-77234d71bad0"}, // sailing-lovers
{Id: "dd58e5ec-842e-498b-8800-61f2ec6f911f"}, // violin-haters
{Id: "262982c1-2362-4afa-bfdf-8cbfef64a06e"}, // physics-lovers
},
},
{
Id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c",
PreferredName: "marie",
OnPremisesSamAccountName: "marie",
Mail: "marie@example.org",
DisplayName: "Marie Curie",
UidNumber: 20001,
GidNumber: 30000,
PasswordProfile: &accountsmsg.PasswordProfile{
Password: "$2a$04$AZd1k6OVpzP7E4hw5.ysFuuL2.XjjgakAuRs2zdBvIMizF0KaZkNG",
},
AccountEnabled: true,
MemberOf: []*accountsmsg.Group{
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
{Id: "7b87fd49-286e-4a5f-bafd-c535d5dd997a"}, // radium-lovers
{Id: "cedc21aa-4072-4614-8676-fa9165f598ff"}, // polonium-lovers
{Id: "262982c1-2362-4afa-bfdf-8cbfef64a06e"}, // physics-lovers
},
},
{
Id: "932b4540-8d16-481e-8ef4-588e4b6b151c",
PreferredName: "richard",
OnPremisesSamAccountName: "richard",
Mail: "richard@example.org",
DisplayName: "Richard Feynman",
UidNumber: 20002,
GidNumber: 30000,
PasswordProfile: &accountsmsg.PasswordProfile{
Password: "$2a$04$aeVYaBH3LCTj9DviV6Y4xO2reoEzY9vnc7a5/0mhJWQUDtPqPINme",
},
AccountEnabled: true,
MemberOf: []*accountsmsg.Group{
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
{Id: "a1726108-01f8-4c30-88df-2b1a9d1cba1a"}, // quantum-lovers
{Id: "167cbee2-0518-455a-bfb2-031fe0621e5d"}, // philosophy-haters
{Id: "262982c1-2362-4afa-bfdf-8cbfef64a06e"}, // physics-lovers
},
},
// admin user(s)
{
Id: "058bff95-6708-4fe5-91e4-9ea3d377588b",
PreferredName: "moss",
OnPremisesSamAccountName: "moss",
Mail: "moss@example.org",
DisplayName: "Maurice Moss",
UidNumber: 20003,
GidNumber: 30000,
PasswordProfile: &accountsmsg.PasswordProfile{
Password: "$2a$04$la2yFV6N.pPySwHnLIxyAuBCJ2t/DxWfXJGnIooA9Ebb3.lSTKXby",
},
AccountEnabled: true,
MemberOf: []*accountsmsg.Group{
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
},
},
{
Id: "ddc2004c-0977-11eb-9d3f-a793888cd0f8",
PreferredName: "admin",
OnPremisesSamAccountName: "admin",
Mail: "admin@example.org",
DisplayName: "Admin",
UidNumber: 20004,
GidNumber: 30000,
PasswordProfile: &accountsmsg.PasswordProfile{
Password: "$2a$04$zqpfwdtBUDg89cpltxd.9ef7ZMzsor1BLCJyTEcdoitmEuS3Hr/Q6",
},
AccountEnabled: true,
MemberOf: []*accountsmsg.Group{
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
},
},
{
Id: "534bb038-6f9d-4093-946f-133be61fa4e7",
PreferredName: "katherine",
OnPremisesSamAccountName: "katherine",
Mail: "katherine@example.org",
DisplayName: "Katherine Johnson",
UidNumber: 20005,
GidNumber: 30000,
PasswordProfile: &accountsmsg.PasswordProfile{
Password: "$2a$04$j0//gOyZ3xg/WtMOk4XUaOMJ1r5niD3paPcFh1O/PNr8pL7yC8rhG",
},
AccountEnabled: true,
MemberOf: []*accountsmsg.Group{
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa"}, // users
{Id: "6040aa17-9c64-4fef-9bd0-77234d71bad0"}, // sailing-lovers
{Id: "a1726108-01f8-4c30-88df-2b1a9d1cba1a"}, // quantum-lovers
{Id: "262982c1-2362-4afa-bfdf-8cbfef64a06e"}, // physics-lovers
},
},
// technical users for kopano and reva
{
Id: "820ba2a1-3f54-4538-80a4-2d73007e30bf",
PreferredName: "idp",
OnPremisesSamAccountName: "idp",
Mail: "idp@example.org",
DisplayName: "Kopano IDP",
UidNumber: 10000,
GidNumber: 15000,
PasswordProfile: &accountsmsg.PasswordProfile{
Password: "$2a$04$TiuPj61Lkwt9hPOj4UUdwO.fupKBO3gpMv1EoXo0XF8Z8L9rFN8Nm",
},
AccountEnabled: true,
MemberOf: []*accountsmsg.Group{
{Id: "34f38767-c937-4eb6-b847-1c175829a2a0"}, // sysusers
},
},
{
Id: "bc596f3c-c955-4328-80a0-60d018b4ad57",
PreferredName: "reva",
OnPremisesSamAccountName: "reva",
Mail: "storage@example.org",
DisplayName: "Reva Inter Operability Platform",
UidNumber: 10001,
GidNumber: 15000,
PasswordProfile: &accountsmsg.PasswordProfile{
Password: "$2a$04$.cYhDMMXsvoCJzH9rX0eKev7fsLZwUv.VsRn66iaCXj2KlgpzHu3a",
},
AccountEnabled: true,
MemberOf: []*accountsmsg.Group{
{Id: "34f38767-c937-4eb6-b847-1c175829a2a0"}, // sysusers
},
},
}
mustHaveAccounts := map[string]bool{
"bc596f3c-c955-4328-80a0-60d018b4ad57": true, // Reva IOP
"820ba2a1-3f54-4538-80a4-2d73007e30bf": true, // Kopano IDP
"ddc2004c-0977-11eb-9d3f-a793888cd0f8": true, // admin
}
// this only deals with the metadata service.
for i := range accounts {
if !withDemoAccounts && !mustHaveAccounts[accounts[i].Id] {
continue
}
a := &accountsmsg.Account{}
err := s.repo.LoadAccount(context.Background(), accounts[i].Id, a)
if !storage.IsNotFoundErr(err) {
continue // account already exists -> do not overwrite
}
if err := s.repo.WriteAccount(context.Background(), &accounts[i]); err != nil {
return err
}
results, err := s.index.Add(&accounts[i])
if err != nil {
if idxerrs.IsAlreadyExistsErr(err) {
continue
} else {
return err
}
}
changed := false
for _, r := range results {
if r.Field == "UidNumber" || r.Field == "GidNumber" {
id, err := strconv.ParseInt(path.Base(r.Value), 10, 0)
if err != nil {
return err
}
if r.Field == "UidNumber" {
accounts[i].UidNumber = id
} else {
accounts[i].GidNumber = id
}
changed = true
}
}
if changed {
if err := s.repo.WriteAccount(context.Background(), &accounts[i]); err != nil {
return err
}
}
}
return nil
}
func (s Service) createDefaultGroups(withDemoGroups bool) (err error) {
groups := []accountsmsg.Group{
{Id: "34f38767-c937-4eb6-b847-1c175829a2a0", GidNumber: 15000, OnPremisesSamAccountName: "sysusers", DisplayName: "Technical users", Description: "A group for technical users. They should not show up in sharing dialogs.", Members: []*accountsmsg.Account{
{Id: "820ba2a1-3f54-4538-80a4-2d73007e30bf"}, // idp
{Id: "bc596f3c-c955-4328-80a0-60d018b4ad57"}, // reva
}},
{Id: "509a9dcd-bb37-4f4f-a01a-19dca27d9cfa", GidNumber: 30000, OnPremisesSamAccountName: "users", DisplayName: "Users", Description: "A group every normal user belongs to.", Members: []*accountsmsg.Account{
{Id: "4c510ada-c86b-4815-8820-42cdf82c3d51"}, // einstein
{Id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}, // marie
{Id: "932b4540-8d16-481e-8ef4-588e4b6b151c"}, // feynman
{Id: "534bb038-6f9d-4093-946f-133be61fa4e7"}, // katherine
}},
{Id: "6040aa17-9c64-4fef-9bd0-77234d71bad0", GidNumber: 30001, OnPremisesSamAccountName: "sailing-lovers", DisplayName: "Sailing lovers", Members: []*accountsmsg.Account{
{Id: "4c510ada-c86b-4815-8820-42cdf82c3d51"}, // einstein
{Id: "534bb038-6f9d-4093-946f-133be61fa4e7"}, // katherine
}},
{Id: "dd58e5ec-842e-498b-8800-61f2ec6f911f", GidNumber: 30002, OnPremisesSamAccountName: "violin-haters", DisplayName: "Violin haters", Members: []*accountsmsg.Account{
{Id: "4c510ada-c86b-4815-8820-42cdf82c3d51"}, // einstein
}},
{Id: "7b87fd49-286e-4a5f-bafd-c535d5dd997a", GidNumber: 30003, OnPremisesSamAccountName: "radium-lovers", DisplayName: "Radium lovers", Members: []*accountsmsg.Account{
{Id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}, // marie
}},
{Id: "cedc21aa-4072-4614-8676-fa9165f598ff", GidNumber: 30004, OnPremisesSamAccountName: "polonium-lovers", DisplayName: "Polonium lovers", Members: []*accountsmsg.Account{
{Id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}, // marie
}},
{Id: "a1726108-01f8-4c30-88df-2b1a9d1cba1a", GidNumber: 30005, OnPremisesSamAccountName: "quantum-lovers", DisplayName: "Quantum lovers", Members: []*accountsmsg.Account{
{Id: "932b4540-8d16-481e-8ef4-588e4b6b151c"}, // feynman
{Id: "534bb038-6f9d-4093-946f-133be61fa4e7"}, // katherine
}},
{Id: "167cbee2-0518-455a-bfb2-031fe0621e5d", GidNumber: 30006, OnPremisesSamAccountName: "philosophy-haters", DisplayName: "Philosophy haters", Members: []*accountsmsg.Account{
{Id: "932b4540-8d16-481e-8ef4-588e4b6b151c"}, // feynman
}},
{Id: "262982c1-2362-4afa-bfdf-8cbfef64a06e", GidNumber: 30007, OnPremisesSamAccountName: "physics-lovers", DisplayName: "Physics lovers", Members: []*accountsmsg.Account{
{Id: "4c510ada-c86b-4815-8820-42cdf82c3d51"}, // einstein
{Id: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}, // marie
{Id: "932b4540-8d16-481e-8ef4-588e4b6b151c"}, // feynman
{Id: "534bb038-6f9d-4093-946f-133be61fa4e7"}, // katherine
}},
}
mustHaveGroups := map[string]bool{
"34f38767-c937-4eb6-b847-1c175829a2a0": true, // sysusers
"509a9dcd-bb37-4f4f-a01a-19dca27d9cfa": true, // users
}
for i := range groups {
if !withDemoGroups && !mustHaveGroups[groups[i].Id] {
continue
}
g := &accountsmsg.Group{}
err := s.repo.LoadGroup(context.Background(), groups[i].Id, g)
if !storage.IsNotFoundErr(err) {
continue // group already exists -> do not overwrite
}
if err := s.repo.WriteGroup(context.Background(), &groups[i]); err != nil {
return err
}
results, err := s.index.Add(&groups[i])
if err != nil {
if idxerrs.IsAlreadyExistsErr(err) {
continue
} else {
return err
}
}
// TODO: can be removed again as soon as we respect the predefined GIDs from the group. Then no autoincrement is happening, therefore we don't need to update groups.
for _, r := range results {
if r.Field == "GidNumber" {
gid, err := strconv.ParseInt(path.Base(r.Value), 10, 0)
if err != nil {
return err
}
groups[i].GidNumber = gid
if err := s.repo.WriteGroup(context.Background(), &groups[i]); err != nil {
return err
}
break
}
}
}
return nil
}
func createMetadataStorage(cfg *config.Config, logger log.Logger) (storage.Repo, error) {
switch cfg.Repo.Backend {
case "disk":
return storage.NewDiskRepo(cfg, logger), nil
case "cs3":
repo, err := storage.NewCS3Repo(cfg)
if err != nil {
return nil, errors.Wrap(err, "cs3 backend was configured but failed to start")
}
return repo, nil
default:
return nil, errors.New("backend type " + cfg.Repo.Backend + " is not supported")
}
}
// Service implements the AccountsServiceHandler interface
type Service struct {
id string
log log.Logger
Config *config.Config
index *indexer.Indexer
RoleService settingssvc.RoleService
RoleManager *roles.Manager
repo storage.Repo
}
func cleanupID(id string) (string, error) {
id = filepath.Clean(id)
if id == "." || strings.Contains(id, "/") {
return "", errors.New("invalid id " + id)
}
return id, nil
}

View File

@@ -1 +0,0 @@
checks = ["all", "-ST1003", "-ST1000", "-SA1019"]

View File

@@ -1,331 +0,0 @@
package storage
import (
"context"
"encoding/json"
"path"
"path/filepath"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
v1beta11 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/auth/scope"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/token"
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
olog "github.com/owncloud/ocis/v2/ocis-pkg/log"
metadatastorage "github.com/owncloud/ocis/v2/ocis-pkg/metadata_storage"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
"google.golang.org/grpc/metadata"
)
// CS3Repo provides a cs3 implementation of the Repo interface
type CS3Repo struct {
cfg *config.Config
tm token.Manager
storageProvider provider.ProviderAPIClient
metadataStorage *metadatastorage.MetadataStorage
}
// NewCS3Repo creates a new cs3 repo
func NewCS3Repo(cfg *config.Config) (Repo, error) {
tokenManager, err := jwt.New(map[string]interface{}{
"secret": cfg.TokenManager.JWTSecret,
})
if err != nil {
return nil, err
}
client, err := pool.GetStorageProviderServiceClient(cfg.Repo.CS3.ProviderAddr)
if err != nil {
return nil, err
}
ms, err := metadatastorage.NewMetadataStorage(cfg.Repo.CS3.ProviderAddr)
if err != nil {
return nil, err
}
r := CS3Repo{
cfg: cfg,
tm: tokenManager,
storageProvider: client,
metadataStorage: &ms,
}
ctx, err := r.getAuthenticatedContext(context.Background())
if err != nil {
return nil, err
}
if err := ms.Init(ctx, cfg.ServiceUser); err != nil {
return nil, err
}
return r, nil
}
// WriteAccount writes an account via cs3 and modifies the provided account (e.g. with a generated id).
func (r CS3Repo) WriteAccount(ctx context.Context, a *accountsmsg.Account) (err error) {
ctx, err = r.getAuthenticatedContext(ctx)
if err != nil {
return err
}
if err := r.makeRootDirIfNotExist(ctx, accountsFolder); err != nil {
return err
}
var by []byte
if by, err = json.Marshal(a); err != nil {
return err
}
err = r.metadataStorage.SimpleUpload(ctx, r.accountURL(a.Id), by)
return err
}
// LoadAccount loads an account via cs3 by id and writes it to the provided account
func (r CS3Repo) LoadAccount(ctx context.Context, id string, a *accountsmsg.Account) (err error) {
ctx, err = r.getAuthenticatedContext(ctx)
if err != nil {
return err
}
return r.loadAccount(ctx, id, a)
}
// LoadAccounts loads all the accounts from the cs3 api
func (r CS3Repo) LoadAccounts(ctx context.Context, a *[]*accountsmsg.Account) (err error) {
ctx, err = r.getAuthenticatedContext(ctx)
if err != nil {
return err
}
res, err := r.storageProvider.ListContainer(ctx, &provider.ListContainerRequest{
Ref: &provider.Reference{
ResourceId: r.metadataStorage.SpaceRoot,
Path: utils.MakeRelativePath(accountsFolder),
},
})
if err != nil {
return err
}
log := olog.NewLogger(olog.Pretty(r.cfg.Log.Pretty), olog.Color(r.cfg.Log.Color), olog.Level(r.cfg.Log.Level))
for i := range res.Infos {
acc := &accountsmsg.Account{}
err := r.loadAccount(ctx, filepath.Base(res.Infos[i].Path), acc)
if err != nil {
log.Err(err).Msg("could not load account")
continue
}
*a = append(*a, acc)
}
return nil
}
func (r CS3Repo) loadAccount(ctx context.Context, id string, a *accountsmsg.Account) error {
account, err := r.metadataStorage.SimpleDownload(ctx, r.accountURL(id))
if err != nil {
if metadatastorage.IsNotFoundErr(err) {
return &notFoundErr{"account", id}
}
return err
}
return json.Unmarshal(account, &a)
}
// DeleteAccount deletes an account via cs3 by id
func (r CS3Repo) DeleteAccount(ctx context.Context, id string) (err error) {
ctx, err = r.getAuthenticatedContext(ctx)
if err != nil {
return err
}
resp, err := r.storageProvider.Delete(ctx, &provider.DeleteRequest{
Ref: &provider.Reference{
ResourceId: r.metadataStorage.SpaceRoot,
Path: utils.MakeRelativePath(filepath.Join("/", accountsFolder, id)),
},
})
if err != nil {
return err
}
// TODO Handle other error codes?
if resp.Status.Code == v1beta11.Code_CODE_NOT_FOUND {
return &notFoundErr{"account", id}
}
return nil
}
// WriteGroup writes a group via cs3 and modifies the provided group (e.g. with a generated id).
func (r CS3Repo) WriteGroup(ctx context.Context, g *accountsmsg.Group) (err error) {
ctx, err = r.getAuthenticatedContext(ctx)
if err != nil {
return err
}
if err := r.makeRootDirIfNotExist(ctx, groupsFolder); err != nil {
return err
}
var by []byte
if by, err = json.Marshal(g); err != nil {
return err
}
err = r.metadataStorage.SimpleUpload(ctx, r.groupURL(g.Id), by)
return err
}
// LoadGroup loads a group via cs3 by id and writes it to the provided group
func (r CS3Repo) LoadGroup(ctx context.Context, id string, g *accountsmsg.Group) (err error) {
ctx, err = r.getAuthenticatedContext(ctx)
if err != nil {
return err
}
return r.loadGroup(ctx, id, g)
}
// LoadGroups loads all the groups from the cs3 api
func (r CS3Repo) LoadGroups(ctx context.Context, g *[]*accountsmsg.Group) (err error) {
ctx, err = r.getAuthenticatedContext(ctx)
if err != nil {
return err
}
res, err := r.storageProvider.ListContainer(ctx, &provider.ListContainerRequest{
Ref: &provider.Reference{
ResourceId: r.metadataStorage.SpaceRoot,
Path: utils.MakeRelativePath(groupsFolder),
},
})
if err != nil {
return err
}
log := olog.NewLogger(olog.Pretty(r.cfg.Log.Pretty), olog.Color(r.cfg.Log.Color), olog.Level(r.cfg.Log.Level))
for i := range res.Infos {
grp := &accountsmsg.Group{}
err := r.loadGroup(ctx, filepath.Base(res.Infos[i].Path), grp)
if err != nil {
log.Err(err).Msg("could not load account")
continue
}
*g = append(*g, grp)
}
return nil
}
func (r CS3Repo) loadGroup(ctx context.Context, id string, g *accountsmsg.Group) error {
group, err := r.metadataStorage.SimpleDownload(ctx, r.groupURL(id))
if err != nil {
if metadatastorage.IsNotFoundErr(err) {
return &notFoundErr{"group", id}
}
return err
}
return json.Unmarshal(group, &g)
}
// DeleteGroup deletes a group via cs3 by id
func (r CS3Repo) DeleteGroup(ctx context.Context, id string) (err error) {
ctx, err = r.getAuthenticatedContext(ctx)
if err != nil {
return err
}
resp, err := r.storageProvider.Delete(ctx, &provider.DeleteRequest{
Ref: &provider.Reference{
ResourceId: r.metadataStorage.SpaceRoot,
Path: utils.MakeRelativePath(filepath.Join(groupsFolder, id)),
},
})
if err != nil {
return err
}
// TODO Handle other error codes?
if resp.Status.Code == v1beta11.Code_CODE_NOT_FOUND {
return &notFoundErr{"group", id}
}
return err
}
func (r CS3Repo) getAuthenticatedContext(ctx context.Context) (context.Context, error) {
t, err := AuthenticateCS3(ctx, r.cfg.ServiceUser, r.tm)
if err != nil {
return nil, err
}
ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, t)
return ctx, nil
}
// AuthenticateCS3 mints an auth token for communicating with cs3 storage based on a service user from config
func AuthenticateCS3(ctx context.Context, su config.ServiceUser, tm token.Manager) (token string, err error) {
u := &user.User{
Id: &user.UserId{
OpaqueId: su.UUID,
Type: user.UserType_USER_TYPE_APPLICATION,
},
Groups: []string{},
UidNumber: su.UID,
GidNumber: su.GID,
}
s, err := scope.AddOwnerScope(nil)
if err != nil {
return
}
return tm.MintToken(ctx, u, s)
}
func (r CS3Repo) accountURL(id string) string {
return path.Join(accountsFolder, id)
}
func (r CS3Repo) groupURL(id string) string {
return path.Join(groupsFolder, id)
}
func (r CS3Repo) makeRootDirIfNotExist(ctx context.Context, folder string) error {
return MakeDirIfNotExist(ctx, r.storageProvider, r.metadataStorage.SpaceRoot, folder)
}
// MakeDirIfNotExist will create a root node in the metadata storage. Requires an authenticated context.
func MakeDirIfNotExist(ctx context.Context, sp provider.ProviderAPIClient, root *provider.ResourceId, folder string) error {
var rootPathRef = &provider.Reference{
ResourceId: root,
Path: utils.MakeRelativePath(folder),
}
resp, err := sp.Stat(ctx, &provider.StatRequest{
Ref: rootPathRef,
})
if err != nil {
return err
}
if resp.Status.Code == v1beta11.Code_CODE_NOT_FOUND {
_, err := sp.CreateContainer(ctx, &provider.CreateContainerRequest{
Ref: rootPathRef,
})
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,72 +0,0 @@
package storage
// Uncomment to test locally, requires started metadata-storage for now
//import (
// "context"
// accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
// "github.com/owncloud/ocis/v2/accounts/pkg/config"
// "github.com/stretchr/testify/assert"
// "testing"
//)
//
//var cfg = &config.Config{
// TokenManager: config.TokenManager{
// JWTSecret: "Pive-Fumkiu4",
// },
// Repo: config.Repo{
// CS3: config.CS3{
// ProviderAddr: "0.0.0.0:9215",
// },
// },
//}
//
//func TestCS3Repo_WriteAccount(t *testing.T) {
// r, err := NewCS3Repo("hello", cfg)
// assert.NoError(t, err)
//
// err = r.WriteAccount(context.Background(), &accountsmsg.Account{
// Id: "fefef-egegweg-gegeg",
// AccountEnabled: true,
// DisplayName: "Mike Jones",
// Mail: "mike@example.com",
// })
//
// assert.NoError(t, err)
//}
//
//func TestCS3Repo_LoadAccount(t *testing.T) {
// r, err := NewCS3Repo("hello", cfg)
// assert.NoError(t, err)
//
// err = r.WriteAccount(context.Background(), &accountsmsg.Account{
// Id: "fefef-egegweg-gegeg",
// AccountEnabled: true,
// DisplayName: "Mike Jones",
// Mail: "mike@example.com",
// })
//
// acc := &accountsmsg.Account{}
// err = r.LoadAccount(context.Background(), "fefef-egegweg-gegeg", acc)
//
// assert.NoError(t, err)
// assert.Equal(t, "fefef-egegweg-gegeg", acc.Id)
// assert.Equal(t, "Mike Jones", acc.DisplayName)
// assert.Equal(t, "mike@example.com", acc.Mail)
//}
//
//func TestCS3Repo_DeleteAccount(t *testing.T) {
// r, err := NewCS3Repo("hello", cfg)
// assert.NoError(t, err)
//
// err = r.WriteAccount(context.Background(), &accountsmsg.Account{
// Id: "delete-me-id",
// AccountEnabled: true,
// DisplayName: "Mike Jones",
// Mail: "mike@example.com",
// })
//
// err = r.DeleteAccount(context.Background(), "delete-me-id")
//
// assert.NoError(t, err)
//}

View File

@@ -1,202 +0,0 @@
package storage
import (
"context"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"sync"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
olog "github.com/owncloud/ocis/v2/ocis-pkg/log"
)
var groupLock sync.Mutex
// DiskRepo provides a local filesystem implementation of the Repo interface
type DiskRepo struct {
cfg *config.Config
log olog.Logger
}
// NewDiskRepo creates a new disk repo
func NewDiskRepo(cfg *config.Config, log olog.Logger) DiskRepo {
paths := []string{
filepath.Join(cfg.Repo.Disk.Path, accountsFolder),
filepath.Join(cfg.Repo.Disk.Path, groupsFolder),
}
for i := range paths {
if _, err := os.Stat(paths[i]); err != nil {
if os.IsNotExist(err) {
if err = os.MkdirAll(paths[i], 0700); err != nil {
log.Fatal().Err(err).Msgf("could not create data folder %v", paths[i])
}
}
}
}
return DiskRepo{
cfg: cfg,
log: log,
}
}
// WriteAccount to the local filesystem
func (r DiskRepo) WriteAccount(ctx context.Context, a *accountsmsg.Account) (err error) {
// leave only the group id
r.deflateMemberOf(a)
var bytes []byte
if bytes, err = json.Marshal(a); err != nil {
return err
}
path := filepath.Join(r.cfg.Repo.Disk.Path, accountsFolder, a.Id)
return ioutil.WriteFile(path, bytes, 0600)
}
// LoadAccount from the local filesystem
func (r DiskRepo) LoadAccount(ctx context.Context, id string, a *accountsmsg.Account) (err error) {
path := filepath.Join(r.cfg.Repo.Disk.Path, accountsFolder, id)
var data []byte
if data, err = ioutil.ReadFile(path); err != nil {
if os.IsNotExist(err) {
err = &notFoundErr{"account", id}
}
return
}
return json.Unmarshal(data, a)
}
// LoadAccounts loads all the accounts from the local filesystem
func (r DiskRepo) LoadAccounts(ctx context.Context, a *[]*accountsmsg.Account) (err error) {
root := filepath.Join(r.cfg.Repo.Disk.Path, accountsFolder)
infos, err := ioutil.ReadDir(root)
if err != nil {
return err
}
for i := range infos {
acc := &accountsmsg.Account{}
if e := r.LoadAccount(ctx, infos[i].Name(), acc); e != nil {
r.log.Err(e).Msg("could not load account")
continue
}
*a = append(*a, acc)
}
return nil
}
// DeleteAccount from the local filesystem
func (r DiskRepo) DeleteAccount(ctx context.Context, id string) (err error) {
path := filepath.Join(r.cfg.Repo.Disk.Path, accountsFolder, id)
if err = os.Remove(path); err != nil {
if os.IsNotExist(err) {
err = &notFoundErr{"account", id}
}
}
return
}
// WriteGroup to the local filesystem
func (r DiskRepo) WriteGroup(ctx context.Context, g *accountsmsg.Group) (err error) {
// leave only the member id
r.deflateMembers(g)
var bytes []byte
if bytes, err = json.Marshal(g); err != nil {
return err
}
path := filepath.Join(r.cfg.Repo.Disk.Path, groupsFolder, g.Id)
groupLock.Lock()
defer groupLock.Unlock()
return ioutil.WriteFile(path, bytes, 0600)
}
// LoadGroup from the local filesystem
func (r DiskRepo) LoadGroup(ctx context.Context, id string, g *accountsmsg.Group) (err error) {
path := filepath.Join(r.cfg.Repo.Disk.Path, groupsFolder, id)
groupLock.Lock()
defer groupLock.Unlock()
var data []byte
if data, err = ioutil.ReadFile(path); err != nil {
if os.IsNotExist(err) {
err = &notFoundErr{"group", id}
}
return
}
return json.Unmarshal(data, g)
}
// LoadGroups loads all the groups from the local filesystem
func (r DiskRepo) LoadGroups(ctx context.Context, g *[]*accountsmsg.Group) (err error) {
root := filepath.Join(r.cfg.Repo.Disk.Path, groupsFolder)
infos, err := ioutil.ReadDir(root)
if err != nil {
return err
}
for i := range infos {
grp := &accountsmsg.Group{}
if e := r.LoadGroup(ctx, infos[i].Name(), grp); e != nil {
r.log.Err(e).Msg("could not load group")
continue
}
*g = append(*g, grp)
}
return nil
}
// DeleteGroup from the local filesystem
func (r DiskRepo) DeleteGroup(ctx context.Context, id string) (err error) {
path := filepath.Join(r.cfg.Repo.Disk.Path, groupsFolder, id)
if err = os.Remove(path); err != nil {
if os.IsNotExist(err) {
err = &notFoundErr{"account", id}
}
}
return
}
// deflateMemberOf replaces the groups of a user with an instance that only contains the id
func (r DiskRepo) deflateMemberOf(a *accountsmsg.Account) {
if a == nil {
return
}
var deflated []*accountsmsg.Group
for i := range a.MemberOf {
if a.MemberOf[i].Id != "" {
deflated = append(deflated, &accountsmsg.Group{Id: a.MemberOf[i].Id})
} else {
// TODO fetch and use an id when group only has a name but no id
r.log.Error().Str("id", a.Id).Interface("group", a.MemberOf[i]).Msg("resolving groups by name is not implemented yet")
}
}
a.MemberOf = deflated
}
// deflateMembers replaces the users of a group with an instance that only contains the id
func (r DiskRepo) deflateMembers(g *accountsmsg.Group) {
if g == nil {
return
}
var deflated []*accountsmsg.Account
for i := range g.Members {
if g.Members[i].Id != "" {
deflated = append(deflated, &accountsmsg.Account{Id: g.Members[i].Id})
} else {
// TODO fetch and use an id when group only has a name but no id
r.log.Error().Str("id", g.Id).Interface("account", g.Members[i]).Msg("resolving members by name is not implemented yet")
}
}
g.Members = deflated
}

View File

@@ -1,19 +0,0 @@
package storage
import (
"fmt"
)
type notFoundErr struct {
typ, id string
}
func (e notFoundErr) Error() string {
return fmt.Sprintf("%s with id %s not found", e.typ, e.id)
}
// IsNotFoundErr can be returned by repo Load and Delete operations
func IsNotFoundErr(e error) bool {
_, ok := e.(*notFoundErr)
return ok
}

View File

@@ -1,24 +0,0 @@
package storage
import (
"context"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
)
const (
accountsFolder = "accounts"
groupsFolder = "groups"
)
// Repo defines the storage operations
type Repo interface {
WriteAccount(ctx context.Context, a *accountsmsg.Account) (err error)
LoadAccount(ctx context.Context, id string, a *accountsmsg.Account) (err error)
LoadAccounts(ctx context.Context, a *[]*accountsmsg.Account) (err error)
DeleteAccount(ctx context.Context, id string) (err error)
WriteGroup(ctx context.Context, g *accountsmsg.Group) (err error)
LoadGroup(ctx context.Context, id string, g *accountsmsg.Group) (err error)
LoadGroups(ctx context.Context, g *[]*accountsmsg.Group) (err error)
DeleteGroup(ctx context.Context, id string) (err error)
}

View File

@@ -1,23 +0,0 @@
package tracing
import (
"github.com/owncloud/ocis/v2/extensions/accounts/pkg/config"
pkgtrace "github.com/owncloud/ocis/v2/ocis-pkg/tracing"
"go.opentelemetry.io/otel/trace"
)
var (
// TraceProvider is the global trace provider for the proxy 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
}

View File

@@ -1,5 +0,0 @@
# backend
-r '^(cmd|pkg)/.*\.go$' -R '^node_modules/' -s -- sh -c 'make bin/ocis-accounts && bin/ocis-accounts server --asset-path assets/'
# frontend
-r '^ui/.*\.(vue|js)$' -R '^node_modules/' -- sh -c 'yarn build && make generate'

View File

@@ -1,52 +0,0 @@
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/accounts.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()
]
}

View File

@@ -1,44 +0,0 @@
import 'regenerator-runtime/runtime'
import App from './components/App.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('Accounts'),
id: 'accounts',
icon: 'team',
isFileEditor: false
}
const routes = [
{
name: 'accounts',
path: '/',
component: App
}
]
const navItems = [
{
name: $gettext('Accounts'),
icon: appInfo.icon,
route: {
name: 'accounts',
path: `/${appInfo.id}/`
},
menu: 'apps'
}
]
export default {
appInfo,
routes,
navItems,
store,
translations
}

View File

@@ -1,723 +0,0 @@
/* 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)
}
}
/*==========================================================
*
==========================================================*/
/**
* Creates an account
* request: AccountsService_CreateAccount
* url: AccountsService_CreateAccountURL
* method: AccountsService_CreateAccount_TYPE
* raw_url: AccountsService_CreateAccount_RAW_URL
* @param body -
*/
export const AccountsService_CreateAccount = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/accounts/accounts-create'
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 AccountsService_CreateAccount_RAW_URL = function() {
return '/api/v0/accounts/accounts-create'
}
export const AccountsService_CreateAccount_TYPE = function() {
return 'post'
}
export const AccountsService_CreateAccountURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/accounts/accounts-create'
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('&')) : '')
}
/**
* Deletes an account
* request: AccountsService_DeleteAccount
* url: AccountsService_DeleteAccountURL
* method: AccountsService_DeleteAccount_TYPE
* raw_url: AccountsService_DeleteAccount_RAW_URL
* @param body -
*/
export const AccountsService_DeleteAccount = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/accounts/accounts-delete'
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 AccountsService_DeleteAccount_RAW_URL = function() {
return '/api/v0/accounts/accounts-delete'
}
export const AccountsService_DeleteAccount_TYPE = function() {
return 'post'
}
export const AccountsService_DeleteAccountURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/accounts/accounts-delete'
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('&')) : '')
}
/**
* Gets an account
* request: AccountsService_GetAccount
* url: AccountsService_GetAccountURL
* method: AccountsService_GetAccount_TYPE
* raw_url: AccountsService_GetAccount_RAW_URL
* @param body -
*/
export const AccountsService_GetAccount = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/accounts/accounts-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 AccountsService_GetAccount_RAW_URL = function() {
return '/api/v0/accounts/accounts-get'
}
export const AccountsService_GetAccount_TYPE = function() {
return 'post'
}
export const AccountsService_GetAccountURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/accounts/accounts-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('&')) : '')
}
/**
* Lists accounts
* request: AccountsService_ListAccounts
* url: AccountsService_ListAccountsURL
* method: AccountsService_ListAccounts_TYPE
* raw_url: AccountsService_ListAccounts_RAW_URL
* @param body -
*/
export const AccountsService_ListAccounts = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/accounts/accounts-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 AccountsService_ListAccounts_RAW_URL = function() {
return '/api/v0/accounts/accounts-list'
}
export const AccountsService_ListAccounts_TYPE = function() {
return 'post'
}
export const AccountsService_ListAccountsURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/accounts/accounts-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('&')) : '')
}
/**
* Updates an account
* request: AccountsService_UpdateAccount
* url: AccountsService_UpdateAccountURL
* method: AccountsService_UpdateAccount_TYPE
* raw_url: AccountsService_UpdateAccount_RAW_URL
* @param body -
*/
export const AccountsService_UpdateAccount = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/api/v0/accounts/accounts-update'
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 AccountsService_UpdateAccount_RAW_URL = function() {
return '/api/v0/accounts/accounts-update'
}
export const AccountsService_UpdateAccount_TYPE = function() {
return 'post'
}
export const AccountsService_UpdateAccountURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/api/v0/accounts/accounts-update'
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('&')) : '')
}
/**
* Lists groups
* request: GroupsService_ListGroups
* url: GroupsService_ListGroupsURL
* method: GroupsService_ListGroups_TYPE
* raw_url: GroupsService_ListGroups_RAW_URL
* @param pageSize - Optional. The maximum number of groups to return in the response.
* @param pageToken - Optional. A pagination token returned from a previous call to `Get`
that indicates from where search should continue.
* @param fieldMaskPaths - The set of field mask paths.
* @param query - Optional. Search criteria used to select the groups to return.
If no search criteria is specified then all groups will be
returned. TODO update query language
Query expressions can be used to restrict results based upon
the account properties where the operators `=`, `NOT`, `AND` and `OR`
can be used along with the suffix wildcard symbol `*`.
The string properties in a query expression should use escaped quotes
for values that include whitespace to prevent unexpected behavior.
Some example queries are:
* Query `display_name=Th*` returns accounts whose display_name
starts with "Th"
* Query `display_name=\\"Test String\\"` returns groups with
display names that include both "Test" and "String"
*/
export const GroupsService_ListGroups = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/v1/groups'
let body
let queryParameters = {}
let form = {}
if (parameters['pageSize'] !== undefined) {
queryParameters['page_size'] = parameters['pageSize']
}
if (parameters['pageToken'] !== undefined) {
queryParameters['page_token'] = parameters['pageToken']
}
if (parameters['fieldMaskPaths'] !== undefined) {
queryParameters['field_mask.paths'] = parameters['fieldMaskPaths']
}
if (parameters['query'] !== undefined) {
queryParameters['query'] = parameters['query']
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('get', domain + path, body, queryParameters, form, config)
}
export const GroupsService_ListGroups_RAW_URL = function() {
return '/v1/groups'
}
export const GroupsService_ListGroups_TYPE = function() {
return 'get'
}
export const GroupsService_ListGroupsURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/v1/groups'
if (parameters['pageSize'] !== undefined) {
queryParameters['page_size'] = parameters['pageSize']
}
if (parameters['pageToken'] !== undefined) {
queryParameters['page_token'] = parameters['pageToken']
}
if (parameters['fieldMaskPaths'] !== undefined) {
queryParameters['field_mask.paths'] = parameters['fieldMaskPaths']
}
if (parameters['query'] !== undefined) {
queryParameters['query'] = parameters['query']
}
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('&')) : '')
}
/**
* Creates a group
* request: GroupsService_CreateGroup
* url: GroupsService_CreateGroupURL
* method: GroupsService_CreateGroup_TYPE
* raw_url: GroupsService_CreateGroup_RAW_URL
* @param body - The account resource to create
*/
export const GroupsService_CreateGroup = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/v1/groups'
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 GroupsService_CreateGroup_RAW_URL = function() {
return '/v1/groups'
}
export const GroupsService_CreateGroup_TYPE = function() {
return 'post'
}
export const GroupsService_CreateGroupURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/v1/groups'
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('&')) : '')
}
/**
* Updates a group
* request: GroupsService_UpdateGroup
* url: GroupsService_UpdateGroupURL
* method: GroupsService_UpdateGroup_TYPE
* raw_url: GroupsService_UpdateGroup_RAW_URL
* @param groupId - The unique identifier for the group.
Returned by default. Inherited from directoryObject. Key. Not nullable. Read-only.
* @param body - The group resource which replaces the resource on the server
*/
export const GroupsService_UpdateGroup = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/v1/groups/{group.id}'
let body
let queryParameters = {}
let form = {}
path = path.replace('{group.id}', `${parameters['groupId']}`)
if (parameters['groupId'] === undefined) {
return Promise.reject(new Error('Missing required parameter: groupId'))
}
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('patch', domain + path, body, queryParameters, form, config)
}
export const GroupsService_UpdateGroup_RAW_URL = function() {
return '/v1/groups/{group.id}'
}
export const GroupsService_UpdateGroup_TYPE = function() {
return 'patch'
}
export const GroupsService_UpdateGroupURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/v1/groups/{group.id}'
path = path.replace('{group.id}', `${parameters['groupId']}`)
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('&')) : '')
}
/**
* Gets an groups
* request: GroupsService_GetGroup
* url: GroupsService_GetGroupURL
* method: GroupsService_GetGroup_TYPE
* raw_url: GroupsService_GetGroup_RAW_URL
* @param id -
*/
export const GroupsService_GetGroup = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/v1/groups/{id}'
let body
let queryParameters = {}
let form = {}
path = path.replace('{id}', `${parameters['id']}`)
if (parameters['id'] === undefined) {
return Promise.reject(new Error('Missing required parameter: id'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('get', domain + path, body, queryParameters, form, config)
}
export const GroupsService_GetGroup_RAW_URL = function() {
return '/v1/groups/{id}'
}
export const GroupsService_GetGroup_TYPE = function() {
return 'get'
}
export const GroupsService_GetGroupURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/v1/groups/{id}'
path = path.replace('{id}', `${parameters['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('&')) : '')
}
/**
* Deletes a group
* request: GroupsService_DeleteGroup
* url: GroupsService_DeleteGroupURL
* method: GroupsService_DeleteGroup_TYPE
* raw_url: GroupsService_DeleteGroup_RAW_URL
* @param id -
*/
export const GroupsService_DeleteGroup = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/v1/groups/{id}'
let body
let queryParameters = {}
let form = {}
path = path.replace('{id}', `${parameters['id']}`)
if (parameters['id'] === undefined) {
return Promise.reject(new Error('Missing required parameter: id'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('delete', domain + path, body, queryParameters, form, config)
}
export const GroupsService_DeleteGroup_RAW_URL = function() {
return '/v1/groups/{id}'
}
export const GroupsService_DeleteGroup_TYPE = function() {
return 'delete'
}
export const GroupsService_DeleteGroupURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/v1/groups/{id}'
path = path.replace('{id}', `${parameters['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('&')) : '')
}
/**
* group:listmembers https://docs.microsoft.com/en-us/graph/api/group-list-members?view=graph-rest-1.0
* request: GroupsService_ListMembers
* url: GroupsService_ListMembersURL
* method: GroupsService_ListMembers_TYPE
* raw_url: GroupsService_ListMembers_RAW_URL
* @param id - The group id
* @param pageSize -
* @param pageToken - Optional. A pagination token returned from a previous call to `Get`
that indicates from where search should continue.
* @param fieldMaskPaths - The set of field mask paths.
* @param query - Optional. Search criteria used to select the groups to return.
If no search criteria is specified then all groups will be
returned. TODO update query language
Query expressions can be used to restrict results based upon
the account properties where the operators `=`, `NOT`, `AND` and `OR`
can be used along with the suffix wildcard symbol `*`.
The string properties in a query expression should use escaped quotes
for values that include whitespace to prevent unexpected behavior.
Some example queries are:
* Query `display_name=Th*` returns accounts whose display_name
starts with "Th"
* Query `display_name=\\"Test String\\"` returns groups with
display names that include both "Test" and "String"
*/
export const GroupsService_ListMembers = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/v1/groups/{id}/members/$ref'
let body
let queryParameters = {}
let form = {}
path = path.replace('{id}', `${parameters['id']}`)
if (parameters['id'] === undefined) {
return Promise.reject(new Error('Missing required parameter: id'))
}
if (parameters['pageSize'] !== undefined) {
queryParameters['page_size'] = parameters['pageSize']
}
if (parameters['pageToken'] !== undefined) {
queryParameters['page_token'] = parameters['pageToken']
}
if (parameters['fieldMaskPaths'] !== undefined) {
queryParameters['field_mask.paths'] = parameters['fieldMaskPaths']
}
if (parameters['query'] !== undefined) {
queryParameters['query'] = parameters['query']
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('get', domain + path, body, queryParameters, form, config)
}
export const GroupsService_ListMembers_RAW_URL = function() {
return '/v1/groups/{id}/members/$ref'
}
export const GroupsService_ListMembers_TYPE = function() {
return 'get'
}
export const GroupsService_ListMembersURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/v1/groups/{id}/members/$ref'
path = path.replace('{id}', `${parameters['id']}`)
if (parameters['pageSize'] !== undefined) {
queryParameters['page_size'] = parameters['pageSize']
}
if (parameters['pageToken'] !== undefined) {
queryParameters['page_token'] = parameters['pageToken']
}
if (parameters['fieldMaskPaths'] !== undefined) {
queryParameters['field_mask.paths'] = parameters['fieldMaskPaths']
}
if (parameters['query'] !== undefined) {
queryParameters['query'] = parameters['query']
}
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('&')) : '')
}
/**
* group:addmember https://docs.microsoft.com/en-us/graph/api/group-post-members?view=graph-rest-1.0&tabs=http
* request: GroupsService_AddMember
* url: GroupsService_AddMemberURL
* method: GroupsService_AddMember_TYPE
* raw_url: GroupsService_AddMember_RAW_URL
* @param id - The account id to add
* @param body -
*/
export const GroupsService_AddMember = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/v1/groups/{id}/members/$ref'
let body
let queryParameters = {}
let form = {}
path = path.replace('{id}', `${parameters['id']}`)
if (parameters['id'] === undefined) {
return Promise.reject(new Error('Missing required parameter: id'))
}
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 GroupsService_AddMember_RAW_URL = function() {
return '/v1/groups/{id}/members/$ref'
}
export const GroupsService_AddMember_TYPE = function() {
return 'post'
}
export const GroupsService_AddMemberURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/v1/groups/{id}/members/$ref'
path = path.replace('{id}', `${parameters['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('&')) : '')
}
/**
* group:removemember https://docs.microsoft.com/en-us/graph/api/group-delete-members?view=graph-rest-1.0
* request: GroupsService_RemoveMember
* url: GroupsService_RemoveMemberURL
* method: GroupsService_RemoveMember_TYPE
* raw_url: GroupsService_RemoveMember_RAW_URL
* @param id - The group id
* @param accountId - The account id to remove
*/
export const GroupsService_RemoveMember = function(parameters = {}) {
const domain = parameters.$domain ? parameters.$domain : getDomain()
const config = parameters.$config
let path = '/v1/groups/{id}/members/{account_id}/$ref'
let body
let queryParameters = {}
let form = {}
path = path.replace('{id}', `${parameters['id']}`)
if (parameters['id'] === undefined) {
return Promise.reject(new Error('Missing required parameter: id'))
}
path = path.replace('{account_id}', `${parameters['accountId']}`)
if (parameters['accountId'] === undefined) {
return Promise.reject(new Error('Missing required parameter: accountId'))
}
if (parameters.$queryParameters) {
Object.keys(parameters.$queryParameters).forEach(function(parameterName) {
queryParameters[parameterName] = parameters.$queryParameters[parameterName]
});
}
return request('delete', domain + path, body, queryParameters, form, config)
}
export const GroupsService_RemoveMember_RAW_URL = function() {
return '/v1/groups/{id}/members/{account_id}/$ref'
}
export const GroupsService_RemoveMember_TYPE = function() {
return 'delete'
}
export const GroupsService_RemoveMemberURL = function(parameters = {}) {
let queryParameters = {}
const domain = parameters.$domain ? parameters.$domain : getDomain()
let path = '/v1/groups/{id}/members/{account_id}/$ref'
path = path.replace('{id}', `${parameters['id']}`)
path = path.replace('{account_id}', `${parameters['accountId']}`)
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('&')) : '')
}

View File

@@ -1,627 +0,0 @@
/* 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: 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('&')) : '')
}

View File

@@ -1,74 +0,0 @@
<template>
<div>
<main class="oc-flex oc-flex-column oc-height-1-1 oc-p-m" id="accounts-app">
<template v-if="isInitialized">
<h1 class="oc-invisible-sr">
<translate>Accounts</translate>
</h1>
<div class="oc-app-bar">
<accounts-batch-actions
v-if="isAnyAccountSelected"
:number-of-selected-accounts="numberOfSelectedAccounts"
:selected-accounts="selectedAccounts"
/>
<accounts-create v-else />
</div>
<oc-grid class="oc-flex-1 oc-overflow-auto">
<div class="oc-width-expand">
<accounts-list :accounts="accounts" />
</div>
</oc-grid>
</template>
<template v-else-if="hasFailed">
<oc-alert
variation="warning"
no-close
class="oc-m"
id="accounts-list-loading-failed"
>
<oc-icon
name="error-warning"
variation="warning"
class="oc-float-left oc-mr-s"
/>
<translate>You don't have permissions to manage accounts.</translate>
</oc-alert>
</template>
<oc-loader id="accounts-list-loader" v-else />
</main>
</div>
</template>
<script>
import { mapGetters, mapActions, mapState } from 'vuex'
import AccountsList from './accounts/AccountsList.vue'
import AccountsCreate from './accounts/AccountsCreate.vue'
import AccountsBatchActions from './accounts/AccountsBatchActions.vue'
export default {
name: 'App',
components: { AccountsBatchActions, AccountsList, AccountsCreate },
computed: {
...mapGetters('Accounts', [
'isInitialized',
'hasFailed',
'getAccountsSorted',
'isAnyAccountSelected'
]),
...mapState('Accounts', ['selectedAccounts']),
accounts () {
return this.getAccountsSorted
},
numberOfSelectedAccounts () {
return this.selectedAccounts.length
}
},
methods: {
...mapActions('Accounts', ['initialize'])
},
created () {
this.initialize()
}
}
</script>

View File

@@ -1,157 +0,0 @@
<template>
<oc-grid key="selected-accounts-info" gutter="small" class="oc-flex-middle">
<span v-text="selectionInfoText" />
<span>|</span>
<div>
<oc-button
v-text="$gettext('Clear selection')"
appearance="raw"
@click="RESET_ACCOUNTS_SELECTION"
/>
</div>
<oc-grid gutter="small" id="accounts-batch-actions">
<div v-for="action in actions" :key="action.label">
<div
v-if="isConfirmationInProgress[action.id]"
:variation="action.confirmation.variation || 'primary'"
noClose
class="oc-flex oc-flex-middle tmp-alert-fixes"
>
<span>{{ action.confirmation.message }}</span>
<oc-button
:id="action.confirmation.cancel.id"
@click="action.confirmation.cancel.handler"
:variation="action.confirmation.cancel.variation || 'passive'"
>
{{ action.confirmation.cancel.label }}
</oc-button>
<oc-button
:id="action.confirmation.confirm.id"
@click="action.confirmation.confirm.handler"
:variation="action.confirmation.confirm.variation || 'primary'"
>
{{ action.confirmation.confirm.label }}
</oc-button>
</div>
<oc-button
v-else
:id="action.id"
@click="action.handler"
:variation="action.variation || 'primary'"
:icon="action.icon"
>
{{ action.label }}
</oc-button>
</div>
</oc-grid>
</oc-grid>
</template>
<script>
import { mapActions, mapMutations } from 'vuex'
export default {
name: 'AccountsBatchActions',
props: {
numberOfSelectedAccounts: {
type: Number,
required: true
},
selectedAccounts: {
type: Array,
required: true
}
},
data: () => {
return {
isConfirmationInProgress: {}
}
},
computed: {
selectionInfoText () {
const translated = this.$ngettext('%{ amount } selected user', '%{ amount } selected users', this.numberOfSelectedAccounts)
return this.$gettextInterpolate(translated, { amount: this.numberOfSelectedAccounts })
},
actions () {
const actions = []
const numberOfDisabledAccounts = this.selectedAccounts.filter(account => !account.accountEnabled).length
const isAnyAccountDisabled = numberOfDisabledAccounts > 0
const isAnyAccountEnabled = numberOfDisabledAccounts < this.numberOfSelectedAccounts
if (isAnyAccountDisabled) {
actions.push({
id: 'accounts-batch-action-enable',
label: this.$gettext('Activate'),
icon: 'ready',
handler: () => this.setAccountActivated(true)
})
}
if (isAnyAccountEnabled) {
actions.push({
id: 'accounts-batch-action-disable',
label: this.$gettext('Block'),
icon: 'deprecated',
handler: () => this.setAccountActivated(false)
})
}
const idDeleteAction = 'accounts-batch-action-delete'
actions.push({
id: idDeleteAction,
label: this.$gettext('Delete'),
icon: 'delete',
variation: 'danger',
handler: () => this.showConfirmationRequest(idDeleteAction),
confirmation: {
variation: 'danger',
message: this.$ngettext(
'Delete the selected account?',
'Delete the selected accounts?',
this.numberOfSelectedAccounts
),
cancel: {
id: 'accounts-batch-action-delete-cancel',
label: this.$gettext('Cancel'),
handler: () => this.hideConfirmationRequest(idDeleteAction)
},
confirm: {
id: 'accounts-batch-action-delete-confirm',
label: this.$gettext('Confirm'),
variation: 'danger',
handler: this.deleteAccounts
}
}
})
return actions
}
},
methods: {
...mapActions('Accounts', ['setAccountActivated', 'deleteAccounts']),
...mapMutations('Accounts', ['RESET_ACCOUNTS_SELECTION']),
showConfirmationRequest (actionId) {
this.isConfirmationInProgress = { ...this.isConfirmationInProgress, [actionId]: true }
},
hideConfirmationRequest (actionId) {
this.isConfirmationInProgress = { ...this.isConfirmationInProgress, [actionId]: false }
}
}
}
</script>
<style lang="scss" scoped>
.tmp-alert-fixes {
color: rgb(224, 0, 0) !important;
font-size: 1.125rem !important;
font-weight: 600 !important;
line-height: 1.4 !important;
}
.tmp-alert-fixes > *:not(:last-child) {
margin-right: 8px;
}
.tmp-alert-fixes > button {
padding: 0.2rem 0.5rem;
}
</style>

View File

@@ -1,196 +0,0 @@
<template>
<div>
<oc-grid v-if="isFormInProgress" gutter="small">
<oc-text-input
id="accounts-new-account-input-username"
type="text"
v-model="formData.username"
:error-message="formValidation.usernameError"
:label="$gettext('Username')"
:disabled="isRequestInProgress"
@keydown.enter="createAccount"
/>
<oc-text-input
id="accounts-new-account-input-email"
type="email"
v-model="formData.email"
:error-message="formValidation.emailError"
:label="$gettext('Email')"
:disabled="isRequestInProgress"
@keydown.enter="createAccount"
/>
<oc-text-input
id="accounts-new-account-input-password"
type="password"
v-model="formData.password"
:error-message="formValidation.passwordError"
:label="$gettext('Password')"
:disabled="isRequestInProgress"
@keydown.enter="createAccount"
/>
<div class="oc-flex">
<oc-button
class="oc-mr-s oc-mb-s"
v-text="$gettext('Cancel')"
@click="cancelForm"
:disabled="isRequestInProgress"
/>
<oc-button
id="accounts-new-account-button-confirm"
class="oc-mr-s oc-mb-s"
variation="primary"
appearance="filled"
:disabled="isRequestInProgress"
@click="createAccount"
gap-size="small"
:class="{ 'border-ods-tmp-fix': !isRequestInProgress }"
>
<oc-spinner
v-if="isRequestInProgress"
key="account-creation-in-progress"
size="small"
aria-hidden="true"
/>
<span
v-text="
isRequestInProgress ? $gettext('Creating') : $gettext('Create')
"
/>
</oc-button>
</div>
</oc-grid>
<oc-grid v-else gutter="small">
<div>
<oc-button
id="accounts-new-account-trigger"
key="create-accounts-button"
variation="primary"
appearance="filled"
gap-size="small"
@click="setFormInProgress(true)"
>
<oc-icon name="user-add" />
<translate>Create new account</translate>
</oc-button>
</div>
</oc-grid>
</div>
</template>
<script>
import isEmail from 'validator/es/lib/isEmail'
import isEmpty from 'validator/es/lib/isEmpty'
import debounce from 'debounce'
import { mapActions } from 'vuex'
export default {
name: 'AccountsCreate',
data: () => ({
isFormInProgress: false,
isRequestInProgress: false,
formData: {
username: '',
email: '',
password: ''
},
formValidation: {
usernameError: '',
emailError: '',
passwordError: ''
}
}),
methods: {
...mapActions('Accounts', ['createNewAccount']),
setFormInProgress (inProgress) {
this.isFormInProgress = inProgress
},
cancelForm () {
this.isRequestInProgress = false
this.setFormInProgress(false)
this.formData = {
username: '',
email: '',
password: ''
}
this.formValidation = {
usernameError: '',
emailError: '',
passwordError: ''
}
},
createAccount () {
// note: use bitwise AND because we want all checks to be performed
if (!(this.checkUsername() & this.checkEmail() & this.checkPassword())) {
return
}
this.isRequestInProgress = true
this.createNewAccount(this.formData)
.then((success) => {
if (success) {
this.cancelForm()
}
})
.finally(() => {
this.isRequestInProgress = false
})
},
checkUsername () {
if (isEmpty(this.formData.username)) {
debounce(this.formValidation.usernameError = this.$gettext('Username cannot be empty'), 500)
return false
}
// hacky check: we want to allow emails and the username part of emails as username
if (!isEmail(this.formData.username) && !isEmail(this.formData.username + '@validate.it')) {
debounce(this.formValidation.usernameError = this.$gettext('Invalid username'), 500)
return false
}
this.formValidation.usernameError = ''
return true
},
checkEmail () {
if (isEmpty(this.formData.email)) {
debounce(this.formValidation.emailError = this.$gettext('Email cannot be empty'), 500)
return false
}
if (!isEmail(this.formData.email)) {
debounce(this.formValidation.emailError = this.$gettext('Invalid email address'), 500)
return false
}
this.formValidation.emailError = ''
return true
},
checkPassword () {
// Later on some restrictions might be applied here
if (isEmpty(this.formData.password)) {
debounce(this.formValidation.passwordError = this.$gettext('Password cannot be empty'), 500)
return false
}
this.formValidation.passwordError = ''
return true
}
},
onDestroy () {
this.cancelForm()
}
}
</script>
<style>
#accounts-new-account-button-confirm > span {
display: flex;
align-items: center;
}
</style>

View File

@@ -1,65 +0,0 @@
<template>
<div>
<oc-table-simple id="accounts-user-list" class="oc-mt-l">
<oc-thead>
<oc-tr>
<oc-th shrink type="head" align-h="center">
<oc-checkbox
class="oc-ml-s"
:value="areAllAccountsSelected"
@input="toggleSelectionAll"
:label="$gettext('Select all users')"
hide-label
/>
</oc-th>
<oc-th shrink type="head" />
<oc-th type="head" v-text="$gettext('Username')" />
<oc-th type="head" v-text="$gettext('Display name')" />
<oc-th type="head" v-text="$gettext('Email')" />
<oc-th type="head" v-text="$gettext('Role')" />
<oc-th
shrink
type="head"
v-text="$gettext('Activated')"
align-h="center"
/>
</oc-tr>
</oc-thead>
<oc-tbody>
<accounts-list-row
v-for="account in accounts"
:key="`account-list-row-${account.id}`"
:account="account"
/>
</oc-tbody>
</oc-table-simple>
</div>
</template>
<script>
import { mapActions, mapGetters, mapMutations } from 'vuex'
import AccountsListRow from './AccountsListRow.vue'
export default {
name: 'AccountsList',
components: {
AccountsListRow
},
props: {
accounts: {
type: Array,
required: true
}
},
computed: {
...mapGetters('Accounts', ['areAllAccountsSelected'])
},
methods: {
...mapActions('Accounts', ['toggleSelectionAll']),
...mapMutations('Accounts', ['RESET_ACCOUNTS_SELECTION'])
},
beforeDestroy () {
this.RESET_ACCOUNTS_SELECTION()
}
}
</script>

View File

@@ -1,170 +0,0 @@
<template>
<oc-tr>
<oc-td align-h="center">
<oc-checkbox
class="oc-ml-s"
size="large"
:value="selectedAccounts"
:option="account"
@input="TOGGLE_SELECTION_ACCOUNT(account)"
:label="selectAccountLabel"
hide-label
/>
</oc-td>
<oc-td>
<avatar
:user-name="account.displayName || account.onPremisesSamAccountName"
:userid="account.id"
:width="35"
/>
</oc-td>
<oc-td v-text="account.onPremisesSamAccountName" />
<oc-td v-text="account.displayName || '-'" />
<oc-td v-text="account.mail" />
<oc-td>
<oc-button
:id="`accounts-roles-select-trigger-${account.id}`"
class="accounts-roles-select-trigger"
appearance="outline"
>
<span class="oc-flex oc-flex-middle accounts-roles-current-role">
{{ currentRole ? currentRole.displayName : $gettext("Select role") }}
<oc-icon name="arrow-down-s" aria-hidden="true" />
</span>
</oc-button>
<oc-drop
:drop-id="`accounts-roles-select-dropdown-${account.id}`"
:toggle="`#accounts-roles-select-trigger-${account.id}`"
mode="click"
close-on-click
:options="{ delayHide: 0 }"
>
<ul class="oc-list">
<li v-for="role in roles" :key="role.id">
<oc-radio
class="accounts-roles-dropdown-role"
v-model="currentRole"
:option="role"
@input="changeRole(role.id)"
:label="role.displayName"
/>
</li>
</ul>
</oc-drop>
</oc-td>
<oc-td align-h="center">
<oc-icon
v-if="account.accountEnabled"
key="account-icon-enabled"
name="user-follow"
variation="success"
:aria-label="$gettext('Account is activated')"
class="accounts-status-indicator-enabled"
/>
<oc-icon
v-else
key="account-icon-disabled"
name="user-unfollow"
variation="danger"
:aria-label="$gettext('Account is blocked')"
class="accounts-status-indicator-disabled"
/>
</oc-td>
</oc-tr>
</template>
<script>
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex'
import { isObjectEmpty } from '../../helpers/utils'
import { injectAuthToken } from '../../helpers/auth'
// eslint-disable-next-line camelcase
import { RoleService_AssignRoleToUser, RoleService_ListRoleAssignments } from '../../client/settings'
import Avatar from './Avatar.vue'
export default {
name: 'AccountsListRow',
components: { Avatar },
props: {
account: {
type: Object,
required: true
}
},
data () {
return {
currentRole: null
}
},
computed: {
...mapGetters(['user', 'getServerForJsClient']),
...mapState('Accounts', ['roles', 'selectedAccounts']),
selectAccountLabel () {
const translated = this.$gettext('Select %{ account }')
return this.$gettextInterpolate(translated, { account: this.account.displayName }, true)
}
},
created () {
this.getUsersCurrentRole()
},
methods: {
...mapActions(['showMessage']),
...mapMutations('Accounts', ['TOGGLE_SELECTION_ACCOUNT']),
async changeRole (roleId) {
injectAuthToken(this.user.token)
const response = await RoleService_AssignRoleToUser({
$domain: this.getServerForJsClient,
body: {
account_uuid: this.account.id,
role_id: roleId
}
})
if (response.status === 201) {
const roleId = response.data.assignment.roleId
this.currentRole = this.roles.find(role => {
return role.id === roleId
})
} else {
this.showMessage({
title: this.$gettext('Failed to change role.'),
desc: response.statusText,
status: 'danger'
})
}
},
async getUsersCurrentRole () {
injectAuthToken(this.user.token)
const response = await RoleService_ListRoleAssignments({
$domain: this.getServerForJsClient,
body: {
account_uuid: this.account.id
}
})
if (response.status === 201) {
const assignedRole = response.data
if (isObjectEmpty(assignedRole)) {
return
}
this.currentRole = this.roles.find(role => {
return role.id === assignedRole.assignments[0].roleId
})
}
}
}
}
</script>

View File

@@ -1,130 +0,0 @@
<template>
<component :is="type" v-if="enabled">
<oc-spinner
v-if="loading"
key="avatar-loading"
size="small"
:aria-label="$gettext('Loading')"
:style="`width: ${width}px; height: ${width}px;`"
/>
<oc-avatar
v-else
key="avatar-loaded"
:width="width"
:src="avatarSource"
:user-name="userName"
/>
</component>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
/**
* FIXME: this component has been copied over from ownCloud Web. It should be moved over to ODS, then we can reuse it in
* this extension.
*/
name: 'Avatar',
props: {
/**
* The html element used for the avatar container.
* `div, span`
*/
type: {
type: String,
default: 'div',
validator: value => {
return value.match(/(div|span)/)
}
},
userName: {
type: String,
default: ''
},
userid: {
/**
* Allow empty string to show placeholder
*/
type: String,
default: ''
},
width: {
type: Number,
required: false,
default: 42
}
},
data () {
return {
/**
* Set to object URL when loaded, or on failure, icon placeholder is shown
*/
avatarSource: '',
/**
* Shows spinner in place whilst loading avatar from server
*/
loading: true
}
},
computed: {
...mapGetters(['getToken', 'configuration']),
enabled: function () {
return this.configuration.enableAvatars || true
}
},
watch: {
userid: function (userid, old) {
this.setUser(userid)
}
},
mounted: function () {
// Handled mounted situation. Userid might not be set yet so try placeholder
if (this.userid !== '') {
this.setUser(this.userid)
} else {
this.loading = false
}
},
methods: {
/**
* Load a new avatar from this userid
*/
setUser (userid) {
this.loading = true
this.avatarSource = ''
if (!this.enabled || userid === '') {
this.loading = false
return
}
const headers = new Headers()
const instance = this.configuration.server || window.location.origin
const url = instance + '/remote.php/dav/avatars/' + this.userid + '/128.png'
headers.append('Authorization', 'Bearer ' + this.getToken)
headers.append('X-Requested-With', 'XMLHttpRequest')
fetch(url, { headers })
.then(response => {
if (response.ok) {
return response.blob()
}
if (response.status !== 404) {
throw new Error(`Unexpected status code ${response.status}`)
}
})
.then(blob => {
this.loading = false
if (blob) {
this.avatarSource = window.URL.createObjectURL(blob)
} else {
// 404, none found
this.avatarSource = ''
}
})
.catch(error => {
this.avatarSource = ''
this.loading = false
console.error(`Error loading avatar image for user "${this.userid}": `, error.message)
})
}
}
}
</script>

View File

@@ -1,16 +0,0 @@
/**
* 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('Guest'),
$gettext('Admin'),
$gettext('User')
]

View File

@@ -1,12 +0,0 @@
import axios from 'axios'
export function injectAuthToken (token) {
axios.interceptors.request.use(config => {
if (typeof config.headers.Authorization === 'undefined') {
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
}
return config
})
}

View File

@@ -1,8 +0,0 @@
/**
* Asserts whether the given object is empty
* @param {Object} obj Object to be checked
* @returns {Boolean}
*/
export function isObjectEmpty (obj) {
return Object.keys(obj).length === 0 && obj.constructor === Object
}

View File

@@ -1,253 +0,0 @@
/* eslint-disable camelcase */
import {
AccountsService_ListAccounts,
AccountsService_UpdateAccount,
AccountsService_CreateAccount,
AccountsService_DeleteAccount
} from '../client/accounts'
import { RoleService_ListRoles } from '../client/settings'
/* eslint-enable camelcase */
import { injectAuthToken } from '../helpers/auth'
const state = {
initialized: false,
failed: false,
accounts: {},
roles: null,
selectedAccounts: []
}
const getters = {
isInitialized: state => state.initialized,
hasFailed: state => state.failed,
getAccountsSorted: state => {
return Object.values(state.accounts).sort((a1, a2) => {
if (a1.onPremisesSamAccountName === a2.onPremisesSamAccountName) {
return a1.id.localeCompare(a2.id)
}
return a1.onPremisesSamAccountName.localeCompare(a2.onPremisesSamAccountName)
})
},
areAllAccountsSelected: state => state.accounts.length === state.selectedAccounts.length,
isAnyAccountSelected: state => state.selectedAccounts.length > 0,
getServerForJsClient: (state, getters, rootState, rootGetters) => rootGetters.configuration.server.replace(/\/$/, '')
}
const mutations = {
SET_INITIALIZED (state, value) {
state.initialized = value
},
SET_FAILED (state, value) {
state.failed = value
},
SET_ACCOUNTS (state, accounts) {
state.accounts = accounts
},
SET_ROLES (state, roles) {
state.roles = roles
},
TOGGLE_SELECTION_ACCOUNT (state, account) {
const accountIndex = state.selectedAccounts.indexOf(account)
accountIndex > -1 ? state.selectedAccounts.splice(accountIndex, 1) : state.selectedAccounts.push(account)
},
SET_SELECTED_ACCOUNTS (state, accounts) {
state.selectedAccounts = accounts
},
UPDATE_ACCOUNT (state, updatedAccount) {
const accountIndex = state.accounts.findIndex(account => account.id === updatedAccount.id)
state.accounts.splice(accountIndex, 1, updatedAccount)
},
RESET_ACCOUNTS_SELECTION (state) {
state.selectedAccounts = []
},
PUSH_NEW_ACCOUNT (state, account) {
state.accounts.push(account)
},
DELETE_ACCOUNT (state, accountId) {
const accountIndex = state.accounts.findIndex(account => account.id === accountId)
state.accounts.splice(accountIndex, 1)
}
}
const actions = {
async initialize ({ commit, dispatch, getters }) {
await Promise.all([
dispatch('fetchAccounts'),
dispatch('fetchRoles')
])
if (!getters.hasFailed) {
commit('SET_INITIALIZED', true)
}
},
async fetchAccounts ({ commit, getters, rootGetters }) {
injectAuthToken(rootGetters.user.token)
try {
const response = await AccountsService_ListAccounts({
$domain: getters.getServerForJsClient,
body: {}
})
if (response.status === 201) {
const accounts = response.data.accounts
commit('SET_ACCOUNTS', accounts || [])
return
}
} catch (e) {
}
commit('SET_FAILED', true)
},
async fetchRoles ({ commit, getters, rootGetters }) {
injectAuthToken(rootGetters.user.token)
try {
const response = await RoleService_ListRoles({
$domain: getters.getServerForJsClient,
body: {}
})
if (response.status === 201) {
const roles = response.data.bundles
commit('SET_ROLES', roles || [])
return
}
} catch (e) {
}
commit('SET_FAILED', true)
},
toggleSelectionAll ({ commit, getters, state }) {
getters.areAllAccountsSelected ? commit('RESET_ACCOUNTS_SELECTION') : commit('SET_SELECTED_ACCOUNTS', [...state.accounts])
},
async setAccountActivated ({ commit, dispatch, state, getters, rootGetters }, activated) {
const failedAccounts = []
injectAuthToken(rootGetters.user.token)
for (const account of state.selectedAccounts) {
if (account.accountEnabled === activated) {
continue
}
try {
const response = await AccountsService_UpdateAccount({
$domain: getters.getServerForJsClient,
body: {
account: {
id: account.id,
accountEnabled: activated
},
update_mask: {
paths: ['AccountEnabled']
}
}
})
if (response.status === 201) {
commit('UPDATE_ACCOUNT', { ...account, accountEnabled: activated })
} else {
failedAccounts.push({ account: account.username })
}
} catch (error) {
failedAccounts.push({ account: account.username })
}
}
if (failedAccounts.length > 0) {
let errorTitle = ''
if (failedAccounts.length === 1) {
errorTitle = activated ? 'Failed to activate account.' : 'Failed to block account.'
} else {
errorTitle = activated ? 'Failed to activate accounts.' : 'Failed to block accounts.'
}
dispatch('showMessage', {
title: errorTitle,
status: 'danger'
}, { root: true })
return Promise.resolve(false)
}
commit('RESET_ACCOUNTS_SELECTION')
return Promise.resolve(true)
},
async createNewAccount ({ getters, rootGetters, commit, dispatch }, account) {
injectAuthToken(rootGetters.user.token)
try {
const response = await AccountsService_CreateAccount({
$domain: getters.getServerForJsClient,
body: {
account: {
on_premises_sam_account_name: account.username,
preferred_name: account.username,
mail: account.email,
password_profile: {
password: account.password
},
account_enabled: true,
display_name: account.username
}
}
})
if (response.status === 201) {
commit('PUSH_NEW_ACCOUNT', response.data)
return Promise.resolve(true)
}
} catch (error) {
dispatch('showMessage', {
title: 'Failed to create account.',
status: 'danger'
}, { root: true })
return Promise.reject(error)
}
return Promise.resolve(false)
},
async deleteAccounts ({ getters, rootGetters, state, commit, dispatch }) {
const failedAccounts = []
injectAuthToken(rootGetters.user.token)
for (const account of state.selectedAccounts) {
try {
const response = await AccountsService_DeleteAccount({
$domain: getters.getServerForJsClient,
body: {
id: account.id
}
})
if (response.status === 201 || response.status === 204) {
commit('DELETE_ACCOUNT', account.id)
} else {
failedAccounts.push({ account: account.username })
}
} catch (error) {
failedAccounts.push({ account: account.username })
}
}
if (failedAccounts.length > 0) {
const errorTitle = failedAccounts.length === 1 ? 'Failed to delete account.' : 'Failed to delete accounts.'
dispatch('showMessage', {
title: errorTitle,
status: 'danger'
}, { root: true })
return Promise.resolve(false)
}
commit('RESET_ACCOUNTS_SELECTION')
return Promise.resolve(true)
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}

View File

@@ -1,75 +0,0 @@
Feature: Accounts
Scenario: admin checks accounts list
Given user "Moss" has logged in using the webUI
When the user browses to the accounts page
Then user "einstein" should be displayed in the accounts list on the WebUI
And user "idp" should be displayed in the accounts list on the WebUI
And user "marie" should be displayed in the accounts list on the WebUI
And user "reva" should be displayed in the accounts list on the WebUI
And user "richard" should be displayed in the accounts list on the WebUI
Scenario: admin changes non-admin user's role to admin
Given user "Moss" has logged in using the webUI
When the user browses to the accounts page
Then user "einstein" should be displayed in the accounts list on the WebUI
When the user changes the role of user "einstein" to "Admin" using the WebUI
Then the displayed role of user "einstein" should be "Admin" on the WebUI
When the user reloads the current page of the webUI
Then the displayed role of user "einstein" should be "Admin" on the WebUI
@skip @issue-product-167
Scenario: regular user should not be able to see accounts list
Given user "Marie" has logged in using the webUI
When the user browses to the accounts page
Then the user should not be able to see the accounts list on the WebUI
@skip @issue-product-167
Scenario: guest user should not be able to see accounts list
Given user "Moss" has logged in using the webUI
When the user browses to the accounts page
Then user "einstein" should be displayed in the accounts list on the WebUI
When the user changes the role of user "einstein" to "Guest" using the WebUI
And the user logs out of the webUI
And user "Einstein" logs in using the webUI
And the user browses to the accounts page
Then the user should not be able to see the accounts list on the WebUI
# We want to separate this into own scenarios but because we do not have clean env for each scenario yet
# we are resetting it manually by combining them into one
Scenario: disable/enable account
Given user "Moss" has logged in using the webUI
When the user browses to the accounts page
Then user "einstein" should be displayed in the accounts list on the WebUI
When the user disables user "einstein" using the WebUI
Then the status indicator of user "einstein" should be "disabled" on the WebUI
# And user "einstein" should not be able to log in
When the user enables user "einstein" using the WebUI
Then the status indicator of user "einstein" should be "enabled" on the WebUI
# And user "einstein" should be able to log in
Scenario: disable/enable multiple accounts
Given user "Moss" has logged in using the webUI
When the user browses to the accounts page
Then user "einstein" should be displayed in the accounts list on the WebUI
And user "marie" should be displayed in the accounts list on the WebUI
When the user disables users "einstein,marie" using the WebUI
Then the status indicator of users "einstein,marie" should be "disabled" on the WebUI
# And user "einstein" should not be able to log in
# And user "marie" should not be able to log in
When the user enables users "einstein,marie" using the WebUI
Then the status indicator of user "einstein,marie" should be "enabled" on the WebUI
# And user "einstein" should be able to log in
# And user "marie" should be able to log in
Scenario: create a user
Given user "Moss" has logged in using the webUI
And the user browses to the accounts page
When the user creates a new user with username "bob", email "bob@example.org" and password "bob" using the WebUI
Then user "bob" should be displayed in the accounts list on the WebUI
Scenario: delete a user
Given user "Moss" has logged in using the webUI
And the user browses to the accounts page
When the user deletes user "bob" using the WebUI
Then user "bob" should not be displayed in the accounts list on the WebUI

View File

@@ -1,174 +0,0 @@
const util = require('util')
module.exports = {
url: function () {
return this.api.launchUrl + '/accounts'
},
commands: {
navigateAndWaitUntilMounted: async function () {
const url = this.url()
return this.navigate(url).waitForElementVisible('@accountsApp')
},
accountsList: function () {
return this.waitForElementVisible('@accountsListTable')
},
isUserListed: async function (username) {
const usernameInTable = util.format(this.elements.userInAccountsList.selector, username)
await this.useXpath().waitForElementVisible(usernameInTable)
return true
},
isUserDeleted: async function (username) {
const usernameInTable = util.format(this.elements.userInAccountsList.selector, username)
await this.useXpath().waitForElementNotPresent(usernameInTable)
return true
},
selectRole: function (username, role) {
const roleTrigger =
util.format(this.elements.rowByUsername.selector, username) +
this.elements.rolesDropdownTrigger.selector
const roleSelector =
util.format(this.elements.rowByUsername.selector, username) +
util.format(this.elements.roleInRolesDropdown.selector, role)
return this
.initAjaxCounters()
.waitForElementVisible(roleTrigger)
.click(roleTrigger)
.waitForElementVisible(roleSelector)
.click(roleSelector)
.waitForOutstandingAjaxCalls()
},
checkUsersRole: function (username, role) {
const roleSelector =
util.format(this.elements.rowByUsername.selector, username) +
util.format(this.elements.currentRole.selector, role)
return this.useXpath().expect.element(roleSelector).to.be.visible
},
setUserActivated: function (usernames, activated) {
this.selectUsers(usernames)
return this.click(activated === true ? this.elements.batchActionEnable : this.elements.batchActionDisable)
},
checkUsersStatus: function (usernames, status) {
usernames = usernames.split(',')
for (const username of usernames) {
const indicatorSelector =
util.format(this.elements.rowByUsername.selector, username) +
util.format(this.elements.statusIndicator.selector, status)
this.useXpath().waitForElementVisible(indicatorSelector)
}
return this
},
deleteUsers: function (usernames) {
this.selectUsers(usernames)
return this.click(this.elements.batchActionDelete)
.waitForElementVisible(this.elements.batchActionDeleteConfirm)
.click(this.elements.batchActionDeleteConfirm)
},
selectUsers: function (usernames) {
usernames = usernames.split(',')
for (const username of usernames) {
const checkboxSelector =
util.format(this.elements.rowByUsername.selector, username) +
this.elements.rowCheckbox.selector
this.useXpath().click(checkboxSelector)
}
return this
},
createUser: function (username, email, password) {
return this
.click('@accountsNewAccountTrigger')
.setValue('@newAccountInputUsername', username)
.setValue('@newAccountInputEmail', email)
.setValue('@newAccountInputPassword', password)
.click('@newAccountButtonConfirm')
}
},
elements: {
accountsApp: {
selector: '#accounts-app'
},
accountsListTable: {
selector: '#accounts-user-list'
},
userInAccountsList: {
selector: '//table[@id="accounts-user-list"]//td[text()="%s"]',
locateStrategy: 'xpath'
},
rowByUsername: {
selector: '//table[@id="accounts-user-list"]//td[text()="%s"]/ancestor::tr',
locateStrategy: 'xpath'
},
currentRole: {
selector: '//span[contains(@class, "accounts-roles-current-role") and normalize-space()="%s"]',
locateStrategy: 'xpath'
},
roleInRolesDropdown: {
selector: '//span[contains(@class, "accounts-roles-dropdown-role")]/label[normalize-space()="%s"]',
locateStrategy: 'xpath'
},
rolesDropdownTrigger: {
selector: '//button[contains(@class, "accounts-roles-select-trigger")]',
locateStrategy: 'xpath'
},
loadingAccountsList: {
selector: '#accounts-list-loader'
},
loadingAccountsListFailed: {
selector: '#accounts-list-loading-failed'
},
rowCheckbox: {
selector: '//input[contains(@class, "oc-checkbox")]',
locateStrategy: 'xpath'
},
batchActionDisable: {
selector: '#accounts-batch-action-disable'
},
batchActionEnable: {
selector: '#accounts-batch-action-enable'
},
batchActionDelete: {
selector: '#accounts-batch-action-delete'
},
batchActionDeleteCancel: {
selector: '#accounts-batch-action-delete-cancel'
},
batchActionDeleteConfirm: {
selector: '#accounts-batch-action-delete-confirm'
},
statusIndicator: {
selector: '//span[contains(@class, "accounts-status-indicator-%s")]',
locateStrategy: 'xpath'
},
newAccountInputUsername: {
selector: '#accounts-new-account-input-username'
},
newAccountInputEmail: {
selector: '#accounts-new-account-input-email'
},
newAccountInputPassword: {
selector: '#accounts-new-account-input-password'
},
newAccountButtonConfirm: {
selector: '#accounts-new-account-button-confirm'
},
accountsNewAccountTrigger: {
selector: '#accounts-new-account-trigger'
}
}
}

View File

@@ -1,60 +0,0 @@
const assert = require('assert')
const { client } = require('nightwatch-api')
const { Given, When, Then } = require('@cucumber/cucumber')
When('the user browses to the accounts page', function () {
return client.page.accountsPage().navigateAndWaitUntilMounted()
})
Then('user {string} should be displayed in the accounts list on the WebUI', async function (username) {
await client.page.accountsPage().accountsList()
const userListed = await client.page.accountsPage().isUserListed(username)
return assert.strictEqual(userListed, true)
})
Then('user {string} should not be displayed in the accounts list on the WebUI', async function (username) {
await client.page.accountsPage().accountsList()
const userDeleted = await client.page.accountsPage().isUserDeleted(username)
return assert.strictEqual(userDeleted, true)
})
Given('the user has changed the role of user {string} to {string}', function (username, role) {
return client.page.accountsPage().selectRole(username, role)
})
When('the user changes the role of user {string} to {string} using the WebUI', function (username, role) {
return client.page.accountsPage().selectRole(username, role)
})
Then('the displayed role of user {string} should be {string} on the WebUI', function (username, role) {
return client.page.accountsPage().checkUsersRole(username, role)
})
Then('the user should not be able to see the accounts list on the WebUI', async function () {
return client.page.accountsPage()
.waitForAjaxCallsToStartAndFinish()
.waitForElementVisible('@loadingAccountsListFailed')
})
When('the user disables user/users {string} using the WebUI', function (usernames) {
return client.page.accountsPage().setUserActivated(usernames, false)
})
When('the user enables user/users {string} using the WebUI', function (usernames) {
return client.page.accountsPage().setUserActivated(usernames, true)
})
Then('the status indicator of user/users {string} should be {string} on the WebUI', function (usernames, status) {
return client.page.accountsPage().checkUsersStatus(usernames, status)
})
When(
'the user creates a new user with username {string}, email {string} and password {string} using the WebUI',
function (username, email, password) {
return client.page.accountsPage().createUser(username, email, password)
}
)
When('the user deletes user/users {string} using the WebUI', function (usernames) {
return client.page.accountsPage().deleteUsers(usernames)
})

View File

@@ -1,52 +0,0 @@
#!/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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@ func GetCommands(cfg *config.Config) cli.Commands {
}
}
// Execute is the entry point for the ocis-accounts command.
// Execute is the entry point for the ocis-app-provider command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "app-provider",
@@ -41,12 +41,12 @@ func Execute(cfg *config.Config) error {
return app.Run(os.Args)
}
// SutureService allows for the accounts command to be embedded and supervised by a suture supervisor tree.
// SutureService allows for the app-provider command to be embedded and supervised by a suture supervisor tree.
type SutureService struct {
cfg *config.Config
}
// NewSutureService creates a new accounts.SutureService
// NewSutureService creates a new app-provider.SutureService
func NewSutureService(cfg *ociscfg.Config) suture.Service {
cfg.AppProvider.Commons = cfg.Commons
return SutureService{

View File

@@ -4,8 +4,8 @@ import (
"net/http"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
accounts "github.com/owncloud/ocis/v2/extensions/accounts/pkg/service/v0"
"github.com/owncloud/ocis/v2/extensions/graph/pkg/service/v0/errorcode"
settings "github.com/owncloud/ocis/v2/extensions/settings/pkg/service/v0"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
)
@@ -42,7 +42,7 @@ func RequireAdmin(rm *roles.Manager, logger log.Logger) func(next http.Handler)
}
// check if permission is present in roles of the authenticated account
if rm.FindPermissionByID(r.Context(), roleIDs, accounts.AccountManagementPermissionID) != nil {
if rm.FindPermissionByID(r.Context(), roleIDs, settings.AccountManagementPermissionID) != nil {
next.ServeHTTP(w, r)
return
}

View File

@@ -4,9 +4,9 @@ import (
"net/http"
"github.com/go-chi/render"
accounts "github.com/owncloud/ocis/v2/extensions/accounts/pkg/service/v0"
"github.com/owncloud/ocis/v2/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/v2/extensions/ocs/pkg/service/v0/response"
settings "github.com/owncloud/ocis/v2/extensions/settings/pkg/service/v0"
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
)
@@ -31,7 +31,7 @@ func RequireAdmin(opts ...Option) func(next http.Handler) http.Handler {
}
// check if permission is present in roles of the authenticated account
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, accounts.AccountManagementPermissionID) != nil {
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, settings.AccountManagementPermissionID) != nil {
next.ServeHTTP(w, r)
return
}

View File

@@ -7,9 +7,9 @@ import (
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
accounts "github.com/owncloud/ocis/v2/extensions/accounts/pkg/service/v0"
"github.com/owncloud/ocis/v2/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/v2/extensions/ocs/pkg/service/v0/response"
settings "github.com/owncloud/ocis/v2/extensions/settings/pkg/service/v0"
settingsService "github.com/owncloud/ocis/v2/extensions/settings/pkg/service/v0"
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
)
@@ -56,13 +56,13 @@ func RequireSelfOrAdmin(opts ...Option) func(next http.Handler) http.Handler {
}
// check if account management permission is present in roles of the authenticated account
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, accounts.AccountManagementPermissionID) != nil {
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, settings.AccountManagementPermissionID) != nil {
next.ServeHTTP(w, r)
return
}
// check if self management permission is present in roles of the authenticated account
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, accounts.SelfManagementPermissionID) != nil {
if opt.RoleManager.FindPermissionByID(r.Context(), roleIDs, settings.SelfManagementPermissionID) != nil {
userid := chi.URLParam(r, "userid")
var err error
if userid, err = url.PathUnescape(userid); err != nil {

View File

@@ -1,455 +1,98 @@
package svc
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strconv"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/go-chi/chi/v5"
"github.com/owncloud/ocis/v2/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/v2/extensions/ocs/pkg/service/v0/response"
ocstracing "github.com/owncloud/ocis/v2/extensions/ocs/pkg/tracing"
merrors "go-micro.dev/v4/errors"
"go.opentelemetry.io/otel/attribute"
)
// ListUserGroups lists a users groups
func (o Ocs) ListUserGroups(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
if o.config.AccountBackend == "cs3" {
userid, _ = url.PathUnescape(userid)
switch o.config.AccountBackend {
case "cs3":
// TODO
o.mustRender(w, r, response.DataRender(&data.Groups{}))
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
// short circuit if there is a user already in the context
if u, ok := revactx.ContextGetUser(r.Context()); ok {
// we are not sure whether the current user in the context is the admin or the authenticated user.
if u.Username == userid {
// the OCS API is a REST API and it uses the username to look for groups. If the id from the user in the context
// differs from that of the url we can assume we are an admin because we are past the selfOrAdmin middleware.
_, span := ocstracing.TraceProvider.
Tracer("ocs").
Start(r.Context(), "ListUserGroups")
defer span.End()
span.SetAttributes(attribute.StringSlice("groups", u.Groups))
if len(u.Groups) > 0 {
o.mustRender(w, r, response.DataRender(&data.Groups{Groups: u.Groups}))
return
}
}
}
if isValidUUID(userid) {
account, err = o.getAccountService().GetAccount(r.Context(), &accountssvc.GetAccountRequest{
Id: userid,
})
} else {
// despite the confusion, if we make it here we got ourselves a username
account, err = o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not get list of user groups")
return
}
}
groups := make([]string, 0, len(account.MemberOf))
for i := range account.MemberOf {
if account.MemberOf[i].OnPremisesSamAccountName == "" {
o.logger.Warn().Str("groupid", account.MemberOf[i].Id).Msg("group on_premises_sam_account_name is empty, trying to lookup by id")
// we can try to look up the name
group, err := o.getGroupsService().GetGroup(r.Context(), &accountssvc.GetGroupRequest{
Id: account.MemberOf[i].Id,
})
if err != nil {
o.logger.Error().Err(err).Str("groupid", account.MemberOf[i].Id).Msg("could not get group")
continue
}
if group.OnPremisesSamAccountName == "" {
o.logger.Error().Err(err).Str("groupid", account.MemberOf[i].Id).Msg("group on_premises_sam_account_name is empty")
continue
}
groups = append(groups, group.OnPremisesSamAccountName)
} else {
groups = append(groups, account.MemberOf[i].OnPremisesSamAccountName)
}
}
o.logger.Error().Err(err).Int("count", len(groups)).Str("userid", account.Id).Msg("listing groups for user")
_, span := ocstracing.TraceProvider.
Tracer("ocs").
Start(r.Context(), "ListUserGroups")
defer span.End()
span.SetAttributes(attribute.StringSlice("groups", groups))
o.mustRender(w, r, response.DataRender(&data.Groups{Groups: groups}))
return
}
// AddToGroup adds a user to a group
func (o Ocs) AddToGroup(w http.ResponseWriter, r *http.Request) {
groupid := r.PostFormValue("groupid")
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
if groupid == "" {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "empty group assignment: unspecified group"))
switch o.config.AccountBackend {
case "cs3":
// TODO
o.cs3WriteNotSupported(w, r)
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
account, err := o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().AddMember(r.Context(), &accountssvc.AddMemberRequest{
AccountId: account.Id,
GroupId: group.Id,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", account.Id).Str("groupid", group.Id).Msg("could not add user to group")
return
}
o.logger.Debug().Str("userid", account.Id).Str("groupid", group.Id).Msg("added user to group")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// RemoveFromGroup removes a user from a group
func (o Ocs) RemoveFromGroup(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
// Really? a DELETE with form encoded body?!?
// but it is not encoded as mime, so we cannot just call r.ParseForm()
// read it manually
body, err := ioutil.ReadAll(r.Body)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, err.Error()))
switch o.config.AccountBackend {
case "cs3":
// TODO
o.cs3WriteNotSupported(w, r)
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err = r.Body.Close(); err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
return
}
values, err := url.ParseQuery(string(body))
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, err.Error()))
return
}
groupid := values.Get("groupid")
if groupid == "" {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "no group id"))
return
}
var account *accountsmsg.Account
if isValidUUID(userid) {
account, _ = o.getAccountService().GetAccount(r.Context(), &accountssvc.GetAccountRequest{
Id: userid,
})
} else {
// despite the confusion, if we make it here we got ourselves a username
account, err = o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not get list of user groups")
return
}
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().RemoveMember(r.Context(), &accountssvc.RemoveMemberRequest{
AccountId: account.Id,
GroupId: group.Id,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", account.Id).Str("groupid", group.Id).Msg("could not remove user from group")
return
}
o.logger.Debug().Str("userid", account.Id).Str("groupid", group.Id).Msg("removed user from group")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// ListGroups lists all groups
func (o Ocs) ListGroups(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("search")
query := ""
if search != "" {
query = fmt.Sprintf("id eq '%s' or on_premises_sam_account_name eq '%s'", escapeValue(search), escapeValue(search))
}
res, err := o.getGroupsService().ListGroups(r.Context(), &accountssvc.ListGroupsRequest{
Query: query,
})
if err != nil {
o.logger.Err(err).Msg("could not list users")
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not list users"))
switch o.config.AccountBackend {
case "cs3":
// TODO
o.mustRender(w, r, response.DataRender(&data.Groups{}))
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
groups := make([]string, 0, len(res.Groups))
for i := range res.Groups {
groups = append(groups, res.Groups[i].OnPremisesSamAccountName)
}
_, span := ocstracing.TraceProvider.
Tracer("ocs").
Start(r.Context(), "ListGroups")
defer span.End()
span.SetAttributes(attribute.StringSlice("groups", groups))
o.mustRender(w, r, response.DataRender(&data.Groups{Groups: groups}))
return
}
// AddGroup adds a group
// oC10 implementation: https://github.com/owncloud/core/blob/762780a23c9eadda4fb5fa8db99eba66a5100b6e/apps/provisioning_api/lib/Groups.php#L126-L154
func (o Ocs) AddGroup(w http.ResponseWriter, r *http.Request) {
groupid := r.PostFormValue("groupid")
displayname := r.PostFormValue("displayname")
gid := r.PostFormValue("gidnumber")
if displayname == "" && groupid == "" {
code := data.MetaFailure.StatusCode // v1
if response.APIVersion(r.Context()) == "2" {
code = data.MetaBadRequest.StatusCode
}
o.mustRender(w, r, response.ErrRender(code, "No groupid or display name provided"))
switch o.config.AccountBackend {
case "cs3":
o.cs3WriteNotSupported(w, r)
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if displayname == "" {
// oC10 OCS does not know about a group displayname
// therefore we fall back to the oC10 parameter groupid (which is the groupname in the oC10 world)
displayname = groupid
}
var gidNumber int64
var err error
if gid != "" {
gidNumber, err = strconv.ParseInt(gid, 10, 64)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "Cannot use the gidnumber provided"))
o.logger.Error().Err(err).Str("gid", gid).Str("groupid", groupid).Msg("Cannot use the gidnumber provided")
return
}
}
newGroup := &accountsmsg.Group{
Id: groupid,
DisplayName: displayname,
OnPremisesSamAccountName: groupid,
GidNumber: gidNumber,
}
group, err := o.getGroupsService().CreateGroup(r.Context(), &accountssvc.CreateGroupRequest{
Group: newGroup,
})
if err != nil {
merr := merrors.FromError(err)
switch merr.Code {
case http.StatusBadRequest:
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
case http.StatusConflict:
if response.APIVersion(r.Context()) == "2" {
// it seems the application framework sets the ocs status code to the httpstatus code, which affects the provisioning api
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaInvalidInput.StatusCode, merr.Detail))
}
default:
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", groupid).Msg("could not add group")
// TODO check error if group already existed
return
}
o.logger.Debug().Interface("group", group).Msg("added group")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// DeleteGroup deletes a group
func (o Ocs) DeleteGroup(w http.ResponseWriter, r *http.Request) {
groupid := chi.URLParam(r, "groupid")
groupid, err := url.PathUnescape(groupid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
switch o.config.AccountBackend {
case "cs3":
o.cs3WriteNotSupported(w, r)
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
_, err = o.getGroupsService().DeleteGroup(r.Context(), &accountssvc.DeleteGroupRequest{
Id: group.Id,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", group.Id).Msg("could not remove group")
return
}
o.logger.Debug().Str("groupid", group.Id).Msg("removed group")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// GetGroupMembers lists all members of a group
func (o Ocs) GetGroupMembers(w http.ResponseWriter, r *http.Request) {
groupid := chi.URLParam(r, "groupid")
groupid, err := url.PathUnescape(groupid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
switch o.config.AccountBackend {
case "cs3":
// TODO
o.mustRender(w, r, response.DataRender(&data.Users{}))
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
res, err := o.getGroupsService().ListMembers(r.Context(), &accountssvc.ListMembersRequest{Id: group.Id})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", group.Id).Msg("could not get list of members")
return
}
members := make([]string, 0, len(res.Members))
for i := range res.Members {
members = append(members, res.Members[i].OnPremisesSamAccountName)
}
o.logger.Error().Err(err).Int("count", len(members)).Str("groupid", groupid).Msg("listing group members")
o.mustRender(w, r, response.DataRender(&data.Users{Users: members}))
}
func isValidUUID(uuid string) bool {
r := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
return r.MatchString(uuid)
}
func (o Ocs) fetchGroupByName(ctx context.Context, name string) (*accountsmsg.Group, error) {
var res *accountssvc.ListGroupsResponse
res, err := o.getGroupsService().ListGroups(ctx, &accountssvc.ListGroupsRequest{
Query: fmt.Sprintf("on_premises_sam_account_name eq '%v'", escapeValue(name)),
})
if err != nil {
return nil, err
}
if res != nil && len(res.Groups) == 1 {
return res.Groups[0], nil
}
return nil, merrors.NotFound("", data.MessageGroupNotFound)
return
}

View File

@@ -11,8 +11,6 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
"github.com/owncloud/ocis/v2/extensions/ocs/pkg/config"
ocsm "github.com/owncloud/ocis/v2/extensions/ocs/pkg/middleware"
"github.com/owncloud/ocis/v2/extensions/ocs/pkg/service/v0/data"
@@ -61,7 +59,7 @@ func NewService(opts ...Option) Service {
}
if svc.config.AccountBackend == "" {
svc.config.AccountBackend = "accounts"
svc.config.AccountBackend = "cs3"
}
requireUser := ocsm.RequireUser(
@@ -159,10 +157,6 @@ func (o Ocs) NotFound(w http.ResponseWriter, r *http.Request) {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "not found"))
}
func (o Ocs) getAccountService() accountssvc.AccountsService {
return accountssvc.NewAccountsService("com.owncloud.api.accounts", grpc.DefaultClient)
}
func (o Ocs) getCS3Backend() backend.UserBackend {
revaClient, err := pool.GetGatewayServiceClient(o.config.Reva.Address)
if err != nil {
@@ -171,10 +165,6 @@ func (o Ocs) getCS3Backend() backend.UserBackend {
return backend.NewCS3UserBackend(nil, revaClient, o.config.MachineAuthAPIKey, o.logger)
}
func (o Ocs) getGroupsService() accountssvc.GroupsService {
return accountssvc.NewGroupsService("com.owncloud.api.accounts", grpc.DefaultClient)
}
// NotImplementedStub returns a not implemented error
func (o Ocs) NotImplementedStub(w http.ResponseWriter, r *http.Request) {
o.mustRender(w, r, response.ErrRender(data.MetaUnknownError.StatusCode, "Not implemented"))

View File

@@ -4,89 +4,40 @@ import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
accountsmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/accounts/v0"
storemsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/store/v0"
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
revauser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/auth/scope"
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/token/manager/jwt"
"github.com/go-chi/chi/v5"
"github.com/go-micro/plugins/v4/client/grpc"
"github.com/google/uuid"
"github.com/owncloud/ocis/v2/extensions/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/v2/extensions/ocs/pkg/service/v0/response"
ocstracing "github.com/owncloud/ocis/v2/extensions/ocs/pkg/tracing"
"github.com/pkg/errors"
merrors "go-micro.dev/v4/errors"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// GetSelf returns the currently logged in user
func (o Ocs) GetSelf(w http.ResponseWriter, r *http.Request) {
var account *accountsmsg.Account
var err error
u, ok := revactx.ContextGetUser(r.Context())
if !ok || u.Id == nil || u.Id.OpaqueId == "" {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "user is missing an id"))
return
}
account, err = o.getAccountService().GetAccount(r.Context(), &accountssvc.GetAccountRequest{
Id: u.Id.OpaqueId,
})
if err != nil {
merr := merrors.FromError(err)
// TODO(someone) this fix is in place because if the user backend (PROXY_ACCOUNT_BACKEND_TYPE) is set to, for instance,
// cs3, we cannot count with the accounts service.
if u != nil {
d := &data.User{
UserID: u.Username,
DisplayName: u.DisplayName,
LegacyDisplayName: u.DisplayName,
Email: u.Mail,
UIDNumber: u.UidNumber,
GIDNumber: u.GidNumber,
}
o.mustRender(w, r, response.DataRender(d))
return
}
o.logger.Error().Err(merr).Interface("user", u).Msg("could not get account for user")
return
}
// remove password from log if it is set
if account.PasswordProfile != nil {
account.PasswordProfile.Password = ""
}
o.logger.Debug().Interface("account", account).Msg("got user")
d := &data.User{
UserID: account.OnPremisesSamAccountName,
DisplayName: account.DisplayName,
LegacyDisplayName: account.DisplayName,
Email: account.Mail,
UIDNumber: account.UidNumber,
GIDNumber: account.GidNumber,
// TODO hide enabled flag or it might get rendered as false
UserID: u.Username,
DisplayName: u.DisplayName,
LegacyDisplayName: u.DisplayName,
Email: u.Mail,
UIDNumber: u.UidNumber,
GIDNumber: u.GidNumber,
}
o.mustRender(w, r, response.DataRender(d))
return
}
// GetUser returns the user with the given userid
@@ -96,15 +47,13 @@ func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) {
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
var user *cs3.User
switch {
case userid == "":
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "missing user in context"))
case o.config.AccountBackend == "accounts":
account, err = o.fetchAccountByUsername(r.Context(), userid)
case o.config.AccountBackend == "cs3":
account, err = o.fetchAccountFromCS3Backend(r.Context(), userid)
user, err = o.fetchAccountFromCS3Backend(r.Context(), userid)
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
@@ -120,28 +69,16 @@ func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) {
return
}
// remove password from log if it is set
if account.PasswordProfile != nil {
account.PasswordProfile.Password = ""
}
o.logger.Debug().Interface("account", account).Msg("got user")
// mimic the oc10 bool as string for the user enabled property
var enabled string
if account.AccountEnabled {
enabled = "true"
} else {
enabled = "false"
}
o.logger.Debug().Interface("user", user).Msg("got user")
d := &data.User{
UserID: account.OnPremisesSamAccountName,
DisplayName: account.DisplayName,
LegacyDisplayName: account.DisplayName,
Email: account.Mail,
UIDNumber: account.UidNumber,
GIDNumber: account.GidNumber,
Enabled: enabled, // TODO include in response only when admin?
UserID: user.Username,
DisplayName: user.DisplayName,
LegacyDisplayName: user.DisplayName,
Email: user.Mail,
UIDNumber: user.UidNumber,
GIDNumber: user.GidNumber,
Enabled: "true", // TODO include in response only when admin?
// TODO query storage registry for free space? of home storage, maybe...
Quota: &data.Quota{
Free: 2840756224000,
@@ -162,488 +99,57 @@ func (o Ocs) GetUser(w http.ResponseWriter, r *http.Request) {
// AddUser creates a new user account
func (o Ocs) AddUser(w http.ResponseWriter, r *http.Request) {
userid := r.PostFormValue("userid")
password := r.PostFormValue("password")
displayname := r.PostFormValue("displayname")
email := r.PostFormValue("email")
uid := r.PostFormValue("uidnumber")
gid := r.PostFormValue("gidnumber")
var uidNumber, gidNumber int64
var err error
if uid != "" {
uidNumber, err = strconv.ParseInt(uid, 10, 64)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "Cannot use the uidnumber provided"))
o.logger.Error().Err(err).Str("userid", userid).Msg("Cannot use the uidnumber provided")
return
}
}
if gid != "" {
gidNumber, err = strconv.ParseInt(gid, 10, 64)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "Cannot use the gidnumber provided"))
o.logger.Error().Err(err).Str("userid", userid).Msg("Cannot use the gidnumber provided")
return
}
}
if strings.TrimSpace(password) == "" {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "empty password not allowed"))
o.logger.Error().Str("userid", userid).Msg("empty password not allowed")
return
}
// fallbacks
/* TODO decide if we want to make these fallbacks. Keep in mind:
- oCIS requires a preferred_name and email
*/
if displayname == "" {
displayname = userid
}
newAccount := &accountsmsg.Account{
Id: uuid.New().String(),
DisplayName: displayname,
PreferredName: userid,
OnPremisesSamAccountName: userid,
PasswordProfile: &accountsmsg.PasswordProfile{
Password: password,
},
Mail: email,
AccountEnabled: true,
}
if uidNumber != 0 {
newAccount.UidNumber = uidNumber
}
if gidNumber != 0 {
newAccount.GidNumber = gidNumber
}
var account *accountsmsg.Account
switch o.config.AccountBackend {
case "accounts":
account, err = o.getAccountService().CreateAccount(r.Context(), &accountssvc.CreateAccountRequest{
Account: newAccount,
})
case "cs3":
o.cs3WriteNotSupported(w, r)
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
switch merr.Code {
case http.StatusBadRequest:
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
case http.StatusConflict:
if response.APIVersion(r.Context()) == "2" {
// it seems the application framework sets the ocs status code to the httpstatus code, which affects the provisioning api
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaInvalidInput.StatusCode, merr.Detail))
}
default:
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not add user")
// TODO check error if account already existed
return
}
// remove password from log if it is set
if account.PasswordProfile != nil {
account.PasswordProfile.Password = ""
}
o.logger.Debug().Interface("account", account).Msg("added user")
// mimic the oc10 bool as string for the user enabled property
var enabled string
if account.AccountEnabled {
enabled = "true"
} else {
enabled = "false"
}
o.mustRender(w, r, response.DataRender(&data.User{
UserID: account.OnPremisesSamAccountName,
DisplayName: account.DisplayName,
LegacyDisplayName: account.DisplayName,
Email: account.Mail,
UIDNumber: account.UidNumber,
GIDNumber: account.GidNumber,
Enabled: enabled,
}))
}
// EditUser creates a new user account
func (o Ocs) EditUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
switch o.config.AccountBackend {
case "accounts":
account, err = o.fetchAccountByUsername(r.Context(), userid)
case "cs3":
o.cs3WriteNotSupported(w, r)
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not edit user")
return
}
req := accountssvc.UpdateAccountRequest{
Account: &accountsmsg.Account{
Id: account.Id,
},
}
key := r.PostFormValue("key")
value := r.PostFormValue("value")
switch key {
case "email":
req.Account.Mail = value
req.UpdateMask = &fieldmaskpb.FieldMask{Paths: []string{"Mail"}}
case "username":
req.Account.PreferredName = value
req.Account.OnPremisesSamAccountName = value
req.UpdateMask = &fieldmaskpb.FieldMask{Paths: []string{"PreferredName", "OnPremisesSamAccountName"}}
case "password":
req.Account.PasswordProfile = &accountsmsg.PasswordProfile{
Password: value,
}
req.UpdateMask = &fieldmaskpb.FieldMask{Paths: []string{"PasswordProfile.Password"}}
case "displayname", "display":
req.Account.DisplayName = value
req.UpdateMask = &fieldmaskpb.FieldMask{Paths: []string{"DisplayName"}}
default:
// https://github.com/owncloud/core/blob/24b7fa1d2604a208582055309a5638dbd9bda1d1/apps/provisioning_api/lib/Users.php#L321
o.mustRender(w, r, response.ErrRender(103, "unknown key '"+key+"'"))
return
}
account, err = o.getAccountService().UpdateAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
switch merr.Code {
case http.StatusBadRequest:
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
default:
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("account_id", req.Account.Id).Str("user_id", userid).Msg("could not edit user")
return
}
// remove password from log if it is set
if account.PasswordProfile != nil {
account.PasswordProfile.Password = ""
}
o.logger.Debug().Interface("account", account).Msg("updated user")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// DeleteUser deletes a user
func (o Ocs) DeleteUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
switch o.config.AccountBackend {
case "accounts":
account, err = o.fetchAccountByUsername(r.Context(), userid)
case "cs3":
o.cs3WriteNotSupported(w, r)
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not delete user")
return
}
if o.config.Reva.Address != "" && o.config.StorageUsersDriver != "owncloud" {
t, err := o.mintTokenForUser(r.Context(), account)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "error minting token").Error()))
return
}
ctx := metadata.AppendToOutgoingContext(r.Context(), revactx.TokenHeader, t)
gwc, err := pool.GetGatewayServiceClient(o.config.Reva.Address)
if err != nil {
o.logger.Error().Err(err).Msg("error securing a connection to Reva gateway")
}
lsRes, err := gwc.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{
Filters: []*provider.ListStorageSpacesRequest_Filter{
{
Type: provider.ListStorageSpacesRequest_Filter_TYPE_OWNER,
Term: &provider.ListStorageSpacesRequest_Filter_Owner{
Owner: &revauser.UserId{
Idp: o.config.IdentityManagement.Address,
OpaqueId: account.Id,
},
},
},
},
})
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "could not list owned personal spaces").Error()))
return
}
if lsRes.Status.Code != rpcv1beta1.Code_CODE_OK {
o.logger.Error().
Interface("status", lsRes.Status).
Msg("DeleteUser: could not list personal spaces")
return
}
for _, space := range lsRes.StorageSpaces {
dsRes, err := gwc.DeleteStorageSpace(ctx, &provider.DeleteStorageSpaceRequest{
Id: space.Id,
})
if err != nil {
o.logger.Error().Err(err).Msg("DeleteUser: could not make delete space request")
continue
}
if dsRes.Status.Code != rpcv1beta1.Code_CODE_OK && dsRes.Status.Code != rpcv1beta1.Code_CODE_NOT_FOUND {
o.logger.Error().
Interface("status", dsRes.Status).
Msg("DeleteUser: could not delete space")
continue
}
}
lsRes, err = gwc.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{
Filters: []*provider.ListStorageSpacesRequest_Filter{
{
Type: provider.ListStorageSpacesRequest_Filter_TYPE_OWNER,
Term: &provider.ListStorageSpacesRequest_Filter_Owner{
Owner: &revauser.UserId{
Idp: o.config.IdentityManagement.Address,
OpaqueId: account.Id,
},
},
},
},
})
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, errors.Wrap(err, "could not list owned personal spaces").Error()))
return
}
if lsRes.Status.Code != rpcv1beta1.Code_CODE_OK {
o.logger.Error().
Interface("status", lsRes.Status).
Msg("DeleteUser: could not list personal spaces")
return
}
for _, space := range lsRes.StorageSpaces {
dsRes, err := gwc.DeleteStorageSpace(ctx, &provider.DeleteStorageSpaceRequest{
Opaque: &typesv1beta1.Opaque{
Map: map[string]*typesv1beta1.OpaqueEntry{
"purge": {},
},
},
Id: space.Id,
})
if err != nil {
o.logger.Error().Err(err).Msg("DeleteUser: could not make delete space request")
continue
}
if dsRes.Status.Code != rpcv1beta1.Code_CODE_OK && dsRes.Status.Code != rpcv1beta1.Code_CODE_NOT_FOUND {
o.logger.Error().
Interface("status", dsRes.Status).
Msg("DeleteUser: could not delete space")
continue
}
}
}
req := accountssvc.DeleteAccountRequest{
Id: account.Id,
}
_, err = o.getAccountService().DeleteAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", req.Id).Msg("could not delete user")
return
}
o.logger.Debug().Str("userid", req.Id).Msg("deleted user")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// TODO(refs) this to ocis-pkg ... we are minting tokens all over the place ... or use a service? ... like reva?
func (o Ocs) mintTokenForUser(ctx context.Context, account *accountsmsg.Account) (string, error) {
tm, _ := jwt.New(map[string]interface{}{
"secret": o.config.TokenManager.JWTSecret,
"expires": int64(24 * 60 * 60),
})
u := &revauser.User{
Id: &revauser.UserId{
OpaqueId: account.Id,
Idp: o.config.IdentityManagement.Address,
},
Groups: []string{},
UidNumber: account.UidNumber,
GidNumber: account.GidNumber,
}
s, err := scope.AddOwnerScope(nil)
if err != nil {
return "", err
}
return tm.MintToken(ctx, u, s)
}
// EnableUser enables a user
func (o Ocs) EnableUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
switch o.config.AccountBackend {
case "accounts":
account, err = o.fetchAccountByUsername(r.Context(), userid)
case "cs3":
o.cs3WriteNotSupported(w, r)
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not enable user")
return
}
account.AccountEnabled = true
req := accountssvc.UpdateAccountRequest{
Account: account,
UpdateMask: &field_mask.FieldMask{
Paths: []string{"AccountEnabled"},
},
}
_, err = o.getAccountService().UpdateAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested account could not be found"))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("account_id", account.Id).Msg("could not enable account")
return
}
o.logger.Debug().Str("account_id", account.Id).Msg("enabled user")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// DisableUser disables a user
func (o Ocs) DisableUser(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
switch o.config.AccountBackend {
case "accounts":
account, err = o.fetchAccountByUsername(r.Context(), userid)
case "cs3":
o.cs3WriteNotSupported(w, r)
return
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not disable user")
return
}
account.AccountEnabled = false
req := accountssvc.UpdateAccountRequest{
Account: account,
UpdateMask: &field_mask.FieldMask{
Paths: []string{"AccountEnabled"},
},
}
_, err = o.getAccountService().UpdateAccount(r.Context(), &req)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, "The requested account could not be found"))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("account_id", account.Id).Msg("could not disable account")
return
}
o.logger.Debug().Str("account_id", account.Id).Msg("disabled user")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// GetSigningKey returns the signing key for the current user. It will create it on the fly if it does not exist
@@ -720,19 +226,7 @@ func (o Ocs) GetSigningKey(w http.ResponseWriter, r *http.Request) {
// ListUsers lists the users
func (o Ocs) ListUsers(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("search")
query := ""
if search != "" {
query = fmt.Sprintf("on_premises_sam_account_name eq '%s'", escapeValue(search))
}
var res *accountssvc.ListAccountsResponse
var err error
switch o.config.AccountBackend {
case "accounts":
res, err = o.getAccountService().ListAccounts(r.Context(), &accountssvc.ListAccountsRequest{
Query: query,
})
case "cs3":
// TODO
o.cs3WriteNotSupported(w, r)
@@ -740,19 +234,6 @@ func (o Ocs) ListUsers(w http.ResponseWriter, r *http.Request) {
default:
o.logger.Fatal().Msgf("Invalid accounts backend type '%s'", o.config.AccountBackend)
}
if err != nil {
o.logger.Err(err).Msg("could not list users")
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not list users"))
return
}
users := make([]string, 0, len(res.Accounts))
for i := range res.Accounts {
users = append(users, res.Accounts[i].OnPremisesSamAccountName)
}
o.mustRender(w, r, response.DataRender(&data.Users{Users: users}))
}
// escapeValue escapes all special characters in the value
@@ -760,33 +241,13 @@ func escapeValue(value string) string {
return strings.ReplaceAll(value, "'", "''")
}
func (o Ocs) fetchAccountByUsername(ctx context.Context, name string) (*accountsmsg.Account, error) {
var res *accountssvc.ListAccountsResponse
res, err := o.getAccountService().ListAccounts(ctx, &accountssvc.ListAccountsRequest{
Query: fmt.Sprintf("on_premises_sam_account_name eq '%v'", escapeValue(name)),
})
if err != nil {
return nil, err
}
if res != nil && len(res.Accounts) == 1 {
return res.Accounts[0], nil
}
return nil, merrors.NotFound("", data.MessageUserNotFound)
}
func (o Ocs) fetchAccountFromCS3Backend(ctx context.Context, name string) (*accountsmsg.Account, error) {
func (o Ocs) fetchAccountFromCS3Backend(ctx context.Context, name string) (*cs3.User, error) {
backend := o.getCS3Backend()
u, _, err := backend.GetUserByClaims(ctx, "username", name, false)
if err != nil {
return nil, err
}
return &accountsmsg.Account{
OnPremisesSamAccountName: u.Username,
DisplayName: u.DisplayName,
Mail: u.Mail,
UidNumber: u.UidNumber,
GidNumber: u.GidNumber,
}, nil
return u, nil
}
func (o Ocs) cs3WriteNotSupported(w http.ResponseWriter, r *http.Request) {

Some files were not shown because too many files have changed in this diff Show More