mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-23 22:29:59 -05:00
Remove "accounts" service
This commit is contained in:
committed by
Ralf Haferkamp
parent
5ba1b8f2c1
commit
d25aa7b20f
72
.drone.star
72
.drone.star
@@ -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
2
.vscode/launch.json
vendored
@@ -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
|
||||
|
||||
1
Makefile
1
Makefile
@@ -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 \
|
||||
|
||||
1
docs/extensions/accounts/.gitignore
vendored
1
docs/extensions/accounts/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
grpc.md
|
||||
@@ -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 >}}
|
||||
@@ -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" >}}
|
||||
@@ -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)
|
||||
@@ -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/
|
||||
```
|
||||
@@ -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) |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!bin/
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"amd": true
|
||||
},
|
||||
"extends": [
|
||||
"standard",
|
||||
"plugin:vue/essential"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
|
||||
}
|
||||
}
|
||||
17
extensions/accounts/.gitignore
vendored
17
extensions/accounts/.gitignore
vendored
@@ -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.*
|
||||
768
extensions/accounts/.yarn/releases/yarn-3.1.0.cjs
vendored
768
extensions/accounts/.yarn/releases/yarn-3.1.0.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.1.0.cjs
|
||||
|
||||
enableScripts: false
|
||||
enableTelemetry: false
|
||||
@@ -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
|
||||
@@ -1,9 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:generate make generate
|
||||
//go:embed assets/*
|
||||
var Assets embed.FS
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}()
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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."`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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:"-"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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."`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package config
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `yaml:"-"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 := ×tamppb.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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
checks = ["all", "-ST1003", "-ST1000", "-SA1019"]
|
||||
@@ -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 ¬FoundErr{"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 ¬FoundErr{"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 ¬FoundErr{"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 ¬FoundErr{"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
|
||||
}
|
||||
@@ -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)
|
||||
//}
|
||||
@@ -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 = ¬FoundErr{"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 = ¬FoundErr{"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 = ¬FoundErr{"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 = ¬FoundErr{"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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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()
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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('&')) : '')
|
||||
}
|
||||
@@ -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('&')) : '')
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
]
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user