Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
# maintain v2 separate mocks dir
dir: "{{.InterfaceDir}}/mocks"
structname: "{{.InterfaceName}}"
filename: "{{.InterfaceName | snakecase }}.go"
pkgname: mocks
template: testify
packages:
github.com/qsfera/server/services/graph/pkg/service/v0:
config:
dir: mocks
interfaces:
BaseGraphProvider: {}
DrivesDriveItemProvider: {}
DriveItemPermissionsProvider: {}
HTTPClient: {}
Permissions: {}
RoleService: {}
UsersUserProfilePhotoProvider: {}
github.com/opencloud-eu/reva/v2/pkg/events:
config:
dir: mocks
interfaces:
Publisher: {}
github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata:
config:
dir: mocks
interfaces:
Storage: {}
github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool:
config:
dir: mocks
interfaces:
Selectable:
config:
filename: gateway_selector.go
github.com/qsfera/server/services/graph/pkg/identity:
interfaces:
Backend: {}
EducationBackend: {}
github.com/go-ldap/ldap/v3:
config:
dir: pkg/identity/mocks
interfaces:
Client:
config:
filename: ldapclient.go
github.com/nats-io/nats.go/jetstream:
config:
dir: mocks
interfaces:
KeyValue: {}
KeyValueEntry: {}
+33
View File
@@ -0,0 +1,33 @@
SHELL := bash
NAME := graph
OUTPUT_DIR = ./pkg/l10n
TEMPLATE_FILE = ./pkg/l10n/graph.pot
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
include ../../.bingo/Variables.mk
endif
include ../../.make/default.mk
include ../../.make/go.mk
include ../../.make/release.mk
include ../../.make/docs.mk
.PHONY: go-generate
go-generate: $(MOCKERY)
$(MOCKERY)
.PHONY: l10n-pull
l10n-pull:
cd $(OUTPUT_DIR) && tx pull --all --force --skip --minimum-perc=75
.PHONY: l10n-push
l10n-push:
cd $(OUTPUT_DIR) && tx push -s --skip
.PHONY: l10n-read
l10n-read: $(GO_XGETTEXT)
$(GO_XGETTEXT) -o $(OUTPUT_DIR)/graph.pot --keyword=l10n.Template --add-comments -s pkg/service/v0/spacetemplates.go -s pkg/unifiedrole/roles.go
.PHONY: l10n-clean
l10n-clean:
rm -f $(TEMPLATE_FILE);
+186
View File
@@ -0,0 +1,186 @@
# Graph
The graph service provides the Graph API which is a RESTful web API used to access КуСфера
resources. It is inspired by the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/use-the-api)
and can be used by clients or other services or extensions. Visit the [Libre Graph API](https://docs.qsfera.eu/swagger/libre-graph-api/)
for a detailed specification of the API implemented by the graph service.
## Sequence Diagram
The following image gives an overview of the scenario when a client requests to list available spaces the user has access to. To do so, the client is directed with his request automatically via the proxy service to the graph service.
<img src="https://raw.githubusercontent.com/qsfera/server/main/services/graph/images/mermaid-graph.svg" width="900" />
## Users and Groups API
The graph service provides endpoints for querying users and groups. It features two different backend implementations:
* `ldap`: This is currently the default backend. It queries user and group information from an
LDAP server. Depending on the configuration, it can also be used to manage (create, update,
delete) users and groups provided by an LDAP server.
* `cs3`: This backend queries users and groups using the CS3 identity APIs as implemented by the
`users` and `groups` service. This backend is currently still experimental and only implements a
subset of the Libre Graph API. It should not be used in production.
### LDAP Configuration
The LDAP backend is configured using a set of environment variables. A detailed list of all the
available configuration options can be found in the [documentation](https://docs.qsfera.eu/docs/dev/server/services/graph/environment-variables).
The LDAP related options are prefixed with `OC_LDAP_` (or `GRAPH_LDAP_` for settings specific to graph service).
#### Read-Only Access to Existing LDAP Servers
To connect the graph service to an existing LDAP server, set `OC_LDAP_SERVER_WRITE_ENABLED` to
`false` to prevent the graph service from sending write operations to the LDAP server. Also set the
various `OC_LDAP_*` environment variables to match the configuration of the LDAP server you are connecting
to. A more detailed explanation can be found [here](https://docs.qsfera.eu/docs/admin/configuration/authentication-and-user-management/.
#### Using a Write Enabled LDAP Server
To use the graph service for managing (create, update, delete) users and groups, a write enabled LDAP
server is required. In the default configuration, the graph service will use the simple LDAP server
that is bundled with КуСфера in the `idm` service which provides all the required features.
It is also possible to setup up an external LDAP server with write access for use with КуСфера. It is
recommend to use OpenLDAP for this. The LDAP server needs to fulfill a couple of requirements with
respect to the available schema:
* The LDAP server must provide the `inetOrgPerson` object class for users and the `groupOfNames`
object class for groups.
* The graph service maintains a few additional attributes for users and groups that are not
available in the standard LDAP schema. An schema file, ready to use with OpenLDAP, defining those
additional attributes is available [here](https://github.com/qsfera/server-compose/blob/main/config/ldap/schemas/10_qsfera_schema.ldif)
## Query Filters Provided by the Graph API
Some API endpoints provided by the graph service allow to specify query filters. The filter syntax
is based on the [OData Specification](https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter).
See the [Libre Graph API](https://docs.qsfera.eu/swagger/libre-graph-api/#/users/ListUsers) for examples
on the filters supported when querying users.
## Caching
The `graph` service can use a configured store via `GRAPH_CACHE_STORE`. Possible stores are:
- `memory`: Basic in-memory store and the default.
- `redis-sentinel`: Stores data in a configured Redis Sentinel cluster.
- `nats-js-kv`: Stores data using key-value-store feature of [nats jetstream](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
- `noop`: Stores nothing. Useful for testing. Not recommended in production environments.
Other store types may work but are not supported currently.
Store specific notes:
- When using `redis-sentinel`, the Redis master to use is configured via e.g. `OC_CACHE_STORE_NODES` in the form of `<sentinel-host>:<sentinel-port>/<redis-master>` like `10.10.0.200:26379/mymaster`.
- When using `nats-js-kv` it is recommended to set `OC_CACHE_STORE_NODES` to the same value as `OC_EVENTS_ENDPOINT`. That way the cache uses the same nats instance as the event bus.
- When using the `nats-js-kv` store, it is possible to set `OC_CACHE_DISABLE_PERSISTENCE` to instruct nats to not persist cache data on disc.
## Keycloak Configuration For The Personal Data Export
If Keycloak is used for authentication, GDPR regulations require to add all personal identifiable information that Keycloak has about the user to the personal data export. To do this, the following environment variables must be set:
* `OC_KEYCLOAK_BASE_PATH` - The URL to the keycloak instance.
* `OC_KEYCLOAK_CLIENT_ID` - The client ID of the client that is used to authenticate with keycloak, this client has to be able to list users and get the credential data.
* `OC_KEYCLOAK_CLIENT_SECRET` - The client secret of the client that is used to authenticate with keycloak.
* `OC_KEYCLOAK_CLIENT_REALM` - The realm the client is defined in.
* `OC_KEYCLOAK_USER_REALM` - The realm the КуСфера users are defined in.
* `OC_KEYCLOAK_INSECURE_SKIP_VERIFY` - If set to true, the TLS certificate of the keycloak instance is not verified.
### Keycloak Client Configuration
The client that is used to authenticate with keycloak has to be able to list users and get the credential data. To do this, the following roles have to be assigned to the client and they have to be about the realm that contains the КуСфера users:
* `view-users`
* `view-identity-providers`
* `view-realm`
* `view-clients`
* `view-events`
* `view-authorization`
:::note
These roles are only available to assign if the client is in the `master` realm.
:::
## Translations
The `graph` service has embedded translations sourced via transifex to provide a basic set of translated languages. These embedded translations are available for all deployment scenarios. In addition, the service supports custom translations, though it is currently not possible to just add custom translations to embedded ones. If custom translations are configured, the embedded ones are not used. To configure custom translations, the `GRAPH_TRANSLATION_PATH` environment variable needs to point to a base folder that will contain the translation files. This path must be available from all instances of the graph service, a shared storage is recommended. Translation files must be of type [.po](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files) or [.mo](https://www.gnu.org/software/gettext/manual/html_node/Binaries.html). For each language, the filename needs to be `graph.po` (or `graph.mo`) and stored in a folder structure defining the language code. In general the path/name pattern for a translation file needs to be:
```text
{GRAPH_TRANSLATION_PATH}/{language-code}/LC_MESSAGES/graph.po
```
The language code pattern is composed of `language[_territory]` where `language` is the base language and `_territory` is optional and defines a country.
For example, for the language `de`, one needs to place the corresponding translation files to `{GRAPH_TRANSLATION_PATH}/de_DE/LC_MESSAGES/graph.po`.
<!-- also see the notifications readme -->
:::warning
For the time being, the embedded КуСфера Web frontend only supports the main language code but does not handle any territory. When strings are available in the language code `language_territory`, the web frontend does not see it as it only requests `language`. In consequence, any translations made must exist in the requested `language` to avoid a fallback to the default.
:::
### Translation Rules
* If a requested language code is not available, the service tries to fall back to the base language if available. For example, if the requested language-code `de_DE` is not available, the service tries to fall back to translations in the `de` folder.
* If the base language `de` is also not available, the service falls back to the system's default English (`en`),
which is the source of the texts provided by the code.
## Default Language
The default language can be defined via the `OC_DEFAULT_LANGUAGE` environment variable. See the `settings` service for a detailed description.
## Unified Role Management
Unified Roles are roles granted a user for sharing and can be enabled or disabled. A CLI command is provided to list existing roles and their state among other data.
:::info
Note that a disabled role does not lose previously assigned permissions. It only means that the role is not available for new assignments.
:::
The following roles are **enabled** by default:
- `UnifiedRoleViewerID`
- `UnifiedRoleSpaceViewer`
- `UnifiedRoleEditor`
- `UnifiedRoleSpaceEditor`
- `UnifiedRoleFileEditor`
- `UnifiedRoleEditorLite`
- `UnifiedRoleManager`
The following role is **disabled** by default:
- `UnifiedRoleSecureViewer`
To enable disabled roles like the `UnifiedRoleSecureViewer`, you must provide the UID(s) by one of the following methods:
- Using the `GRAPH_AVAILABLE_ROLES` environment variable.
- Setting the `available_roles` configuration value.
The following CLI command simplifies the process of finding out which UID belongs to which role:
```bash
qsfera graph list-unified-roles
```
The output of this command includes the following information for each role:
* `UID`\
The unique identifier of the role.
* `Enabled`\
Whether the role is enabled or not.
* `Description`\
A short description of the role.
* `Condition`
* `Allowed resource actions`
**Example output (shortned)**
```bash
+--------------------------------------+----------+--------------------------------+--------------------------------+------------------------------------------+
| UID | ENABLED | DESCRIPTION | CONDITION | ALLOWED RESOURCE ACTIONS |
+--------------------------------------+----------+--------------------------------+--------------------------------+------------------------------------------+
| a8d5fe5e-96e3-418d-825b-534dbdf22b99 | enabled | View and download. | exists @Resource.Root | libre.graph/driveItem/path/read |
| | | | | libre.graph/driveItem/quota/read |
| | | | | libre.graph/driveItem/content/read |
| | | | | libre.graph/driveItem/permissions/read |
| | | | | libre.graph/driveItem/children/read |
| | | | | libre.graph/driveItem/deleted/read |
| | | | | libre.graph/driveItem/basic/read |
+--------------------------------------+----------+--------------------------------+--------------------------------+------------------------------------------+
```
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

@@ -0,0 +1,177 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
"github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
"github.com/opencloud-eu/libre-graph-api-go"
mock "github.com/stretchr/testify/mock"
)
// NewBaseGraphProvider creates a new instance of BaseGraphProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewBaseGraphProvider(t interface {
mock.TestingT
Cleanup(func())
}) *BaseGraphProvider {
mock := &BaseGraphProvider{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// BaseGraphProvider is an autogenerated mock type for the BaseGraphProvider type
type BaseGraphProvider struct {
mock.Mock
}
type BaseGraphProvider_Expecter struct {
mock *mock.Mock
}
func (_m *BaseGraphProvider) EXPECT() *BaseGraphProvider_Expecter {
return &BaseGraphProvider_Expecter{mock: &_m.Mock}
}
// CS3ReceivedOCMSharesToDriveItems provides a mock function for the type BaseGraphProvider
func (_mock *BaseGraphProvider) CS3ReceivedOCMSharesToDriveItems(ctx context.Context, receivedOCMShares []*ocmv1beta1.ReceivedShare) ([]libregraph.DriveItem, error) {
ret := _mock.Called(ctx, receivedOCMShares)
if len(ret) == 0 {
panic("no return value specified for CS3ReceivedOCMSharesToDriveItems")
}
var r0 []libregraph.DriveItem
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, []*ocmv1beta1.ReceivedShare) ([]libregraph.DriveItem, error)); ok {
return returnFunc(ctx, receivedOCMShares)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, []*ocmv1beta1.ReceivedShare) []libregraph.DriveItem); ok {
r0 = returnFunc(ctx, receivedOCMShares)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]libregraph.DriveItem)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, []*ocmv1beta1.ReceivedShare) error); ok {
r1 = returnFunc(ctx, receivedOCMShares)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CS3ReceivedOCMSharesToDriveItems'
type BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call struct {
*mock.Call
}
// CS3ReceivedOCMSharesToDriveItems is a helper method to define mock.On call
// - ctx context.Context
// - receivedOCMShares []*ocmv1beta1.ReceivedShare
func (_e *BaseGraphProvider_Expecter) CS3ReceivedOCMSharesToDriveItems(ctx interface{}, receivedOCMShares interface{}) *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call {
return &BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call{Call: _e.mock.On("CS3ReceivedOCMSharesToDriveItems", ctx, receivedOCMShares)}
}
func (_c *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call) Run(run func(ctx context.Context, receivedOCMShares []*ocmv1beta1.ReceivedShare)) *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 []*ocmv1beta1.ReceivedShare
if args[1] != nil {
arg1 = args[1].([]*ocmv1beta1.ReceivedShare)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call) Return(driveItems []libregraph.DriveItem, err error) *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call {
_c.Call.Return(driveItems, err)
return _c
}
func (_c *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call) RunAndReturn(run func(ctx context.Context, receivedOCMShares []*ocmv1beta1.ReceivedShare) ([]libregraph.DriveItem, error)) *BaseGraphProvider_CS3ReceivedOCMSharesToDriveItems_Call {
_c.Call.Return(run)
return _c
}
// CS3ReceivedSharesToDriveItems provides a mock function for the type BaseGraphProvider
func (_mock *BaseGraphProvider) CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaborationv1beta1.ReceivedShare) ([]libregraph.DriveItem, error) {
ret := _mock.Called(ctx, receivedShares)
if len(ret) == 0 {
panic("no return value specified for CS3ReceivedSharesToDriveItems")
}
var r0 []libregraph.DriveItem
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, []*collaborationv1beta1.ReceivedShare) ([]libregraph.DriveItem, error)); ok {
return returnFunc(ctx, receivedShares)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, []*collaborationv1beta1.ReceivedShare) []libregraph.DriveItem); ok {
r0 = returnFunc(ctx, receivedShares)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]libregraph.DriveItem)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, []*collaborationv1beta1.ReceivedShare) error); ok {
r1 = returnFunc(ctx, receivedShares)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CS3ReceivedSharesToDriveItems'
type BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call struct {
*mock.Call
}
// CS3ReceivedSharesToDriveItems is a helper method to define mock.On call
// - ctx context.Context
// - receivedShares []*collaborationv1beta1.ReceivedShare
func (_e *BaseGraphProvider_Expecter) CS3ReceivedSharesToDriveItems(ctx interface{}, receivedShares interface{}) *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call {
return &BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call{Call: _e.mock.On("CS3ReceivedSharesToDriveItems", ctx, receivedShares)}
}
func (_c *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call) Run(run func(ctx context.Context, receivedShares []*collaborationv1beta1.ReceivedShare)) *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 []*collaborationv1beta1.ReceivedShare
if args[1] != nil {
arg1 = args[1].([]*collaborationv1beta1.ReceivedShare)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call) Return(driveItems []libregraph.DriveItem, err error) *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call {
_c.Call.Return(driveItems, err)
return _c
}
func (_c *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call) RunAndReturn(run func(ctx context.Context, receivedShares []*collaborationv1beta1.ReceivedShare) ([]libregraph.DriveItem, error)) *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call {
_c.Call.Return(run)
return _c
}
@@ -0,0 +1,911 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/service/v0"
mock "github.com/stretchr/testify/mock"
)
// NewDriveItemPermissionsProvider creates a new instance of DriveItemPermissionsProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewDriveItemPermissionsProvider(t interface {
mock.TestingT
Cleanup(func())
}) *DriveItemPermissionsProvider {
mock := &DriveItemPermissionsProvider{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// DriveItemPermissionsProvider is an autogenerated mock type for the DriveItemPermissionsProvider type
type DriveItemPermissionsProvider struct {
mock.Mock
}
type DriveItemPermissionsProvider_Expecter struct {
mock *mock.Mock
}
func (_m *DriveItemPermissionsProvider) EXPECT() *DriveItemPermissionsProvider_Expecter {
return &DriveItemPermissionsProvider_Expecter{mock: &_m.Mock}
}
// CreateLink provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) CreateLink(ctx context.Context, driveItemID *providerv1beta1.ResourceId, createLink libregraph.DriveItemCreateLink) (libregraph.Permission, error) {
ret := _mock.Called(ctx, driveItemID, createLink)
if len(ret) == 0 {
panic("no return value specified for CreateLink")
}
var r0 libregraph.Permission
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemCreateLink) (libregraph.Permission, error)); ok {
return returnFunc(ctx, driveItemID, createLink)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemCreateLink) libregraph.Permission); ok {
r0 = returnFunc(ctx, driveItemID, createLink)
} else {
r0 = ret.Get(0).(libregraph.Permission)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemCreateLink) error); ok {
r1 = returnFunc(ctx, driveItemID, createLink)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_CreateLink_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateLink'
type DriveItemPermissionsProvider_CreateLink_Call struct {
*mock.Call
}
// CreateLink is a helper method to define mock.On call
// - ctx context.Context
// - driveItemID *providerv1beta1.ResourceId
// - createLink libregraph.DriveItemCreateLink
func (_e *DriveItemPermissionsProvider_Expecter) CreateLink(ctx interface{}, driveItemID interface{}, createLink interface{}) *DriveItemPermissionsProvider_CreateLink_Call {
return &DriveItemPermissionsProvider_CreateLink_Call{Call: _e.mock.On("CreateLink", ctx, driveItemID, createLink)}
}
func (_c *DriveItemPermissionsProvider_CreateLink_Call) Run(run func(ctx context.Context, driveItemID *providerv1beta1.ResourceId, createLink libregraph.DriveItemCreateLink)) *DriveItemPermissionsProvider_CreateLink_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 libregraph.DriveItemCreateLink
if args[2] != nil {
arg2 = args[2].(libregraph.DriveItemCreateLink)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_CreateLink_Call) Return(permission libregraph.Permission, err error) *DriveItemPermissionsProvider_CreateLink_Call {
_c.Call.Return(permission, err)
return _c
}
func (_c *DriveItemPermissionsProvider_CreateLink_Call) RunAndReturn(run func(ctx context.Context, driveItemID *providerv1beta1.ResourceId, createLink libregraph.DriveItemCreateLink) (libregraph.Permission, error)) *DriveItemPermissionsProvider_CreateLink_Call {
_c.Call.Return(run)
return _c
}
// CreateSpaceRootLink provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) CreateSpaceRootLink(ctx context.Context, driveID *providerv1beta1.ResourceId, createLink libregraph.DriveItemCreateLink) (libregraph.Permission, error) {
ret := _mock.Called(ctx, driveID, createLink)
if len(ret) == 0 {
panic("no return value specified for CreateSpaceRootLink")
}
var r0 libregraph.Permission
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemCreateLink) (libregraph.Permission, error)); ok {
return returnFunc(ctx, driveID, createLink)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemCreateLink) libregraph.Permission); ok {
r0 = returnFunc(ctx, driveID, createLink)
} else {
r0 = ret.Get(0).(libregraph.Permission)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemCreateLink) error); ok {
r1 = returnFunc(ctx, driveID, createLink)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_CreateSpaceRootLink_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateSpaceRootLink'
type DriveItemPermissionsProvider_CreateSpaceRootLink_Call struct {
*mock.Call
}
// CreateSpaceRootLink is a helper method to define mock.On call
// - ctx context.Context
// - driveID *providerv1beta1.ResourceId
// - createLink libregraph.DriveItemCreateLink
func (_e *DriveItemPermissionsProvider_Expecter) CreateSpaceRootLink(ctx interface{}, driveID interface{}, createLink interface{}) *DriveItemPermissionsProvider_CreateSpaceRootLink_Call {
return &DriveItemPermissionsProvider_CreateSpaceRootLink_Call{Call: _e.mock.On("CreateSpaceRootLink", ctx, driveID, createLink)}
}
func (_c *DriveItemPermissionsProvider_CreateSpaceRootLink_Call) Run(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, createLink libregraph.DriveItemCreateLink)) *DriveItemPermissionsProvider_CreateSpaceRootLink_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 libregraph.DriveItemCreateLink
if args[2] != nil {
arg2 = args[2].(libregraph.DriveItemCreateLink)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_CreateSpaceRootLink_Call) Return(permission libregraph.Permission, err error) *DriveItemPermissionsProvider_CreateSpaceRootLink_Call {
_c.Call.Return(permission, err)
return _c
}
func (_c *DriveItemPermissionsProvider_CreateSpaceRootLink_Call) RunAndReturn(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, createLink libregraph.DriveItemCreateLink) (libregraph.Permission, error)) *DriveItemPermissionsProvider_CreateSpaceRootLink_Call {
_c.Call.Return(run)
return _c
}
// DeletePermission provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) DeletePermission(ctx context.Context, itemID *providerv1beta1.ResourceId, permissionID string) error {
ret := _mock.Called(ctx, itemID, permissionID)
if len(ret) == 0 {
panic("no return value specified for DeletePermission")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string) error); ok {
r0 = returnFunc(ctx, itemID, permissionID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DriveItemPermissionsProvider_DeletePermission_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePermission'
type DriveItemPermissionsProvider_DeletePermission_Call struct {
*mock.Call
}
// DeletePermission is a helper method to define mock.On call
// - ctx context.Context
// - itemID *providerv1beta1.ResourceId
// - permissionID string
func (_e *DriveItemPermissionsProvider_Expecter) DeletePermission(ctx interface{}, itemID interface{}, permissionID interface{}) *DriveItemPermissionsProvider_DeletePermission_Call {
return &DriveItemPermissionsProvider_DeletePermission_Call{Call: _e.mock.On("DeletePermission", ctx, itemID, permissionID)}
}
func (_c *DriveItemPermissionsProvider_DeletePermission_Call) Run(run func(ctx context.Context, itemID *providerv1beta1.ResourceId, permissionID string)) *DriveItemPermissionsProvider_DeletePermission_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_DeletePermission_Call) Return(err error) *DriveItemPermissionsProvider_DeletePermission_Call {
_c.Call.Return(err)
return _c
}
func (_c *DriveItemPermissionsProvider_DeletePermission_Call) RunAndReturn(run func(ctx context.Context, itemID *providerv1beta1.ResourceId, permissionID string) error) *DriveItemPermissionsProvider_DeletePermission_Call {
_c.Call.Return(run)
return _c
}
// DeleteSpaceRootPermission provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) DeleteSpaceRootPermission(ctx context.Context, driveID *providerv1beta1.ResourceId, permissionID string) error {
ret := _mock.Called(ctx, driveID, permissionID)
if len(ret) == 0 {
panic("no return value specified for DeleteSpaceRootPermission")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string) error); ok {
r0 = returnFunc(ctx, driveID, permissionID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DriveItemPermissionsProvider_DeleteSpaceRootPermission_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteSpaceRootPermission'
type DriveItemPermissionsProvider_DeleteSpaceRootPermission_Call struct {
*mock.Call
}
// DeleteSpaceRootPermission is a helper method to define mock.On call
// - ctx context.Context
// - driveID *providerv1beta1.ResourceId
// - permissionID string
func (_e *DriveItemPermissionsProvider_Expecter) DeleteSpaceRootPermission(ctx interface{}, driveID interface{}, permissionID interface{}) *DriveItemPermissionsProvider_DeleteSpaceRootPermission_Call {
return &DriveItemPermissionsProvider_DeleteSpaceRootPermission_Call{Call: _e.mock.On("DeleteSpaceRootPermission", ctx, driveID, permissionID)}
}
func (_c *DriveItemPermissionsProvider_DeleteSpaceRootPermission_Call) Run(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, permissionID string)) *DriveItemPermissionsProvider_DeleteSpaceRootPermission_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_DeleteSpaceRootPermission_Call) Return(err error) *DriveItemPermissionsProvider_DeleteSpaceRootPermission_Call {
_c.Call.Return(err)
return _c
}
func (_c *DriveItemPermissionsProvider_DeleteSpaceRootPermission_Call) RunAndReturn(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, permissionID string) error) *DriveItemPermissionsProvider_DeleteSpaceRootPermission_Call {
_c.Call.Return(run)
return _c
}
// Invite provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) Invite(ctx context.Context, resourceId *providerv1beta1.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) {
ret := _mock.Called(ctx, resourceId, invite)
if len(ret) == 0 {
panic("no return value specified for Invite")
}
var r0 libregraph.Permission
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemInvite) (libregraph.Permission, error)); ok {
return returnFunc(ctx, resourceId, invite)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemInvite) libregraph.Permission); ok {
r0 = returnFunc(ctx, resourceId, invite)
} else {
r0 = ret.Get(0).(libregraph.Permission)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemInvite) error); ok {
r1 = returnFunc(ctx, resourceId, invite)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_Invite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Invite'
type DriveItemPermissionsProvider_Invite_Call struct {
*mock.Call
}
// Invite is a helper method to define mock.On call
// - ctx context.Context
// - resourceId *providerv1beta1.ResourceId
// - invite libregraph.DriveItemInvite
func (_e *DriveItemPermissionsProvider_Expecter) Invite(ctx interface{}, resourceId interface{}, invite interface{}) *DriveItemPermissionsProvider_Invite_Call {
return &DriveItemPermissionsProvider_Invite_Call{Call: _e.mock.On("Invite", ctx, resourceId, invite)}
}
func (_c *DriveItemPermissionsProvider_Invite_Call) Run(run func(ctx context.Context, resourceId *providerv1beta1.ResourceId, invite libregraph.DriveItemInvite)) *DriveItemPermissionsProvider_Invite_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 libregraph.DriveItemInvite
if args[2] != nil {
arg2 = args[2].(libregraph.DriveItemInvite)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_Invite_Call) Return(permission libregraph.Permission, err error) *DriveItemPermissionsProvider_Invite_Call {
_c.Call.Return(permission, err)
return _c
}
func (_c *DriveItemPermissionsProvider_Invite_Call) RunAndReturn(run func(ctx context.Context, resourceId *providerv1beta1.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error)) *DriveItemPermissionsProvider_Invite_Call {
_c.Call.Return(run)
return _c
}
// ListPermissions provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) ListPermissions(ctx context.Context, itemID *providerv1beta1.ResourceId, queryOptions svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
ret := _mock.Called(ctx, itemID, queryOptions)
if len(ret) == 0 {
panic("no return value specified for ListPermissions")
}
var r0 libregraph.CollectionOfPermissionsWithAllowedValues
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)); ok {
return returnFunc(ctx, itemID, queryOptions)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) libregraph.CollectionOfPermissionsWithAllowedValues); ok {
r0 = returnFunc(ctx, itemID, queryOptions)
} else {
r0 = ret.Get(0).(libregraph.CollectionOfPermissionsWithAllowedValues)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) error); ok {
r1 = returnFunc(ctx, itemID, queryOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_ListPermissions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPermissions'
type DriveItemPermissionsProvider_ListPermissions_Call struct {
*mock.Call
}
// ListPermissions is a helper method to define mock.On call
// - ctx context.Context
// - itemID *providerv1beta1.ResourceId
// - queryOptions svc.ListPermissionsQueryOptions
func (_e *DriveItemPermissionsProvider_Expecter) ListPermissions(ctx interface{}, itemID interface{}, queryOptions interface{}) *DriveItemPermissionsProvider_ListPermissions_Call {
return &DriveItemPermissionsProvider_ListPermissions_Call{Call: _e.mock.On("ListPermissions", ctx, itemID, queryOptions)}
}
func (_c *DriveItemPermissionsProvider_ListPermissions_Call) Run(run func(ctx context.Context, itemID *providerv1beta1.ResourceId, queryOptions svc.ListPermissionsQueryOptions)) *DriveItemPermissionsProvider_ListPermissions_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 svc.ListPermissionsQueryOptions
if args[2] != nil {
arg2 = args[2].(svc.ListPermissionsQueryOptions)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_ListPermissions_Call) Return(collectionOfPermissionsWithAllowedValues libregraph.CollectionOfPermissionsWithAllowedValues, err error) *DriveItemPermissionsProvider_ListPermissions_Call {
_c.Call.Return(collectionOfPermissionsWithAllowedValues, err)
return _c
}
func (_c *DriveItemPermissionsProvider_ListPermissions_Call) RunAndReturn(run func(ctx context.Context, itemID *providerv1beta1.ResourceId, queryOptions svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)) *DriveItemPermissionsProvider_ListPermissions_Call {
_c.Call.Return(run)
return _c
}
// ListSpaceRootPermissions provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) ListSpaceRootPermissions(ctx context.Context, driveID *providerv1beta1.ResourceId, queryOptions svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
ret := _mock.Called(ctx, driveID, queryOptions)
if len(ret) == 0 {
panic("no return value specified for ListSpaceRootPermissions")
}
var r0 libregraph.CollectionOfPermissionsWithAllowedValues
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)); ok {
return returnFunc(ctx, driveID, queryOptions)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) libregraph.CollectionOfPermissionsWithAllowedValues); ok {
r0 = returnFunc(ctx, driveID, queryOptions)
} else {
r0 = ret.Get(0).(libregraph.CollectionOfPermissionsWithAllowedValues)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, svc.ListPermissionsQueryOptions) error); ok {
r1 = returnFunc(ctx, driveID, queryOptions)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_ListSpaceRootPermissions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListSpaceRootPermissions'
type DriveItemPermissionsProvider_ListSpaceRootPermissions_Call struct {
*mock.Call
}
// ListSpaceRootPermissions is a helper method to define mock.On call
// - ctx context.Context
// - driveID *providerv1beta1.ResourceId
// - queryOptions svc.ListPermissionsQueryOptions
func (_e *DriveItemPermissionsProvider_Expecter) ListSpaceRootPermissions(ctx interface{}, driveID interface{}, queryOptions interface{}) *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call {
return &DriveItemPermissionsProvider_ListSpaceRootPermissions_Call{Call: _e.mock.On("ListSpaceRootPermissions", ctx, driveID, queryOptions)}
}
func (_c *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call) Run(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, queryOptions svc.ListPermissionsQueryOptions)) *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 svc.ListPermissionsQueryOptions
if args[2] != nil {
arg2 = args[2].(svc.ListPermissionsQueryOptions)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call) Return(collectionOfPermissionsWithAllowedValues libregraph.CollectionOfPermissionsWithAllowedValues, err error) *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call {
_c.Call.Return(collectionOfPermissionsWithAllowedValues, err)
return _c
}
func (_c *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call) RunAndReturn(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, queryOptions svc.ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)) *DriveItemPermissionsProvider_ListSpaceRootPermissions_Call {
_c.Call.Return(run)
return _c
}
// SetPublicLinkPassword provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) SetPublicLinkPassword(ctx context.Context, driveItemID *providerv1beta1.ResourceId, permissionID string, password string) (libregraph.Permission, error) {
ret := _mock.Called(ctx, driveItemID, permissionID, password)
if len(ret) == 0 {
panic("no return value specified for SetPublicLinkPassword")
}
var r0 libregraph.Permission
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string, string) (libregraph.Permission, error)); ok {
return returnFunc(ctx, driveItemID, permissionID, password)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string, string) libregraph.Permission); ok {
r0 = returnFunc(ctx, driveItemID, permissionID, password)
} else {
r0 = ret.Get(0).(libregraph.Permission)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, string, string) error); ok {
r1 = returnFunc(ctx, driveItemID, permissionID, password)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_SetPublicLinkPassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPublicLinkPassword'
type DriveItemPermissionsProvider_SetPublicLinkPassword_Call struct {
*mock.Call
}
// SetPublicLinkPassword is a helper method to define mock.On call
// - ctx context.Context
// - driveItemID *providerv1beta1.ResourceId
// - permissionID string
// - password string
func (_e *DriveItemPermissionsProvider_Expecter) SetPublicLinkPassword(ctx interface{}, driveItemID interface{}, permissionID interface{}, password interface{}) *DriveItemPermissionsProvider_SetPublicLinkPassword_Call {
return &DriveItemPermissionsProvider_SetPublicLinkPassword_Call{Call: _e.mock.On("SetPublicLinkPassword", ctx, driveItemID, permissionID, password)}
}
func (_c *DriveItemPermissionsProvider_SetPublicLinkPassword_Call) Run(run func(ctx context.Context, driveItemID *providerv1beta1.ResourceId, permissionID string, password string)) *DriveItemPermissionsProvider_SetPublicLinkPassword_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_SetPublicLinkPassword_Call) Return(permission libregraph.Permission, err error) *DriveItemPermissionsProvider_SetPublicLinkPassword_Call {
_c.Call.Return(permission, err)
return _c
}
func (_c *DriveItemPermissionsProvider_SetPublicLinkPassword_Call) RunAndReturn(run func(ctx context.Context, driveItemID *providerv1beta1.ResourceId, permissionID string, password string) (libregraph.Permission, error)) *DriveItemPermissionsProvider_SetPublicLinkPassword_Call {
_c.Call.Return(run)
return _c
}
// SetPublicLinkPasswordOnSpaceRoot provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) SetPublicLinkPasswordOnSpaceRoot(ctx context.Context, driveID *providerv1beta1.ResourceId, permissionID string, password string) (libregraph.Permission, error) {
ret := _mock.Called(ctx, driveID, permissionID, password)
if len(ret) == 0 {
panic("no return value specified for SetPublicLinkPasswordOnSpaceRoot")
}
var r0 libregraph.Permission
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string, string) (libregraph.Permission, error)); ok {
return returnFunc(ctx, driveID, permissionID, password)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string, string) libregraph.Permission); ok {
r0 = returnFunc(ctx, driveID, permissionID, password)
} else {
r0 = ret.Get(0).(libregraph.Permission)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, string, string) error); ok {
r1 = returnFunc(ctx, driveID, permissionID, password)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_SetPublicLinkPasswordOnSpaceRoot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetPublicLinkPasswordOnSpaceRoot'
type DriveItemPermissionsProvider_SetPublicLinkPasswordOnSpaceRoot_Call struct {
*mock.Call
}
// SetPublicLinkPasswordOnSpaceRoot is a helper method to define mock.On call
// - ctx context.Context
// - driveID *providerv1beta1.ResourceId
// - permissionID string
// - password string
func (_e *DriveItemPermissionsProvider_Expecter) SetPublicLinkPasswordOnSpaceRoot(ctx interface{}, driveID interface{}, permissionID interface{}, password interface{}) *DriveItemPermissionsProvider_SetPublicLinkPasswordOnSpaceRoot_Call {
return &DriveItemPermissionsProvider_SetPublicLinkPasswordOnSpaceRoot_Call{Call: _e.mock.On("SetPublicLinkPasswordOnSpaceRoot", ctx, driveID, permissionID, password)}
}
func (_c *DriveItemPermissionsProvider_SetPublicLinkPasswordOnSpaceRoot_Call) Run(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, permissionID string, password string)) *DriveItemPermissionsProvider_SetPublicLinkPasswordOnSpaceRoot_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_SetPublicLinkPasswordOnSpaceRoot_Call) Return(permission libregraph.Permission, err error) *DriveItemPermissionsProvider_SetPublicLinkPasswordOnSpaceRoot_Call {
_c.Call.Return(permission, err)
return _c
}
func (_c *DriveItemPermissionsProvider_SetPublicLinkPasswordOnSpaceRoot_Call) RunAndReturn(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, permissionID string, password string) (libregraph.Permission, error)) *DriveItemPermissionsProvider_SetPublicLinkPasswordOnSpaceRoot_Call {
_c.Call.Return(run)
return _c
}
// SpaceRootInvite provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) SpaceRootInvite(ctx context.Context, driveID *providerv1beta1.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) {
ret := _mock.Called(ctx, driveID, invite)
if len(ret) == 0 {
panic("no return value specified for SpaceRootInvite")
}
var r0 libregraph.Permission
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemInvite) (libregraph.Permission, error)); ok {
return returnFunc(ctx, driveID, invite)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemInvite) libregraph.Permission); ok {
r0 = returnFunc(ctx, driveID, invite)
} else {
r0 = ret.Get(0).(libregraph.Permission)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, libregraph.DriveItemInvite) error); ok {
r1 = returnFunc(ctx, driveID, invite)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_SpaceRootInvite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SpaceRootInvite'
type DriveItemPermissionsProvider_SpaceRootInvite_Call struct {
*mock.Call
}
// SpaceRootInvite is a helper method to define mock.On call
// - ctx context.Context
// - driveID *providerv1beta1.ResourceId
// - invite libregraph.DriveItemInvite
func (_e *DriveItemPermissionsProvider_Expecter) SpaceRootInvite(ctx interface{}, driveID interface{}, invite interface{}) *DriveItemPermissionsProvider_SpaceRootInvite_Call {
return &DriveItemPermissionsProvider_SpaceRootInvite_Call{Call: _e.mock.On("SpaceRootInvite", ctx, driveID, invite)}
}
func (_c *DriveItemPermissionsProvider_SpaceRootInvite_Call) Run(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, invite libregraph.DriveItemInvite)) *DriveItemPermissionsProvider_SpaceRootInvite_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 libregraph.DriveItemInvite
if args[2] != nil {
arg2 = args[2].(libregraph.DriveItemInvite)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_SpaceRootInvite_Call) Return(permission libregraph.Permission, err error) *DriveItemPermissionsProvider_SpaceRootInvite_Call {
_c.Call.Return(permission, err)
return _c
}
func (_c *DriveItemPermissionsProvider_SpaceRootInvite_Call) RunAndReturn(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error)) *DriveItemPermissionsProvider_SpaceRootInvite_Call {
_c.Call.Return(run)
return _c
}
// UpdatePermission provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) UpdatePermission(ctx context.Context, itemID *providerv1beta1.ResourceId, permissionID string, newPermission libregraph.Permission) (libregraph.Permission, error) {
ret := _mock.Called(ctx, itemID, permissionID, newPermission)
if len(ret) == 0 {
panic("no return value specified for UpdatePermission")
}
var r0 libregraph.Permission
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string, libregraph.Permission) (libregraph.Permission, error)); ok {
return returnFunc(ctx, itemID, permissionID, newPermission)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string, libregraph.Permission) libregraph.Permission); ok {
r0 = returnFunc(ctx, itemID, permissionID, newPermission)
} else {
r0 = ret.Get(0).(libregraph.Permission)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, string, libregraph.Permission) error); ok {
r1 = returnFunc(ctx, itemID, permissionID, newPermission)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_UpdatePermission_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdatePermission'
type DriveItemPermissionsProvider_UpdatePermission_Call struct {
*mock.Call
}
// UpdatePermission is a helper method to define mock.On call
// - ctx context.Context
// - itemID *providerv1beta1.ResourceId
// - permissionID string
// - newPermission libregraph.Permission
func (_e *DriveItemPermissionsProvider_Expecter) UpdatePermission(ctx interface{}, itemID interface{}, permissionID interface{}, newPermission interface{}) *DriveItemPermissionsProvider_UpdatePermission_Call {
return &DriveItemPermissionsProvider_UpdatePermission_Call{Call: _e.mock.On("UpdatePermission", ctx, itemID, permissionID, newPermission)}
}
func (_c *DriveItemPermissionsProvider_UpdatePermission_Call) Run(run func(ctx context.Context, itemID *providerv1beta1.ResourceId, permissionID string, newPermission libregraph.Permission)) *DriveItemPermissionsProvider_UpdatePermission_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 libregraph.Permission
if args[3] != nil {
arg3 = args[3].(libregraph.Permission)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_UpdatePermission_Call) Return(permission libregraph.Permission, err error) *DriveItemPermissionsProvider_UpdatePermission_Call {
_c.Call.Return(permission, err)
return _c
}
func (_c *DriveItemPermissionsProvider_UpdatePermission_Call) RunAndReturn(run func(ctx context.Context, itemID *providerv1beta1.ResourceId, permissionID string, newPermission libregraph.Permission) (libregraph.Permission, error)) *DriveItemPermissionsProvider_UpdatePermission_Call {
_c.Call.Return(run)
return _c
}
// UpdateSpaceRootPermission provides a mock function for the type DriveItemPermissionsProvider
func (_mock *DriveItemPermissionsProvider) UpdateSpaceRootPermission(ctx context.Context, driveID *providerv1beta1.ResourceId, permissionID string, newPermission libregraph.Permission) (libregraph.Permission, error) {
ret := _mock.Called(ctx, driveID, permissionID, newPermission)
if len(ret) == 0 {
panic("no return value specified for UpdateSpaceRootPermission")
}
var r0 libregraph.Permission
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string, libregraph.Permission) (libregraph.Permission, error)); ok {
return returnFunc(ctx, driveID, permissionID, newPermission)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string, libregraph.Permission) libregraph.Permission); ok {
r0 = returnFunc(ctx, driveID, permissionID, newPermission)
} else {
r0 = ret.Get(0).(libregraph.Permission)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, string, libregraph.Permission) error); ok {
r1 = returnFunc(ctx, driveID, permissionID, newPermission)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DriveItemPermissionsProvider_UpdateSpaceRootPermission_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateSpaceRootPermission'
type DriveItemPermissionsProvider_UpdateSpaceRootPermission_Call struct {
*mock.Call
}
// UpdateSpaceRootPermission is a helper method to define mock.On call
// - ctx context.Context
// - driveID *providerv1beta1.ResourceId
// - permissionID string
// - newPermission libregraph.Permission
func (_e *DriveItemPermissionsProvider_Expecter) UpdateSpaceRootPermission(ctx interface{}, driveID interface{}, permissionID interface{}, newPermission interface{}) *DriveItemPermissionsProvider_UpdateSpaceRootPermission_Call {
return &DriveItemPermissionsProvider_UpdateSpaceRootPermission_Call{Call: _e.mock.On("UpdateSpaceRootPermission", ctx, driveID, permissionID, newPermission)}
}
func (_c *DriveItemPermissionsProvider_UpdateSpaceRootPermission_Call) Run(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, permissionID string, newPermission libregraph.Permission)) *DriveItemPermissionsProvider_UpdateSpaceRootPermission_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 libregraph.Permission
if args[3] != nil {
arg3 = args[3].(libregraph.Permission)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *DriveItemPermissionsProvider_UpdateSpaceRootPermission_Call) Return(permission libregraph.Permission, err error) *DriveItemPermissionsProvider_UpdateSpaceRootPermission_Call {
_c.Call.Return(permission, err)
return _c
}
func (_c *DriveItemPermissionsProvider_UpdateSpaceRootPermission_Call) RunAndReturn(run func(ctx context.Context, driveID *providerv1beta1.ResourceId, permissionID string, newPermission libregraph.Permission) (libregraph.Permission, error)) *DriveItemPermissionsProvider_UpdateSpaceRootPermission_Call {
_c.Call.Return(run)
return _c
}
@@ -0,0 +1,457 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
"github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
"github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/qsfera/server/services/graph/pkg/service/v0"
mock "github.com/stretchr/testify/mock"
)
// NewDrivesDriveItemProvider creates a new instance of DrivesDriveItemProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewDrivesDriveItemProvider(t interface {
mock.TestingT
Cleanup(func())
}) *DrivesDriveItemProvider {
mock := &DrivesDriveItemProvider{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// DrivesDriveItemProvider is an autogenerated mock type for the DrivesDriveItemProvider type
type DrivesDriveItemProvider struct {
mock.Mock
}
type DrivesDriveItemProvider_Expecter struct {
mock *mock.Mock
}
func (_m *DrivesDriveItemProvider) EXPECT() *DrivesDriveItemProvider_Expecter {
return &DrivesDriveItemProvider_Expecter{mock: &_m.Mock}
}
// GetShare provides a mock function for the type DrivesDriveItemProvider
func (_mock *DrivesDriveItemProvider) GetShare(ctx context.Context, shareID *collaborationv1beta1.ShareId) (*collaborationv1beta1.ReceivedShare, error) {
ret := _mock.Called(ctx, shareID)
if len(ret) == 0 {
panic("no return value specified for GetShare")
}
var r0 *collaborationv1beta1.ReceivedShare
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.ShareId) (*collaborationv1beta1.ReceivedShare, error)); ok {
return returnFunc(ctx, shareID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.ShareId) *collaborationv1beta1.ReceivedShare); ok {
r0 = returnFunc(ctx, shareID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*collaborationv1beta1.ReceivedShare)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *collaborationv1beta1.ShareId) error); ok {
r1 = returnFunc(ctx, shareID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DrivesDriveItemProvider_GetShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetShare'
type DrivesDriveItemProvider_GetShare_Call struct {
*mock.Call
}
// GetShare is a helper method to define mock.On call
// - ctx context.Context
// - shareID *collaborationv1beta1.ShareId
func (_e *DrivesDriveItemProvider_Expecter) GetShare(ctx interface{}, shareID interface{}) *DrivesDriveItemProvider_GetShare_Call {
return &DrivesDriveItemProvider_GetShare_Call{Call: _e.mock.On("GetShare", ctx, shareID)}
}
func (_c *DrivesDriveItemProvider_GetShare_Call) Run(run func(ctx context.Context, shareID *collaborationv1beta1.ShareId)) *DrivesDriveItemProvider_GetShare_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *collaborationv1beta1.ShareId
if args[1] != nil {
arg1 = args[1].(*collaborationv1beta1.ShareId)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *DrivesDriveItemProvider_GetShare_Call) Return(receivedShare *collaborationv1beta1.ReceivedShare, err error) *DrivesDriveItemProvider_GetShare_Call {
_c.Call.Return(receivedShare, err)
return _c
}
func (_c *DrivesDriveItemProvider_GetShare_Call) RunAndReturn(run func(ctx context.Context, shareID *collaborationv1beta1.ShareId) (*collaborationv1beta1.ReceivedShare, error)) *DrivesDriveItemProvider_GetShare_Call {
_c.Call.Return(run)
return _c
}
// GetSharesForResource provides a mock function for the type DrivesDriveItemProvider
func (_mock *DrivesDriveItemProvider) GetSharesForResource(ctx context.Context, resourceID *providerv1beta1.ResourceId, filters []*collaborationv1beta1.Filter) ([]*collaborationv1beta1.ReceivedShare, error) {
ret := _mock.Called(ctx, resourceID, filters)
if len(ret) == 0 {
panic("no return value specified for GetSharesForResource")
}
var r0 []*collaborationv1beta1.ReceivedShare
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, []*collaborationv1beta1.Filter) ([]*collaborationv1beta1.ReceivedShare, error)); ok {
return returnFunc(ctx, resourceID, filters)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, []*collaborationv1beta1.Filter) []*collaborationv1beta1.ReceivedShare); ok {
r0 = returnFunc(ctx, resourceID, filters)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*collaborationv1beta1.ReceivedShare)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, []*collaborationv1beta1.Filter) error); ok {
r1 = returnFunc(ctx, resourceID, filters)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DrivesDriveItemProvider_GetSharesForResource_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSharesForResource'
type DrivesDriveItemProvider_GetSharesForResource_Call struct {
*mock.Call
}
// GetSharesForResource is a helper method to define mock.On call
// - ctx context.Context
// - resourceID *providerv1beta1.ResourceId
// - filters []*collaborationv1beta1.Filter
func (_e *DrivesDriveItemProvider_Expecter) GetSharesForResource(ctx interface{}, resourceID interface{}, filters interface{}) *DrivesDriveItemProvider_GetSharesForResource_Call {
return &DrivesDriveItemProvider_GetSharesForResource_Call{Call: _e.mock.On("GetSharesForResource", ctx, resourceID, filters)}
}
func (_c *DrivesDriveItemProvider_GetSharesForResource_Call) Run(run func(ctx context.Context, resourceID *providerv1beta1.ResourceId, filters []*collaborationv1beta1.Filter)) *DrivesDriveItemProvider_GetSharesForResource_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 []*collaborationv1beta1.Filter
if args[2] != nil {
arg2 = args[2].([]*collaborationv1beta1.Filter)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DrivesDriveItemProvider_GetSharesForResource_Call) Return(receivedShares []*collaborationv1beta1.ReceivedShare, err error) *DrivesDriveItemProvider_GetSharesForResource_Call {
_c.Call.Return(receivedShares, err)
return _c
}
func (_c *DrivesDriveItemProvider_GetSharesForResource_Call) RunAndReturn(run func(ctx context.Context, resourceID *providerv1beta1.ResourceId, filters []*collaborationv1beta1.Filter) ([]*collaborationv1beta1.ReceivedShare, error)) *DrivesDriveItemProvider_GetSharesForResource_Call {
_c.Call.Return(run)
return _c
}
// MountOCMShare provides a mock function for the type DrivesDriveItemProvider
func (_mock *DrivesDriveItemProvider) MountOCMShare(ctx context.Context, resourceID *providerv1beta1.ResourceId) ([]*ocmv1beta1.ReceivedShare, error) {
ret := _mock.Called(ctx, resourceID)
if len(ret) == 0 {
panic("no return value specified for MountOCMShare")
}
var r0 []*ocmv1beta1.ReceivedShare
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId) ([]*ocmv1beta1.ReceivedShare, error)); ok {
return returnFunc(ctx, resourceID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId) []*ocmv1beta1.ReceivedShare); ok {
r0 = returnFunc(ctx, resourceID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*ocmv1beta1.ReceivedShare)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId) error); ok {
r1 = returnFunc(ctx, resourceID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DrivesDriveItemProvider_MountOCMShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MountOCMShare'
type DrivesDriveItemProvider_MountOCMShare_Call struct {
*mock.Call
}
// MountOCMShare is a helper method to define mock.On call
// - ctx context.Context
// - resourceID *providerv1beta1.ResourceId
func (_e *DrivesDriveItemProvider_Expecter) MountOCMShare(ctx interface{}, resourceID interface{}) *DrivesDriveItemProvider_MountOCMShare_Call {
return &DrivesDriveItemProvider_MountOCMShare_Call{Call: _e.mock.On("MountOCMShare", ctx, resourceID)}
}
func (_c *DrivesDriveItemProvider_MountOCMShare_Call) Run(run func(ctx context.Context, resourceID *providerv1beta1.ResourceId)) *DrivesDriveItemProvider_MountOCMShare_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *DrivesDriveItemProvider_MountOCMShare_Call) Return(receivedShares []*ocmv1beta1.ReceivedShare, err error) *DrivesDriveItemProvider_MountOCMShare_Call {
_c.Call.Return(receivedShares, err)
return _c
}
func (_c *DrivesDriveItemProvider_MountOCMShare_Call) RunAndReturn(run func(ctx context.Context, resourceID *providerv1beta1.ResourceId) ([]*ocmv1beta1.ReceivedShare, error)) *DrivesDriveItemProvider_MountOCMShare_Call {
_c.Call.Return(run)
return _c
}
// MountShare provides a mock function for the type DrivesDriveItemProvider
func (_mock *DrivesDriveItemProvider) MountShare(ctx context.Context, resourceID *providerv1beta1.ResourceId, name string) ([]*collaborationv1beta1.ReceivedShare, error) {
ret := _mock.Called(ctx, resourceID, name)
if len(ret) == 0 {
panic("no return value specified for MountShare")
}
var r0 []*collaborationv1beta1.ReceivedShare
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string) ([]*collaborationv1beta1.ReceivedShare, error)); ok {
return returnFunc(ctx, resourceID, name)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string) []*collaborationv1beta1.ReceivedShare); ok {
r0 = returnFunc(ctx, resourceID, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*collaborationv1beta1.ReceivedShare)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, string) error); ok {
r1 = returnFunc(ctx, resourceID, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DrivesDriveItemProvider_MountShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MountShare'
type DrivesDriveItemProvider_MountShare_Call struct {
*mock.Call
}
// MountShare is a helper method to define mock.On call
// - ctx context.Context
// - resourceID *providerv1beta1.ResourceId
// - name string
func (_e *DrivesDriveItemProvider_Expecter) MountShare(ctx interface{}, resourceID interface{}, name interface{}) *DrivesDriveItemProvider_MountShare_Call {
return &DrivesDriveItemProvider_MountShare_Call{Call: _e.mock.On("MountShare", ctx, resourceID, name)}
}
func (_c *DrivesDriveItemProvider_MountShare_Call) Run(run func(ctx context.Context, resourceID *providerv1beta1.ResourceId, name string)) *DrivesDriveItemProvider_MountShare_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *providerv1beta1.ResourceId
if args[1] != nil {
arg1 = args[1].(*providerv1beta1.ResourceId)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DrivesDriveItemProvider_MountShare_Call) Return(receivedShares []*collaborationv1beta1.ReceivedShare, err error) *DrivesDriveItemProvider_MountShare_Call {
_c.Call.Return(receivedShares, err)
return _c
}
func (_c *DrivesDriveItemProvider_MountShare_Call) RunAndReturn(run func(ctx context.Context, resourceID *providerv1beta1.ResourceId, name string) ([]*collaborationv1beta1.ReceivedShare, error)) *DrivesDriveItemProvider_MountShare_Call {
_c.Call.Return(run)
return _c
}
// UnmountShare provides a mock function for the type DrivesDriveItemProvider
func (_mock *DrivesDriveItemProvider) UnmountShare(ctx context.Context, shareID *collaborationv1beta1.ShareId) error {
ret := _mock.Called(ctx, shareID)
if len(ret) == 0 {
panic("no return value specified for UnmountShare")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.ShareId) error); ok {
r0 = returnFunc(ctx, shareID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DrivesDriveItemProvider_UnmountShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UnmountShare'
type DrivesDriveItemProvider_UnmountShare_Call struct {
*mock.Call
}
// UnmountShare is a helper method to define mock.On call
// - ctx context.Context
// - shareID *collaborationv1beta1.ShareId
func (_e *DrivesDriveItemProvider_Expecter) UnmountShare(ctx interface{}, shareID interface{}) *DrivesDriveItemProvider_UnmountShare_Call {
return &DrivesDriveItemProvider_UnmountShare_Call{Call: _e.mock.On("UnmountShare", ctx, shareID)}
}
func (_c *DrivesDriveItemProvider_UnmountShare_Call) Run(run func(ctx context.Context, shareID *collaborationv1beta1.ShareId)) *DrivesDriveItemProvider_UnmountShare_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *collaborationv1beta1.ShareId
if args[1] != nil {
arg1 = args[1].(*collaborationv1beta1.ShareId)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *DrivesDriveItemProvider_UnmountShare_Call) Return(err error) *DrivesDriveItemProvider_UnmountShare_Call {
_c.Call.Return(err)
return _c
}
func (_c *DrivesDriveItemProvider_UnmountShare_Call) RunAndReturn(run func(ctx context.Context, shareID *collaborationv1beta1.ShareId) error) *DrivesDriveItemProvider_UnmountShare_Call {
_c.Call.Return(run)
return _c
}
// UpdateShares provides a mock function for the type DrivesDriveItemProvider
func (_mock *DrivesDriveItemProvider) UpdateShares(ctx context.Context, shares []*collaborationv1beta1.ReceivedShare, updater svc.UpdateShareClosure) ([]*collaborationv1beta1.ReceivedShare, error) {
ret := _mock.Called(ctx, shares, updater)
if len(ret) == 0 {
panic("no return value specified for UpdateShares")
}
var r0 []*collaborationv1beta1.ReceivedShare
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, []*collaborationv1beta1.ReceivedShare, svc.UpdateShareClosure) ([]*collaborationv1beta1.ReceivedShare, error)); ok {
return returnFunc(ctx, shares, updater)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, []*collaborationv1beta1.ReceivedShare, svc.UpdateShareClosure) []*collaborationv1beta1.ReceivedShare); ok {
r0 = returnFunc(ctx, shares, updater)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*collaborationv1beta1.ReceivedShare)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, []*collaborationv1beta1.ReceivedShare, svc.UpdateShareClosure) error); ok {
r1 = returnFunc(ctx, shares, updater)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DrivesDriveItemProvider_UpdateShares_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateShares'
type DrivesDriveItemProvider_UpdateShares_Call struct {
*mock.Call
}
// UpdateShares is a helper method to define mock.On call
// - ctx context.Context
// - shares []*collaborationv1beta1.ReceivedShare
// - updater svc.UpdateShareClosure
func (_e *DrivesDriveItemProvider_Expecter) UpdateShares(ctx interface{}, shares interface{}, updater interface{}) *DrivesDriveItemProvider_UpdateShares_Call {
return &DrivesDriveItemProvider_UpdateShares_Call{Call: _e.mock.On("UpdateShares", ctx, shares, updater)}
}
func (_c *DrivesDriveItemProvider_UpdateShares_Call) Run(run func(ctx context.Context, shares []*collaborationv1beta1.ReceivedShare, updater svc.UpdateShareClosure)) *DrivesDriveItemProvider_UpdateShares_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 []*collaborationv1beta1.ReceivedShare
if args[1] != nil {
arg1 = args[1].([]*collaborationv1beta1.ReceivedShare)
}
var arg2 svc.UpdateShareClosure
if args[2] != nil {
arg2 = args[2].(svc.UpdateShareClosure)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *DrivesDriveItemProvider_UpdateShares_Call) Return(receivedShares []*collaborationv1beta1.ReceivedShare, err error) *DrivesDriveItemProvider_UpdateShares_Call {
_c.Call.Return(receivedShares, err)
return _c
}
func (_c *DrivesDriveItemProvider_UpdateShares_Call) RunAndReturn(run func(ctx context.Context, shares []*collaborationv1beta1.ReceivedShare, updater svc.UpdateShareClosure) ([]*collaborationv1beta1.ReceivedShare, error)) *DrivesDriveItemProvider_UpdateShares_Call {
_c.Call.Return(run)
return _c
}
@@ -0,0 +1,108 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
mock "github.com/stretchr/testify/mock"
)
// NewSelectable creates a new instance of Selectable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewSelectable[T any](t interface {
mock.TestingT
Cleanup(func())
}) *Selectable[T] {
mock := &Selectable[T]{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Selectable is an autogenerated mock type for the Selectable type
type Selectable[T any] struct {
mock.Mock
}
type Selectable_Expecter[T any] struct {
mock *mock.Mock
}
func (_m *Selectable[T]) EXPECT() *Selectable_Expecter[T] {
return &Selectable_Expecter[T]{mock: &_m.Mock}
}
// Next provides a mock function for the type Selectable
func (_mock *Selectable[T]) Next(opts ...pool.Option) (T, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(opts)
} else {
tmpRet = _mock.Called()
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for Next")
}
var r0 T
var r1 error
if returnFunc, ok := ret.Get(0).(func(...pool.Option) (T, error)); ok {
return returnFunc(opts...)
}
if returnFunc, ok := ret.Get(0).(func(...pool.Option) T); ok {
r0 = returnFunc(opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(T)
}
}
if returnFunc, ok := ret.Get(1).(func(...pool.Option) error); ok {
r1 = returnFunc(opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Selectable_Next_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Next'
type Selectable_Next_Call[T any] struct {
*mock.Call
}
// Next is a helper method to define mock.On call
// - opts ...pool.Option
func (_e *Selectable_Expecter[T]) Next(opts ...interface{}) *Selectable_Next_Call[T] {
return &Selectable_Next_Call[T]{Call: _e.mock.On("Next",
append([]interface{}{}, opts...)...)}
}
func (_c *Selectable_Next_Call[T]) Run(run func(opts ...pool.Option)) *Selectable_Next_Call[T] {
_c.Call.Run(func(args mock.Arguments) {
var arg0 []pool.Option
var variadicArgs []pool.Option
if len(args) > 0 {
variadicArgs = args[0].([]pool.Option)
}
arg0 = variadicArgs
run(
arg0...,
)
})
return _c
}
func (_c *Selectable_Next_Call[T]) Return(v T, err error) *Selectable_Next_Call[T] {
_c.Call.Return(v, err)
return _c
}
func (_c *Selectable_Next_Call[T]) RunAndReturn(run func(opts ...pool.Option) (T, error)) *Selectable_Next_Call[T] {
_c.Call.Return(run)
return _c
}
+100
View File
@@ -0,0 +1,100 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"net/http"
mock "github.com/stretchr/testify/mock"
)
// NewHTTPClient creates a new instance of HTTPClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewHTTPClient(t interface {
mock.TestingT
Cleanup(func())
}) *HTTPClient {
mock := &HTTPClient{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// HTTPClient is an autogenerated mock type for the HTTPClient type
type HTTPClient struct {
mock.Mock
}
type HTTPClient_Expecter struct {
mock *mock.Mock
}
func (_m *HTTPClient) EXPECT() *HTTPClient_Expecter {
return &HTTPClient_Expecter{mock: &_m.Mock}
}
// Do provides a mock function for the type HTTPClient
func (_mock *HTTPClient) Do(req *http.Request) (*http.Response, error) {
ret := _mock.Called(req)
if len(ret) == 0 {
panic("no return value specified for Do")
}
var r0 *http.Response
var r1 error
if returnFunc, ok := ret.Get(0).(func(*http.Request) (*http.Response, error)); ok {
return returnFunc(req)
}
if returnFunc, ok := ret.Get(0).(func(*http.Request) *http.Response); ok {
r0 = returnFunc(req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*http.Response)
}
}
if returnFunc, ok := ret.Get(1).(func(*http.Request) error); ok {
r1 = returnFunc(req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HTTPClient_Do_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Do'
type HTTPClient_Do_Call struct {
*mock.Call
}
// Do is a helper method to define mock.On call
// - req *http.Request
func (_e *HTTPClient_Expecter) Do(req interface{}) *HTTPClient_Do_Call {
return &HTTPClient_Do_Call{Call: _e.mock.On("Do", req)}
}
func (_c *HTTPClient_Do_Call) Run(run func(req *http.Request)) *HTTPClient_Do_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *http.Request
if args[0] != nil {
arg0 = args[0].(*http.Request)
}
run(
arg0,
)
})
return _c
}
func (_c *HTTPClient_Do_Call) Return(response *http.Response, err error) *HTTPClient_Do_Call {
_c.Call.Return(response, err)
return _c
}
func (_c *HTTPClient_Do_Call) RunAndReturn(run func(req *http.Request) (*http.Response, error)) *HTTPClient_Do_Call {
_c.Call.Return(run)
return _c
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,349 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"time"
"github.com/nats-io/nats.go/jetstream"
mock "github.com/stretchr/testify/mock"
)
// NewKeyValueEntry creates a new instance of KeyValueEntry. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewKeyValueEntry(t interface {
mock.TestingT
Cleanup(func())
}) *KeyValueEntry {
mock := &KeyValueEntry{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// KeyValueEntry is an autogenerated mock type for the KeyValueEntry type
type KeyValueEntry struct {
mock.Mock
}
type KeyValueEntry_Expecter struct {
mock *mock.Mock
}
func (_m *KeyValueEntry) EXPECT() *KeyValueEntry_Expecter {
return &KeyValueEntry_Expecter{mock: &_m.Mock}
}
// Bucket provides a mock function for the type KeyValueEntry
func (_mock *KeyValueEntry) Bucket() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Bucket")
}
var r0 string
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// KeyValueEntry_Bucket_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Bucket'
type KeyValueEntry_Bucket_Call struct {
*mock.Call
}
// Bucket is a helper method to define mock.On call
func (_e *KeyValueEntry_Expecter) Bucket() *KeyValueEntry_Bucket_Call {
return &KeyValueEntry_Bucket_Call{Call: _e.mock.On("Bucket")}
}
func (_c *KeyValueEntry_Bucket_Call) Run(run func()) *KeyValueEntry_Bucket_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *KeyValueEntry_Bucket_Call) Return(s string) *KeyValueEntry_Bucket_Call {
_c.Call.Return(s)
return _c
}
func (_c *KeyValueEntry_Bucket_Call) RunAndReturn(run func() string) *KeyValueEntry_Bucket_Call {
_c.Call.Return(run)
return _c
}
// Created provides a mock function for the type KeyValueEntry
func (_mock *KeyValueEntry) Created() time.Time {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Created")
}
var r0 time.Time
if returnFunc, ok := ret.Get(0).(func() time.Time); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(time.Time)
}
return r0
}
// KeyValueEntry_Created_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Created'
type KeyValueEntry_Created_Call struct {
*mock.Call
}
// Created is a helper method to define mock.On call
func (_e *KeyValueEntry_Expecter) Created() *KeyValueEntry_Created_Call {
return &KeyValueEntry_Created_Call{Call: _e.mock.On("Created")}
}
func (_c *KeyValueEntry_Created_Call) Run(run func()) *KeyValueEntry_Created_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *KeyValueEntry_Created_Call) Return(time1 time.Time) *KeyValueEntry_Created_Call {
_c.Call.Return(time1)
return _c
}
func (_c *KeyValueEntry_Created_Call) RunAndReturn(run func() time.Time) *KeyValueEntry_Created_Call {
_c.Call.Return(run)
return _c
}
// Delta provides a mock function for the type KeyValueEntry
func (_mock *KeyValueEntry) Delta() uint64 {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Delta")
}
var r0 uint64
if returnFunc, ok := ret.Get(0).(func() uint64); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(uint64)
}
return r0
}
// KeyValueEntry_Delta_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delta'
type KeyValueEntry_Delta_Call struct {
*mock.Call
}
// Delta is a helper method to define mock.On call
func (_e *KeyValueEntry_Expecter) Delta() *KeyValueEntry_Delta_Call {
return &KeyValueEntry_Delta_Call{Call: _e.mock.On("Delta")}
}
func (_c *KeyValueEntry_Delta_Call) Run(run func()) *KeyValueEntry_Delta_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *KeyValueEntry_Delta_Call) Return(v uint64) *KeyValueEntry_Delta_Call {
_c.Call.Return(v)
return _c
}
func (_c *KeyValueEntry_Delta_Call) RunAndReturn(run func() uint64) *KeyValueEntry_Delta_Call {
_c.Call.Return(run)
return _c
}
// Key provides a mock function for the type KeyValueEntry
func (_mock *KeyValueEntry) Key() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Key")
}
var r0 string
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// KeyValueEntry_Key_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Key'
type KeyValueEntry_Key_Call struct {
*mock.Call
}
// Key is a helper method to define mock.On call
func (_e *KeyValueEntry_Expecter) Key() *KeyValueEntry_Key_Call {
return &KeyValueEntry_Key_Call{Call: _e.mock.On("Key")}
}
func (_c *KeyValueEntry_Key_Call) Run(run func()) *KeyValueEntry_Key_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *KeyValueEntry_Key_Call) Return(s string) *KeyValueEntry_Key_Call {
_c.Call.Return(s)
return _c
}
func (_c *KeyValueEntry_Key_Call) RunAndReturn(run func() string) *KeyValueEntry_Key_Call {
_c.Call.Return(run)
return _c
}
// Operation provides a mock function for the type KeyValueEntry
func (_mock *KeyValueEntry) Operation() jetstream.KeyValueOp {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Operation")
}
var r0 jetstream.KeyValueOp
if returnFunc, ok := ret.Get(0).(func() jetstream.KeyValueOp); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(jetstream.KeyValueOp)
}
return r0
}
// KeyValueEntry_Operation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Operation'
type KeyValueEntry_Operation_Call struct {
*mock.Call
}
// Operation is a helper method to define mock.On call
func (_e *KeyValueEntry_Expecter) Operation() *KeyValueEntry_Operation_Call {
return &KeyValueEntry_Operation_Call{Call: _e.mock.On("Operation")}
}
func (_c *KeyValueEntry_Operation_Call) Run(run func()) *KeyValueEntry_Operation_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *KeyValueEntry_Operation_Call) Return(keyValueOp jetstream.KeyValueOp) *KeyValueEntry_Operation_Call {
_c.Call.Return(keyValueOp)
return _c
}
func (_c *KeyValueEntry_Operation_Call) RunAndReturn(run func() jetstream.KeyValueOp) *KeyValueEntry_Operation_Call {
_c.Call.Return(run)
return _c
}
// Revision provides a mock function for the type KeyValueEntry
func (_mock *KeyValueEntry) Revision() uint64 {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Revision")
}
var r0 uint64
if returnFunc, ok := ret.Get(0).(func() uint64); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(uint64)
}
return r0
}
// KeyValueEntry_Revision_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Revision'
type KeyValueEntry_Revision_Call struct {
*mock.Call
}
// Revision is a helper method to define mock.On call
func (_e *KeyValueEntry_Expecter) Revision() *KeyValueEntry_Revision_Call {
return &KeyValueEntry_Revision_Call{Call: _e.mock.On("Revision")}
}
func (_c *KeyValueEntry_Revision_Call) Run(run func()) *KeyValueEntry_Revision_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *KeyValueEntry_Revision_Call) Return(v uint64) *KeyValueEntry_Revision_Call {
_c.Call.Return(v)
return _c
}
func (_c *KeyValueEntry_Revision_Call) RunAndReturn(run func() uint64) *KeyValueEntry_Revision_Call {
_c.Call.Return(run)
return _c
}
// Value provides a mock function for the type KeyValueEntry
func (_mock *KeyValueEntry) Value() []byte {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Value")
}
var r0 []byte
if returnFunc, ok := ret.Get(0).(func() []byte); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
return r0
}
// KeyValueEntry_Value_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Value'
type KeyValueEntry_Value_Call struct {
*mock.Call
}
// Value is a helper method to define mock.On call
func (_e *KeyValueEntry_Expecter) Value() *KeyValueEntry_Value_Call {
return &KeyValueEntry_Value_Call{Call: _e.mock.On("Value")}
}
func (_c *KeyValueEntry_Value_Call) Run(run func()) *KeyValueEntry_Value_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *KeyValueEntry_Value_Call) Return(bytes []byte) *KeyValueEntry_Value_Call {
_c.Call.Return(bytes)
return _c
}
func (_c *KeyValueEntry_Value_Call) RunAndReturn(run func() []byte) *KeyValueEntry_Value_Call {
_c.Call.Return(run)
return _c
}
+289
View File
@@ -0,0 +1,289 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
mock "github.com/stretchr/testify/mock"
"go-micro.dev/v4/client"
)
// NewPermissions creates a new instance of Permissions. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewPermissions(t interface {
mock.TestingT
Cleanup(func())
}) *Permissions {
mock := &Permissions{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Permissions is an autogenerated mock type for the Permissions type
type Permissions struct {
mock.Mock
}
type Permissions_Expecter struct {
mock *mock.Mock
}
func (_m *Permissions) EXPECT() *Permissions_Expecter {
return &Permissions_Expecter{mock: &_m.Mock}
}
// GetPermissionByID provides a mock function for the type Permissions
func (_mock *Permissions) GetPermissionByID(ctx context.Context, request *v0.GetPermissionByIDRequest, opts ...client.CallOption) (*v0.GetPermissionByIDResponse, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, request, opts)
} else {
tmpRet = _mock.Called(ctx, request)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for GetPermissionByID")
}
var r0 *v0.GetPermissionByIDResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.GetPermissionByIDRequest, ...client.CallOption) (*v0.GetPermissionByIDResponse, error)); ok {
return returnFunc(ctx, request, opts...)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.GetPermissionByIDRequest, ...client.CallOption) *v0.GetPermissionByIDResponse); ok {
r0 = returnFunc(ctx, request, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.GetPermissionByIDResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v0.GetPermissionByIDRequest, ...client.CallOption) error); ok {
r1 = returnFunc(ctx, request, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Permissions_GetPermissionByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPermissionByID'
type Permissions_GetPermissionByID_Call struct {
*mock.Call
}
// GetPermissionByID is a helper method to define mock.On call
// - ctx context.Context
// - request *v0.GetPermissionByIDRequest
// - opts ...client.CallOption
func (_e *Permissions_Expecter) GetPermissionByID(ctx interface{}, request interface{}, opts ...interface{}) *Permissions_GetPermissionByID_Call {
return &Permissions_GetPermissionByID_Call{Call: _e.mock.On("GetPermissionByID",
append([]interface{}{ctx, request}, opts...)...)}
}
func (_c *Permissions_GetPermissionByID_Call) Run(run func(ctx context.Context, request *v0.GetPermissionByIDRequest, opts ...client.CallOption)) *Permissions_GetPermissionByID_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v0.GetPermissionByIDRequest
if args[1] != nil {
arg1 = args[1].(*v0.GetPermissionByIDRequest)
}
var arg2 []client.CallOption
var variadicArgs []client.CallOption
if len(args) > 2 {
variadicArgs = args[2].([]client.CallOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *Permissions_GetPermissionByID_Call) Return(getPermissionByIDResponse *v0.GetPermissionByIDResponse, err error) *Permissions_GetPermissionByID_Call {
_c.Call.Return(getPermissionByIDResponse, err)
return _c
}
func (_c *Permissions_GetPermissionByID_Call) RunAndReturn(run func(ctx context.Context, request *v0.GetPermissionByIDRequest, opts ...client.CallOption) (*v0.GetPermissionByIDResponse, error)) *Permissions_GetPermissionByID_Call {
_c.Call.Return(run)
return _c
}
// ListPermissions provides a mock function for the type Permissions
func (_mock *Permissions) ListPermissions(ctx context.Context, req *v0.ListPermissionsRequest, opts ...client.CallOption) (*v0.ListPermissionsResponse, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, req, opts)
} else {
tmpRet = _mock.Called(ctx, req)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for ListPermissions")
}
var r0 *v0.ListPermissionsResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.ListPermissionsRequest, ...client.CallOption) (*v0.ListPermissionsResponse, error)); ok {
return returnFunc(ctx, req, opts...)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.ListPermissionsRequest, ...client.CallOption) *v0.ListPermissionsResponse); ok {
r0 = returnFunc(ctx, req, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.ListPermissionsResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v0.ListPermissionsRequest, ...client.CallOption) error); ok {
r1 = returnFunc(ctx, req, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Permissions_ListPermissions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPermissions'
type Permissions_ListPermissions_Call struct {
*mock.Call
}
// ListPermissions is a helper method to define mock.On call
// - ctx context.Context
// - req *v0.ListPermissionsRequest
// - opts ...client.CallOption
func (_e *Permissions_Expecter) ListPermissions(ctx interface{}, req interface{}, opts ...interface{}) *Permissions_ListPermissions_Call {
return &Permissions_ListPermissions_Call{Call: _e.mock.On("ListPermissions",
append([]interface{}{ctx, req}, opts...)...)}
}
func (_c *Permissions_ListPermissions_Call) Run(run func(ctx context.Context, req *v0.ListPermissionsRequest, opts ...client.CallOption)) *Permissions_ListPermissions_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v0.ListPermissionsRequest
if args[1] != nil {
arg1 = args[1].(*v0.ListPermissionsRequest)
}
var arg2 []client.CallOption
var variadicArgs []client.CallOption
if len(args) > 2 {
variadicArgs = args[2].([]client.CallOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *Permissions_ListPermissions_Call) Return(listPermissionsResponse *v0.ListPermissionsResponse, err error) *Permissions_ListPermissions_Call {
_c.Call.Return(listPermissionsResponse, err)
return _c
}
func (_c *Permissions_ListPermissions_Call) RunAndReturn(run func(ctx context.Context, req *v0.ListPermissionsRequest, opts ...client.CallOption) (*v0.ListPermissionsResponse, error)) *Permissions_ListPermissions_Call {
_c.Call.Return(run)
return _c
}
// ListPermissionsByResource provides a mock function for the type Permissions
func (_mock *Permissions) ListPermissionsByResource(ctx context.Context, in *v0.ListPermissionsByResourceRequest, opts ...client.CallOption) (*v0.ListPermissionsByResourceResponse, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, in, opts)
} else {
tmpRet = _mock.Called(ctx, in)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for ListPermissionsByResource")
}
var r0 *v0.ListPermissionsByResourceResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.ListPermissionsByResourceRequest, ...client.CallOption) (*v0.ListPermissionsByResourceResponse, error)); ok {
return returnFunc(ctx, in, opts...)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.ListPermissionsByResourceRequest, ...client.CallOption) *v0.ListPermissionsByResourceResponse); ok {
r0 = returnFunc(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.ListPermissionsByResourceResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v0.ListPermissionsByResourceRequest, ...client.CallOption) error); ok {
r1 = returnFunc(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Permissions_ListPermissionsByResource_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPermissionsByResource'
type Permissions_ListPermissionsByResource_Call struct {
*mock.Call
}
// ListPermissionsByResource is a helper method to define mock.On call
// - ctx context.Context
// - in *v0.ListPermissionsByResourceRequest
// - opts ...client.CallOption
func (_e *Permissions_Expecter) ListPermissionsByResource(ctx interface{}, in interface{}, opts ...interface{}) *Permissions_ListPermissionsByResource_Call {
return &Permissions_ListPermissionsByResource_Call{Call: _e.mock.On("ListPermissionsByResource",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *Permissions_ListPermissionsByResource_Call) Run(run func(ctx context.Context, in *v0.ListPermissionsByResourceRequest, opts ...client.CallOption)) *Permissions_ListPermissionsByResource_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v0.ListPermissionsByResourceRequest
if args[1] != nil {
arg1 = args[1].(*v0.ListPermissionsByResourceRequest)
}
var arg2 []client.CallOption
var variadicArgs []client.CallOption
if len(args) > 2 {
variadicArgs = args[2].([]client.CallOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *Permissions_ListPermissionsByResource_Call) Return(listPermissionsByResourceResponse *v0.ListPermissionsByResourceResponse, err error) *Permissions_ListPermissionsByResource_Call {
_c.Call.Return(listPermissionsByResourceResponse, err)
return _c
}
func (_c *Permissions_ListPermissionsByResource_Call) RunAndReturn(run func(ctx context.Context, in *v0.ListPermissionsByResourceRequest, opts ...client.CallOption) (*v0.ListPermissionsByResourceResponse, error)) *Permissions_ListPermissionsByResource_Call {
_c.Call.Return(run)
return _c
}
+109
View File
@@ -0,0 +1,109 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
mock "github.com/stretchr/testify/mock"
"go-micro.dev/v4/events"
)
// NewPublisher creates a new instance of Publisher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewPublisher(t interface {
mock.TestingT
Cleanup(func())
}) *Publisher {
mock := &Publisher{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Publisher is an autogenerated mock type for the Publisher type
type Publisher struct {
mock.Mock
}
type Publisher_Expecter struct {
mock *mock.Mock
}
func (_m *Publisher) EXPECT() *Publisher_Expecter {
return &Publisher_Expecter{mock: &_m.Mock}
}
// Publish provides a mock function for the type Publisher
func (_mock *Publisher) Publish(s string, ifaceVal interface{}, publishOptions ...events.PublishOption) error {
var tmpRet mock.Arguments
if len(publishOptions) > 0 {
tmpRet = _mock.Called(s, ifaceVal, publishOptions)
} else {
tmpRet = _mock.Called(s, ifaceVal)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for Publish")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string, interface{}, ...events.PublishOption) error); ok {
r0 = returnFunc(s, ifaceVal, publishOptions...)
} else {
r0 = ret.Error(0)
}
return r0
}
// Publisher_Publish_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Publish'
type Publisher_Publish_Call struct {
*mock.Call
}
// Publish is a helper method to define mock.On call
// - s string
// - ifaceVal interface{}
// - publishOptions ...events.PublishOption
func (_e *Publisher_Expecter) Publish(s interface{}, ifaceVal interface{}, publishOptions ...interface{}) *Publisher_Publish_Call {
return &Publisher_Publish_Call{Call: _e.mock.On("Publish",
append([]interface{}{s, ifaceVal}, publishOptions...)...)}
}
func (_c *Publisher_Publish_Call) Run(run func(s string, ifaceVal interface{}, publishOptions ...events.PublishOption)) *Publisher_Publish_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 interface{}
if args[1] != nil {
arg1 = args[1].(interface{})
}
var arg2 []events.PublishOption
var variadicArgs []events.PublishOption
if len(args) > 2 {
variadicArgs = args[2].([]events.PublishOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *Publisher_Publish_Call) Return(err error) *Publisher_Publish_Call {
_c.Call.Return(err)
return _c
}
func (_c *Publisher_Publish_Call) RunAndReturn(run func(s string, ifaceVal interface{}, publishOptions ...events.PublishOption) error) *Publisher_Publish_Call {
_c.Call.Return(run)
return _c
}
+456
View File
@@ -0,0 +1,456 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
mock "github.com/stretchr/testify/mock"
"go-micro.dev/v4/client"
"google.golang.org/protobuf/types/known/emptypb"
)
// NewRoleService creates a new instance of RoleService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewRoleService(t interface {
mock.TestingT
Cleanup(func())
}) *RoleService {
mock := &RoleService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// RoleService is an autogenerated mock type for the RoleService type
type RoleService struct {
mock.Mock
}
type RoleService_Expecter struct {
mock *mock.Mock
}
func (_m *RoleService) EXPECT() *RoleService_Expecter {
return &RoleService_Expecter{mock: &_m.Mock}
}
// AssignRoleToUser provides a mock function for the type RoleService
func (_mock *RoleService) AssignRoleToUser(ctx context.Context, in *v0.AssignRoleToUserRequest, opts ...client.CallOption) (*v0.AssignRoleToUserResponse, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, in, opts)
} else {
tmpRet = _mock.Called(ctx, in)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for AssignRoleToUser")
}
var r0 *v0.AssignRoleToUserResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.AssignRoleToUserRequest, ...client.CallOption) (*v0.AssignRoleToUserResponse, error)); ok {
return returnFunc(ctx, in, opts...)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.AssignRoleToUserRequest, ...client.CallOption) *v0.AssignRoleToUserResponse); ok {
r0 = returnFunc(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.AssignRoleToUserResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v0.AssignRoleToUserRequest, ...client.CallOption) error); ok {
r1 = returnFunc(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RoleService_AssignRoleToUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AssignRoleToUser'
type RoleService_AssignRoleToUser_Call struct {
*mock.Call
}
// AssignRoleToUser is a helper method to define mock.On call
// - ctx context.Context
// - in *v0.AssignRoleToUserRequest
// - opts ...client.CallOption
func (_e *RoleService_Expecter) AssignRoleToUser(ctx interface{}, in interface{}, opts ...interface{}) *RoleService_AssignRoleToUser_Call {
return &RoleService_AssignRoleToUser_Call{Call: _e.mock.On("AssignRoleToUser",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *RoleService_AssignRoleToUser_Call) Run(run func(ctx context.Context, in *v0.AssignRoleToUserRequest, opts ...client.CallOption)) *RoleService_AssignRoleToUser_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v0.AssignRoleToUserRequest
if args[1] != nil {
arg1 = args[1].(*v0.AssignRoleToUserRequest)
}
var arg2 []client.CallOption
var variadicArgs []client.CallOption
if len(args) > 2 {
variadicArgs = args[2].([]client.CallOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *RoleService_AssignRoleToUser_Call) Return(assignRoleToUserResponse *v0.AssignRoleToUserResponse, err error) *RoleService_AssignRoleToUser_Call {
_c.Call.Return(assignRoleToUserResponse, err)
return _c
}
func (_c *RoleService_AssignRoleToUser_Call) RunAndReturn(run func(ctx context.Context, in *v0.AssignRoleToUserRequest, opts ...client.CallOption) (*v0.AssignRoleToUserResponse, error)) *RoleService_AssignRoleToUser_Call {
_c.Call.Return(run)
return _c
}
// ListRoleAssignments provides a mock function for the type RoleService
func (_mock *RoleService) ListRoleAssignments(ctx context.Context, in *v0.ListRoleAssignmentsRequest, opts ...client.CallOption) (*v0.ListRoleAssignmentsResponse, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, in, opts)
} else {
tmpRet = _mock.Called(ctx, in)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for ListRoleAssignments")
}
var r0 *v0.ListRoleAssignmentsResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.ListRoleAssignmentsRequest, ...client.CallOption) (*v0.ListRoleAssignmentsResponse, error)); ok {
return returnFunc(ctx, in, opts...)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.ListRoleAssignmentsRequest, ...client.CallOption) *v0.ListRoleAssignmentsResponse); ok {
r0 = returnFunc(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.ListRoleAssignmentsResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v0.ListRoleAssignmentsRequest, ...client.CallOption) error); ok {
r1 = returnFunc(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RoleService_ListRoleAssignments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRoleAssignments'
type RoleService_ListRoleAssignments_Call struct {
*mock.Call
}
// ListRoleAssignments is a helper method to define mock.On call
// - ctx context.Context
// - in *v0.ListRoleAssignmentsRequest
// - opts ...client.CallOption
func (_e *RoleService_Expecter) ListRoleAssignments(ctx interface{}, in interface{}, opts ...interface{}) *RoleService_ListRoleAssignments_Call {
return &RoleService_ListRoleAssignments_Call{Call: _e.mock.On("ListRoleAssignments",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *RoleService_ListRoleAssignments_Call) Run(run func(ctx context.Context, in *v0.ListRoleAssignmentsRequest, opts ...client.CallOption)) *RoleService_ListRoleAssignments_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v0.ListRoleAssignmentsRequest
if args[1] != nil {
arg1 = args[1].(*v0.ListRoleAssignmentsRequest)
}
var arg2 []client.CallOption
var variadicArgs []client.CallOption
if len(args) > 2 {
variadicArgs = args[2].([]client.CallOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *RoleService_ListRoleAssignments_Call) Return(listRoleAssignmentsResponse *v0.ListRoleAssignmentsResponse, err error) *RoleService_ListRoleAssignments_Call {
_c.Call.Return(listRoleAssignmentsResponse, err)
return _c
}
func (_c *RoleService_ListRoleAssignments_Call) RunAndReturn(run func(ctx context.Context, in *v0.ListRoleAssignmentsRequest, opts ...client.CallOption) (*v0.ListRoleAssignmentsResponse, error)) *RoleService_ListRoleAssignments_Call {
_c.Call.Return(run)
return _c
}
// ListRoleAssignmentsFiltered provides a mock function for the type RoleService
func (_mock *RoleService) ListRoleAssignmentsFiltered(ctx context.Context, in *v0.ListRoleAssignmentsFilteredRequest, opts ...client.CallOption) (*v0.ListRoleAssignmentsResponse, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, in, opts)
} else {
tmpRet = _mock.Called(ctx, in)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for ListRoleAssignmentsFiltered")
}
var r0 *v0.ListRoleAssignmentsResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.ListRoleAssignmentsFilteredRequest, ...client.CallOption) (*v0.ListRoleAssignmentsResponse, error)); ok {
return returnFunc(ctx, in, opts...)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.ListRoleAssignmentsFilteredRequest, ...client.CallOption) *v0.ListRoleAssignmentsResponse); ok {
r0 = returnFunc(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.ListRoleAssignmentsResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v0.ListRoleAssignmentsFilteredRequest, ...client.CallOption) error); ok {
r1 = returnFunc(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RoleService_ListRoleAssignmentsFiltered_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRoleAssignmentsFiltered'
type RoleService_ListRoleAssignmentsFiltered_Call struct {
*mock.Call
}
// ListRoleAssignmentsFiltered is a helper method to define mock.On call
// - ctx context.Context
// - in *v0.ListRoleAssignmentsFilteredRequest
// - opts ...client.CallOption
func (_e *RoleService_Expecter) ListRoleAssignmentsFiltered(ctx interface{}, in interface{}, opts ...interface{}) *RoleService_ListRoleAssignmentsFiltered_Call {
return &RoleService_ListRoleAssignmentsFiltered_Call{Call: _e.mock.On("ListRoleAssignmentsFiltered",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *RoleService_ListRoleAssignmentsFiltered_Call) Run(run func(ctx context.Context, in *v0.ListRoleAssignmentsFilteredRequest, opts ...client.CallOption)) *RoleService_ListRoleAssignmentsFiltered_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v0.ListRoleAssignmentsFilteredRequest
if args[1] != nil {
arg1 = args[1].(*v0.ListRoleAssignmentsFilteredRequest)
}
var arg2 []client.CallOption
var variadicArgs []client.CallOption
if len(args) > 2 {
variadicArgs = args[2].([]client.CallOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *RoleService_ListRoleAssignmentsFiltered_Call) Return(listRoleAssignmentsResponse *v0.ListRoleAssignmentsResponse, err error) *RoleService_ListRoleAssignmentsFiltered_Call {
_c.Call.Return(listRoleAssignmentsResponse, err)
return _c
}
func (_c *RoleService_ListRoleAssignmentsFiltered_Call) RunAndReturn(run func(ctx context.Context, in *v0.ListRoleAssignmentsFilteredRequest, opts ...client.CallOption) (*v0.ListRoleAssignmentsResponse, error)) *RoleService_ListRoleAssignmentsFiltered_Call {
_c.Call.Return(run)
return _c
}
// ListRoles provides a mock function for the type RoleService
func (_mock *RoleService) ListRoles(ctx context.Context, in *v0.ListBundlesRequest, opts ...client.CallOption) (*v0.ListBundlesResponse, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, in, opts)
} else {
tmpRet = _mock.Called(ctx, in)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for ListRoles")
}
var r0 *v0.ListBundlesResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.ListBundlesRequest, ...client.CallOption) (*v0.ListBundlesResponse, error)); ok {
return returnFunc(ctx, in, opts...)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.ListBundlesRequest, ...client.CallOption) *v0.ListBundlesResponse); ok {
r0 = returnFunc(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v0.ListBundlesResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v0.ListBundlesRequest, ...client.CallOption) error); ok {
r1 = returnFunc(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RoleService_ListRoles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRoles'
type RoleService_ListRoles_Call struct {
*mock.Call
}
// ListRoles is a helper method to define mock.On call
// - ctx context.Context
// - in *v0.ListBundlesRequest
// - opts ...client.CallOption
func (_e *RoleService_Expecter) ListRoles(ctx interface{}, in interface{}, opts ...interface{}) *RoleService_ListRoles_Call {
return &RoleService_ListRoles_Call{Call: _e.mock.On("ListRoles",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *RoleService_ListRoles_Call) Run(run func(ctx context.Context, in *v0.ListBundlesRequest, opts ...client.CallOption)) *RoleService_ListRoles_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v0.ListBundlesRequest
if args[1] != nil {
arg1 = args[1].(*v0.ListBundlesRequest)
}
var arg2 []client.CallOption
var variadicArgs []client.CallOption
if len(args) > 2 {
variadicArgs = args[2].([]client.CallOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *RoleService_ListRoles_Call) Return(listBundlesResponse *v0.ListBundlesResponse, err error) *RoleService_ListRoles_Call {
_c.Call.Return(listBundlesResponse, err)
return _c
}
func (_c *RoleService_ListRoles_Call) RunAndReturn(run func(ctx context.Context, in *v0.ListBundlesRequest, opts ...client.CallOption) (*v0.ListBundlesResponse, error)) *RoleService_ListRoles_Call {
_c.Call.Return(run)
return _c
}
// RemoveRoleFromUser provides a mock function for the type RoleService
func (_mock *RoleService) RemoveRoleFromUser(ctx context.Context, in *v0.RemoveRoleFromUserRequest, opts ...client.CallOption) (*emptypb.Empty, error) {
var tmpRet mock.Arguments
if len(opts) > 0 {
tmpRet = _mock.Called(ctx, in, opts)
} else {
tmpRet = _mock.Called(ctx, in)
}
ret := tmpRet
if len(ret) == 0 {
panic("no return value specified for RemoveRoleFromUser")
}
var r0 *emptypb.Empty
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.RemoveRoleFromUserRequest, ...client.CallOption) (*emptypb.Empty, error)); ok {
return returnFunc(ctx, in, opts...)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v0.RemoveRoleFromUserRequest, ...client.CallOption) *emptypb.Empty); ok {
r0 = returnFunc(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*emptypb.Empty)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v0.RemoveRoleFromUserRequest, ...client.CallOption) error); ok {
r1 = returnFunc(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RoleService_RemoveRoleFromUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveRoleFromUser'
type RoleService_RemoveRoleFromUser_Call struct {
*mock.Call
}
// RemoveRoleFromUser is a helper method to define mock.On call
// - ctx context.Context
// - in *v0.RemoveRoleFromUserRequest
// - opts ...client.CallOption
func (_e *RoleService_Expecter) RemoveRoleFromUser(ctx interface{}, in interface{}, opts ...interface{}) *RoleService_RemoveRoleFromUser_Call {
return &RoleService_RemoveRoleFromUser_Call{Call: _e.mock.On("RemoveRoleFromUser",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *RoleService_RemoveRoleFromUser_Call) Run(run func(ctx context.Context, in *v0.RemoveRoleFromUserRequest, opts ...client.CallOption)) *RoleService_RemoveRoleFromUser_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v0.RemoveRoleFromUserRequest
if args[1] != nil {
arg1 = args[1].(*v0.RemoveRoleFromUserRequest)
}
var arg2 []client.CallOption
var variadicArgs []client.CallOption
if len(args) > 2 {
variadicArgs = args[2].([]client.CallOption)
}
arg2 = variadicArgs
run(
arg0,
arg1,
arg2...,
)
})
return _c
}
func (_c *RoleService_RemoveRoleFromUser_Call) Return(empty *emptypb.Empty, err error) *RoleService_RemoveRoleFromUser_Call {
_c.Call.Return(empty, err)
return _c
}
func (_c *RoleService_RemoveRoleFromUser_Call) RunAndReturn(run func(ctx context.Context, in *v0.RemoveRoleFromUserRequest, opts ...client.CallOption) (*emptypb.Empty, error)) *RoleService_RemoveRoleFromUser_Call {
_c.Call.Return(run)
return _c
}
+855
View File
@@ -0,0 +1,855 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
mock "github.com/stretchr/testify/mock"
)
// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewStorage(t interface {
mock.TestingT
Cleanup(func())
}) *Storage {
mock := &Storage{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Storage is an autogenerated mock type for the Storage type
type Storage struct {
mock.Mock
}
type Storage_Expecter struct {
mock *mock.Mock
}
func (_m *Storage) EXPECT() *Storage_Expecter {
return &Storage_Expecter{mock: &_m.Mock}
}
// Backend provides a mock function for the type Storage
func (_mock *Storage) Backend() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for Backend")
}
var r0 string
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Storage_Backend_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Backend'
type Storage_Backend_Call struct {
*mock.Call
}
// Backend is a helper method to define mock.On call
func (_e *Storage_Expecter) Backend() *Storage_Backend_Call {
return &Storage_Backend_Call{Call: _e.mock.On("Backend")}
}
func (_c *Storage_Backend_Call) Run(run func()) *Storage_Backend_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Storage_Backend_Call) Return(s string) *Storage_Backend_Call {
_c.Call.Return(s)
return _c
}
func (_c *Storage_Backend_Call) RunAndReturn(run func() string) *Storage_Backend_Call {
_c.Call.Return(run)
return _c
}
// CreateSymlink provides a mock function for the type Storage
func (_mock *Storage) CreateSymlink(ctx context.Context, oldname string, newname string) error {
ret := _mock.Called(ctx, oldname, newname)
if len(ret) == 0 {
panic("no return value specified for CreateSymlink")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = returnFunc(ctx, oldname, newname)
} else {
r0 = ret.Error(0)
}
return r0
}
// Storage_CreateSymlink_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateSymlink'
type Storage_CreateSymlink_Call struct {
*mock.Call
}
// CreateSymlink is a helper method to define mock.On call
// - ctx context.Context
// - oldname string
// - newname string
func (_e *Storage_Expecter) CreateSymlink(ctx interface{}, oldname interface{}, newname interface{}) *Storage_CreateSymlink_Call {
return &Storage_CreateSymlink_Call{Call: _e.mock.On("CreateSymlink", ctx, oldname, newname)}
}
func (_c *Storage_CreateSymlink_Call) Run(run func(ctx context.Context, oldname string, newname string)) *Storage_CreateSymlink_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *Storage_CreateSymlink_Call) Return(err error) *Storage_CreateSymlink_Call {
_c.Call.Return(err)
return _c
}
func (_c *Storage_CreateSymlink_Call) RunAndReturn(run func(ctx context.Context, oldname string, newname string) error) *Storage_CreateSymlink_Call {
_c.Call.Return(run)
return _c
}
// Delete provides a mock function for the type Storage
func (_mock *Storage) Delete(ctx context.Context, path string) error {
ret := _mock.Called(ctx, path)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = returnFunc(ctx, path)
} else {
r0 = ret.Error(0)
}
return r0
}
// Storage_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type Storage_Delete_Call struct {
*mock.Call
}
// Delete is a helper method to define mock.On call
// - ctx context.Context
// - path string
func (_e *Storage_Expecter) Delete(ctx interface{}, path interface{}) *Storage_Delete_Call {
return &Storage_Delete_Call{Call: _e.mock.On("Delete", ctx, path)}
}
func (_c *Storage_Delete_Call) Run(run func(ctx context.Context, path string)) *Storage_Delete_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Storage_Delete_Call) Return(err error) *Storage_Delete_Call {
_c.Call.Return(err)
return _c
}
func (_c *Storage_Delete_Call) RunAndReturn(run func(ctx context.Context, path string) error) *Storage_Delete_Call {
_c.Call.Return(run)
return _c
}
// Download provides a mock function for the type Storage
func (_mock *Storage) Download(ctx context.Context, req metadata.DownloadRequest) (*metadata.DownloadResponse, error) {
ret := _mock.Called(ctx, req)
if len(ret) == 0 {
panic("no return value specified for Download")
}
var r0 *metadata.DownloadResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, metadata.DownloadRequest) (*metadata.DownloadResponse, error)); ok {
return returnFunc(ctx, req)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, metadata.DownloadRequest) *metadata.DownloadResponse); ok {
r0 = returnFunc(ctx, req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*metadata.DownloadResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, metadata.DownloadRequest) error); ok {
r1 = returnFunc(ctx, req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Storage_Download_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Download'
type Storage_Download_Call struct {
*mock.Call
}
// Download is a helper method to define mock.On call
// - ctx context.Context
// - req metadata.DownloadRequest
func (_e *Storage_Expecter) Download(ctx interface{}, req interface{}) *Storage_Download_Call {
return &Storage_Download_Call{Call: _e.mock.On("Download", ctx, req)}
}
func (_c *Storage_Download_Call) Run(run func(ctx context.Context, req metadata.DownloadRequest)) *Storage_Download_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 metadata.DownloadRequest
if args[1] != nil {
arg1 = args[1].(metadata.DownloadRequest)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Storage_Download_Call) Return(downloadResponse *metadata.DownloadResponse, err error) *Storage_Download_Call {
_c.Call.Return(downloadResponse, err)
return _c
}
func (_c *Storage_Download_Call) RunAndReturn(run func(ctx context.Context, req metadata.DownloadRequest) (*metadata.DownloadResponse, error)) *Storage_Download_Call {
_c.Call.Return(run)
return _c
}
// Init provides a mock function for the type Storage
func (_mock *Storage) Init(ctx context.Context, name string) error {
ret := _mock.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for Init")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = returnFunc(ctx, name)
} else {
r0 = ret.Error(0)
}
return r0
}
// Storage_Init_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Init'
type Storage_Init_Call struct {
*mock.Call
}
// Init is a helper method to define mock.On call
// - ctx context.Context
// - name string
func (_e *Storage_Expecter) Init(ctx interface{}, name interface{}) *Storage_Init_Call {
return &Storage_Init_Call{Call: _e.mock.On("Init", ctx, name)}
}
func (_c *Storage_Init_Call) Run(run func(ctx context.Context, name string)) *Storage_Init_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Storage_Init_Call) Return(err error) *Storage_Init_Call {
_c.Call.Return(err)
return _c
}
func (_c *Storage_Init_Call) RunAndReturn(run func(ctx context.Context, name string) error) *Storage_Init_Call {
_c.Call.Return(run)
return _c
}
// ListDir provides a mock function for the type Storage
func (_mock *Storage) ListDir(ctx context.Context, path string) ([]*providerv1beta1.ResourceInfo, error) {
ret := _mock.Called(ctx, path)
if len(ret) == 0 {
panic("no return value specified for ListDir")
}
var r0 []*providerv1beta1.ResourceInfo
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]*providerv1beta1.ResourceInfo, error)); ok {
return returnFunc(ctx, path)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []*providerv1beta1.ResourceInfo); ok {
r0 = returnFunc(ctx, path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*providerv1beta1.ResourceInfo)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Storage_ListDir_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListDir'
type Storage_ListDir_Call struct {
*mock.Call
}
// ListDir is a helper method to define mock.On call
// - ctx context.Context
// - path string
func (_e *Storage_Expecter) ListDir(ctx interface{}, path interface{}) *Storage_ListDir_Call {
return &Storage_ListDir_Call{Call: _e.mock.On("ListDir", ctx, path)}
}
func (_c *Storage_ListDir_Call) Run(run func(ctx context.Context, path string)) *Storage_ListDir_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Storage_ListDir_Call) Return(resourceInfos []*providerv1beta1.ResourceInfo, err error) *Storage_ListDir_Call {
_c.Call.Return(resourceInfos, err)
return _c
}
func (_c *Storage_ListDir_Call) RunAndReturn(run func(ctx context.Context, path string) ([]*providerv1beta1.ResourceInfo, error)) *Storage_ListDir_Call {
_c.Call.Return(run)
return _c
}
// MakeDirIfNotExist provides a mock function for the type Storage
func (_mock *Storage) MakeDirIfNotExist(ctx context.Context, name string) error {
ret := _mock.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for MakeDirIfNotExist")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = returnFunc(ctx, name)
} else {
r0 = ret.Error(0)
}
return r0
}
// Storage_MakeDirIfNotExist_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MakeDirIfNotExist'
type Storage_MakeDirIfNotExist_Call struct {
*mock.Call
}
// MakeDirIfNotExist is a helper method to define mock.On call
// - ctx context.Context
// - name string
func (_e *Storage_Expecter) MakeDirIfNotExist(ctx interface{}, name interface{}) *Storage_MakeDirIfNotExist_Call {
return &Storage_MakeDirIfNotExist_Call{Call: _e.mock.On("MakeDirIfNotExist", ctx, name)}
}
func (_c *Storage_MakeDirIfNotExist_Call) Run(run func(ctx context.Context, name string)) *Storage_MakeDirIfNotExist_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Storage_MakeDirIfNotExist_Call) Return(err error) *Storage_MakeDirIfNotExist_Call {
_c.Call.Return(err)
return _c
}
func (_c *Storage_MakeDirIfNotExist_Call) RunAndReturn(run func(ctx context.Context, name string) error) *Storage_MakeDirIfNotExist_Call {
_c.Call.Return(run)
return _c
}
// ReadDir provides a mock function for the type Storage
func (_mock *Storage) ReadDir(ctx context.Context, path string) ([]string, error) {
ret := _mock.Called(ctx, path)
if len(ret) == 0 {
panic("no return value specified for ReadDir")
}
var r0 []string
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok {
return returnFunc(ctx, path)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok {
r0 = returnFunc(ctx, path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Storage_ReadDir_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadDir'
type Storage_ReadDir_Call struct {
*mock.Call
}
// ReadDir is a helper method to define mock.On call
// - ctx context.Context
// - path string
func (_e *Storage_Expecter) ReadDir(ctx interface{}, path interface{}) *Storage_ReadDir_Call {
return &Storage_ReadDir_Call{Call: _e.mock.On("ReadDir", ctx, path)}
}
func (_c *Storage_ReadDir_Call) Run(run func(ctx context.Context, path string)) *Storage_ReadDir_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Storage_ReadDir_Call) Return(strings []string, err error) *Storage_ReadDir_Call {
_c.Call.Return(strings, err)
return _c
}
func (_c *Storage_ReadDir_Call) RunAndReturn(run func(ctx context.Context, path string) ([]string, error)) *Storage_ReadDir_Call {
_c.Call.Return(run)
return _c
}
// ResolveSymlink provides a mock function for the type Storage
func (_mock *Storage) ResolveSymlink(ctx context.Context, name string) (string, error) {
ret := _mock.Called(ctx, name)
if len(ret) == 0 {
panic("no return value specified for ResolveSymlink")
}
var r0 string
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok {
return returnFunc(ctx, name)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) string); ok {
r0 = returnFunc(ctx, name)
} else {
r0 = ret.Get(0).(string)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Storage_ResolveSymlink_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResolveSymlink'
type Storage_ResolveSymlink_Call struct {
*mock.Call
}
// ResolveSymlink is a helper method to define mock.On call
// - ctx context.Context
// - name string
func (_e *Storage_Expecter) ResolveSymlink(ctx interface{}, name interface{}) *Storage_ResolveSymlink_Call {
return &Storage_ResolveSymlink_Call{Call: _e.mock.On("ResolveSymlink", ctx, name)}
}
func (_c *Storage_ResolveSymlink_Call) Run(run func(ctx context.Context, name string)) *Storage_ResolveSymlink_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Storage_ResolveSymlink_Call) Return(s string, err error) *Storage_ResolveSymlink_Call {
_c.Call.Return(s, err)
return _c
}
func (_c *Storage_ResolveSymlink_Call) RunAndReturn(run func(ctx context.Context, name string) (string, error)) *Storage_ResolveSymlink_Call {
_c.Call.Return(run)
return _c
}
// SimpleDownload provides a mock function for the type Storage
func (_mock *Storage) SimpleDownload(ctx context.Context, path string) ([]byte, error) {
ret := _mock.Called(ctx, path)
if len(ret) == 0 {
panic("no return value specified for SimpleDownload")
}
var r0 []byte
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok {
return returnFunc(ctx, path)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []byte); ok {
r0 = returnFunc(ctx, path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Storage_SimpleDownload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SimpleDownload'
type Storage_SimpleDownload_Call struct {
*mock.Call
}
// SimpleDownload is a helper method to define mock.On call
// - ctx context.Context
// - path string
func (_e *Storage_Expecter) SimpleDownload(ctx interface{}, path interface{}) *Storage_SimpleDownload_Call {
return &Storage_SimpleDownload_Call{Call: _e.mock.On("SimpleDownload", ctx, path)}
}
func (_c *Storage_SimpleDownload_Call) Run(run func(ctx context.Context, path string)) *Storage_SimpleDownload_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Storage_SimpleDownload_Call) Return(bytes []byte, err error) *Storage_SimpleDownload_Call {
_c.Call.Return(bytes, err)
return _c
}
func (_c *Storage_SimpleDownload_Call) RunAndReturn(run func(ctx context.Context, path string) ([]byte, error)) *Storage_SimpleDownload_Call {
_c.Call.Return(run)
return _c
}
// SimpleUpload provides a mock function for the type Storage
func (_mock *Storage) SimpleUpload(ctx context.Context, uploadpath string, content []byte) error {
ret := _mock.Called(ctx, uploadpath, content)
if len(ret) == 0 {
panic("no return value specified for SimpleUpload")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, []byte) error); ok {
r0 = returnFunc(ctx, uploadpath, content)
} else {
r0 = ret.Error(0)
}
return r0
}
// Storage_SimpleUpload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SimpleUpload'
type Storage_SimpleUpload_Call struct {
*mock.Call
}
// SimpleUpload is a helper method to define mock.On call
// - ctx context.Context
// - uploadpath string
// - content []byte
func (_e *Storage_Expecter) SimpleUpload(ctx interface{}, uploadpath interface{}, content interface{}) *Storage_SimpleUpload_Call {
return &Storage_SimpleUpload_Call{Call: _e.mock.On("SimpleUpload", ctx, uploadpath, content)}
}
func (_c *Storage_SimpleUpload_Call) Run(run func(ctx context.Context, uploadpath string, content []byte)) *Storage_SimpleUpload_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 []byte
if args[2] != nil {
arg2 = args[2].([]byte)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *Storage_SimpleUpload_Call) Return(err error) *Storage_SimpleUpload_Call {
_c.Call.Return(err)
return _c
}
func (_c *Storage_SimpleUpload_Call) RunAndReturn(run func(ctx context.Context, uploadpath string, content []byte) error) *Storage_SimpleUpload_Call {
_c.Call.Return(run)
return _c
}
// Stat provides a mock function for the type Storage
func (_mock *Storage) Stat(ctx context.Context, path string) (*providerv1beta1.ResourceInfo, error) {
ret := _mock.Called(ctx, path)
if len(ret) == 0 {
panic("no return value specified for Stat")
}
var r0 *providerv1beta1.ResourceInfo
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*providerv1beta1.ResourceInfo, error)); ok {
return returnFunc(ctx, path)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) *providerv1beta1.ResourceInfo); ok {
r0 = returnFunc(ctx, path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*providerv1beta1.ResourceInfo)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Storage_Stat_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stat'
type Storage_Stat_Call struct {
*mock.Call
}
// Stat is a helper method to define mock.On call
// - ctx context.Context
// - path string
func (_e *Storage_Expecter) Stat(ctx interface{}, path interface{}) *Storage_Stat_Call {
return &Storage_Stat_Call{Call: _e.mock.On("Stat", ctx, path)}
}
func (_c *Storage_Stat_Call) Run(run func(ctx context.Context, path string)) *Storage_Stat_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Storage_Stat_Call) Return(resourceInfo *providerv1beta1.ResourceInfo, err error) *Storage_Stat_Call {
_c.Call.Return(resourceInfo, err)
return _c
}
func (_c *Storage_Stat_Call) RunAndReturn(run func(ctx context.Context, path string) (*providerv1beta1.ResourceInfo, error)) *Storage_Stat_Call {
_c.Call.Return(run)
return _c
}
// Upload provides a mock function for the type Storage
func (_mock *Storage) Upload(ctx context.Context, req metadata.UploadRequest) (*metadata.UploadResponse, error) {
ret := _mock.Called(ctx, req)
if len(ret) == 0 {
panic("no return value specified for Upload")
}
var r0 *metadata.UploadResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, metadata.UploadRequest) (*metadata.UploadResponse, error)); ok {
return returnFunc(ctx, req)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, metadata.UploadRequest) *metadata.UploadResponse); ok {
r0 = returnFunc(ctx, req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*metadata.UploadResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, metadata.UploadRequest) error); ok {
r1 = returnFunc(ctx, req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Storage_Upload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upload'
type Storage_Upload_Call struct {
*mock.Call
}
// Upload is a helper method to define mock.On call
// - ctx context.Context
// - req metadata.UploadRequest
func (_e *Storage_Expecter) Upload(ctx interface{}, req interface{}) *Storage_Upload_Call {
return &Storage_Upload_Call{Call: _e.mock.On("Upload", ctx, req)}
}
func (_c *Storage_Upload_Call) Run(run func(ctx context.Context, req metadata.UploadRequest)) *Storage_Upload_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 metadata.UploadRequest
if args[1] != nil {
arg1 = args[1].(metadata.UploadRequest)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Storage_Upload_Call) Return(uploadResponse *metadata.UploadResponse, err error) *Storage_Upload_Call {
_c.Call.Return(uploadResponse, err)
return _c
}
func (_c *Storage_Upload_Call) RunAndReturn(run func(ctx context.Context, req metadata.UploadRequest) (*metadata.UploadResponse, error)) *Storage_Upload_Call {
_c.Call.Return(run)
return _c
}
@@ -0,0 +1,227 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"io"
mock "github.com/stretchr/testify/mock"
)
// NewUsersUserProfilePhotoProvider creates a new instance of UsersUserProfilePhotoProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewUsersUserProfilePhotoProvider(t interface {
mock.TestingT
Cleanup(func())
}) *UsersUserProfilePhotoProvider {
mock := &UsersUserProfilePhotoProvider{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// UsersUserProfilePhotoProvider is an autogenerated mock type for the UsersUserProfilePhotoProvider type
type UsersUserProfilePhotoProvider struct {
mock.Mock
}
type UsersUserProfilePhotoProvider_Expecter struct {
mock *mock.Mock
}
func (_m *UsersUserProfilePhotoProvider) EXPECT() *UsersUserProfilePhotoProvider_Expecter {
return &UsersUserProfilePhotoProvider_Expecter{mock: &_m.Mock}
}
// DeletePhoto provides a mock function for the type UsersUserProfilePhotoProvider
func (_mock *UsersUserProfilePhotoProvider) DeletePhoto(ctx context.Context, id string) error {
ret := _mock.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for DeletePhoto")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = returnFunc(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// UsersUserProfilePhotoProvider_DeletePhoto_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePhoto'
type UsersUserProfilePhotoProvider_DeletePhoto_Call struct {
*mock.Call
}
// DeletePhoto is a helper method to define mock.On call
// - ctx context.Context
// - id string
func (_e *UsersUserProfilePhotoProvider_Expecter) DeletePhoto(ctx interface{}, id interface{}) *UsersUserProfilePhotoProvider_DeletePhoto_Call {
return &UsersUserProfilePhotoProvider_DeletePhoto_Call{Call: _e.mock.On("DeletePhoto", ctx, id)}
}
func (_c *UsersUserProfilePhotoProvider_DeletePhoto_Call) Run(run func(ctx context.Context, id string)) *UsersUserProfilePhotoProvider_DeletePhoto_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *UsersUserProfilePhotoProvider_DeletePhoto_Call) Return(err error) *UsersUserProfilePhotoProvider_DeletePhoto_Call {
_c.Call.Return(err)
return _c
}
func (_c *UsersUserProfilePhotoProvider_DeletePhoto_Call) RunAndReturn(run func(ctx context.Context, id string) error) *UsersUserProfilePhotoProvider_DeletePhoto_Call {
_c.Call.Return(run)
return _c
}
// GetPhoto provides a mock function for the type UsersUserProfilePhotoProvider
func (_mock *UsersUserProfilePhotoProvider) GetPhoto(ctx context.Context, id string) ([]byte, error) {
ret := _mock.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for GetPhoto")
}
var r0 []byte
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok {
return returnFunc(ctx, id)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []byte); ok {
r0 = returnFunc(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UsersUserProfilePhotoProvider_GetPhoto_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPhoto'
type UsersUserProfilePhotoProvider_GetPhoto_Call struct {
*mock.Call
}
// GetPhoto is a helper method to define mock.On call
// - ctx context.Context
// - id string
func (_e *UsersUserProfilePhotoProvider_Expecter) GetPhoto(ctx interface{}, id interface{}) *UsersUserProfilePhotoProvider_GetPhoto_Call {
return &UsersUserProfilePhotoProvider_GetPhoto_Call{Call: _e.mock.On("GetPhoto", ctx, id)}
}
func (_c *UsersUserProfilePhotoProvider_GetPhoto_Call) Run(run func(ctx context.Context, id string)) *UsersUserProfilePhotoProvider_GetPhoto_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *UsersUserProfilePhotoProvider_GetPhoto_Call) Return(bytes []byte, err error) *UsersUserProfilePhotoProvider_GetPhoto_Call {
_c.Call.Return(bytes, err)
return _c
}
func (_c *UsersUserProfilePhotoProvider_GetPhoto_Call) RunAndReturn(run func(ctx context.Context, id string) ([]byte, error)) *UsersUserProfilePhotoProvider_GetPhoto_Call {
_c.Call.Return(run)
return _c
}
// UpdatePhoto provides a mock function for the type UsersUserProfilePhotoProvider
func (_mock *UsersUserProfilePhotoProvider) UpdatePhoto(ctx context.Context, id string, r io.Reader) error {
ret := _mock.Called(ctx, id, r)
if len(ret) == 0 {
panic("no return value specified for UpdatePhoto")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, io.Reader) error); ok {
r0 = returnFunc(ctx, id, r)
} else {
r0 = ret.Error(0)
}
return r0
}
// UsersUserProfilePhotoProvider_UpdatePhoto_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdatePhoto'
type UsersUserProfilePhotoProvider_UpdatePhoto_Call struct {
*mock.Call
}
// UpdatePhoto is a helper method to define mock.On call
// - ctx context.Context
// - id string
// - r io.Reader
func (_e *UsersUserProfilePhotoProvider_Expecter) UpdatePhoto(ctx interface{}, id interface{}, r interface{}) *UsersUserProfilePhotoProvider_UpdatePhoto_Call {
return &UsersUserProfilePhotoProvider_UpdatePhoto_Call{Call: _e.mock.On("UpdatePhoto", ctx, id, r)}
}
func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) Run(run func(ctx context.Context, id string, r io.Reader)) *UsersUserProfilePhotoProvider_UpdatePhoto_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 io.Reader
if args[2] != nil {
arg2 = args[2].(io.Reader)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) Return(err error) *UsersUserProfilePhotoProvider_UpdatePhoto_Call {
_c.Call.Return(err)
return _c
}
func (_c *UsersUserProfilePhotoProvider_UpdatePhoto_Call) RunAndReturn(run func(ctx context.Context, id string, r io.Reader) error) *UsersUserProfilePhotoProvider_UpdatePhoto_Call {
_c.Call.Return(run)
return _c
}
@@ -0,0 +1,54 @@
package command
import (
"fmt"
"net/http"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/config/parser"
"github.com/spf13/cobra"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "health",
Short: "check health status",
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if err != nil {
logger.Fatal().
Err(err).
Msg("Failed to request health check")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Fatal().
Int("code", resp.StatusCode).
Msg("Health seems to be in bad state")
}
logger.Debug().
Int("code", resp.StatusCode).
Msg("Health got a good state")
return nil
},
}
}
+36
View File
@@ -0,0 +1,36 @@
package command
import (
"os"
"github.com/qsfera/server/pkg/clihelper"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/spf13/cobra"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) []*cobra.Command {
return append([]*cobra.Command{
// start this service
Server(cfg),
// interaction with this service
// infos about this service
Health(cfg),
Version(cfg),
}, UnifiedRoles(cfg)...)
}
// Execute is the entry point for the qsfera graph command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cobra.Command{
Use: "graph",
Short: "Serve Graph API for КуСфера",
})
app.AddCommand(GetCommands(cfg)...)
app.SetArgs(os.Args[1:])
return app.ExecuteContext(cfg.Context)
}
+126
View File
@@ -0,0 +1,126 @@
package command
import (
"context"
"fmt"
"os/signal"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/runner"
"github.com/qsfera/server/pkg/tracing"
"github.com/qsfera/server/pkg/version"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/config/parser"
"github.com/qsfera/server/services/graph/pkg/metrics"
"github.com/qsfera/server/services/graph/pkg/server/debug"
"github.com/qsfera/server/services/graph/pkg/server/http"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "server",
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
traceProvider, err := tracing.GetTraceProvider(cmd.Context(), cfg.Commons.TracesExporter, cfg.Service.Name)
if err != nil {
return err
}
var cancel context.CancelFunc
if cfg.Context == nil {
cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...)
defer cancel()
}
ctx := cfg.Context
mtrcs := metrics.New()
mtrcs.BuildInfo.WithLabelValues(version.GetString()).Set(1)
var kv jetstream.KeyValue
// Allow to run without a NATS store (e.g. for the standalone Education provisioning service)
if len(cfg.Store.Nodes) > 0 {
//Connect to NATS servers
natsOptions := nats.Options{
Servers: cfg.Store.Nodes,
User: cfg.Store.AuthUsername,
Password: cfg.Store.AuthPassword,
}
conn, err := natsOptions.Connect()
if err != nil {
return err
}
js, err := jetstream.New(conn)
if err != nil {
return err
}
kv, err = js.KeyValue(ctx, cfg.Store.Database)
if err != nil {
if !errors.Is(err, jetstream.ErrBucketNotFound) {
return fmt.Errorf("failed to get bucket (%s): %w", cfg.Store.Database, err)
}
kv, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{
Bucket: cfg.Store.Database,
})
if err != nil {
return fmt.Errorf("failed to create bucket (%s): %w", cfg.Store.Database, err)
}
}
}
gr := runner.NewGroup()
{
server, err := http.Server(
http.Logger(logger),
http.Context(ctx),
http.Config(cfg),
http.Metrics(mtrcs),
http.TraceProvider(traceProvider),
http.NatsKeyValue(kv),
)
if err != nil {
logger.Error().Err(err).Str("transport", "http").Msg("Failed to initialize server")
return err
}
gr.Add(runner.NewGoMicroHttpServerRunner(cfg.Service.Name+".http", server))
}
{
server, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server")
return err
}
gr.Add(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", server))
}
grResults := gr.Run(ctx)
// return the first non-nil error found in the results
for _, grResult := range grResults {
if grResult.RunnerError != nil {
return grResult.RunnerError
}
}
return nil
},
}
}
@@ -0,0 +1,103 @@
package command
import (
"os"
"slices"
"strings"
"github.com/qsfera/server/pkg/config/configlog"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/config/parser"
"github.com/qsfera/server/services/graph/pkg/unifiedrole"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"github.com/spf13/cobra"
)
var (
unifiedRolesNames = map[string]string{
unifiedrole.UnifiedRoleViewerID: "Viewer",
unifiedrole.UnifiedRoleViewerListGrantsID: "ViewerListGrants",
unifiedrole.UnifiedRoleSpaceViewerID: "SpaceViewer",
unifiedrole.UnifiedRoleEditorID: "Editor",
unifiedrole.UnifiedRoleEditorListGrantsID: "EditorListGrants",
unifiedrole.UnifiedRoleSpaceEditorID: "SpaceEditor",
unifiedrole.UnifiedRoleSpaceEditorWithoutVersionsID: "SpaceEditorWithoutVersions",
unifiedrole.UnifiedRoleFileEditorID: "FileEditor",
unifiedrole.UnifiedRoleFileEditorListGrantsID: "FileEditorListGrants",
unifiedrole.UnifiedRoleEditorLiteID: "EditorLite",
unifiedrole.UnifiedRoleManagerID: "SpaceManager",
unifiedrole.UnifiedRoleSecureViewerID: "SecureViewer",
}
)
// UnifiedRoles bundles available commands for unified roles
func UnifiedRoles(cfg *config.Config) []*cobra.Command {
cmds := []*cobra.Command{
listUnifiedRoles(cfg),
}
for _, cmd := range cmds {
cmd.Use = strings.Join([]string{cmd.Use, "unified-roles"}, "-")
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
return configlog.ReturnError(parser.ParseConfig(cfg))
}
}
return cmds
}
// unifiedRolesStatus lists available unified roles, it contains an indicator to show if the role is enabled or not
func listUnifiedRoles(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "list available unified roles",
RunE: func(cmd *cobra.Command, args []string) error {
r := tw.Rendition{
Settings: tw.Settings{
Separators: tw.Separators{
BetweenRows: tw.On,
},
},
}
tbl := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(r)))
headers := []string{"Name", "UID", "Enabled", "Description", "Condition", "Allowed resource actions"}
tbl.Header(headers)
for _, definition := range unifiedrole.GetRoles(unifiedrole.RoleFilterAll()) {
const enabled = "enabled"
const disabled = "disabled"
rows := [][]string{
{unifiedRolesNames[definition.GetId()], definition.GetId(), disabled, definition.GetDescription()},
}
if slices.Contains(cfg.UnifiedRoles.AvailableRoles, definition.GetId()) {
rows[0][2] = enabled
}
for i, rolePermission := range definition.GetRolePermissions() {
actions := strings.Join(rolePermission.GetAllowedResourceActions(), "\n")
row := []string{rolePermission.GetCondition(), actions}
switch i {
case 0:
rows[0] = append(rows[0], row...)
default:
rows[0][4] = rows[0][4] + "\n" + rolePermission.GetCondition()
}
}
for _, row := range rows {
// balance the row before adding it to the table,
// this prevents the row from having empty columns.
tbl.Append(append(row, make([]string, len(headers)-len(row))...))
}
}
tbl.Render()
return nil
},
}
}
@@ -0,0 +1,50 @@
package command
import (
"fmt"
"os"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/version"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/spf13/cobra"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "print the version of this binary and the running service instances",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Version: " + version.GetString())
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.HTTP.Namespace + "." + cfg.Service.Name)
if err != nil {
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
return err
}
if len(services) == 0 {
fmt.Println("No running " + cfg.Service.Name + " service found.")
return nil
}
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Version", "Address", "Id"})
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}
@@ -0,0 +1,7 @@
package config
// Application defines the available graph application configuration.
type Application struct {
ID string `yaml:"id" env:"GRAPH_APPLICATION_ID" desc:"The КуСфера application ID shown in the graph. All app roles are tied to this ID." introductionVersion:"1.0.0"`
DisplayName string `yaml:"displayname" env:"GRAPH_APPLICATION_DISPLAYNAME" desc:"The КуСфера application name." introductionVersion:"1.0.0"`
}
+15
View File
@@ -0,0 +1,15 @@
package config
import "time"
// Cache defines the available configuration for a cache store
type Cache struct {
Store string `yaml:"store" env:"OC_CACHE_STORE;GRAPH_CACHE_STORE" desc:"The type of the cache store. Supported values are: 'memory', 'redis-sentinel', 'nats-js-kv', 'noop'. See the text description for details." introductionVersion:"1.0.0"`
Nodes []string `yaml:"nodes" env:"OC_CACHE_STORE_NODES;GRAPH_CACHE_STORE_NODES" desc:"A list of nodes to access the configured store. This has no effect when 'memory' store are configured. Note that the behaviour how nodes are used is dependent on the library of the configured store. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
Database string `yaml:"database" env:"GRAPH_CACHE_STORE_DATABASE" desc:"The database name the configured store should use." introductionVersion:"1.0.0"`
Table string `yaml:"table" env:"GRAPH_CACHE_STORE_TABLE" desc:"The database table the store should use." introductionVersion:"1.0.0"`
TTL time.Duration `yaml:"ttl" env:"OC_CACHE_TTL;GRAPH_CACHE_TTL" desc:"Time to live for cache records in the graph. Defaults to '336h' (2 weeks). See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
DisablePersistence bool `yaml:"disable_persistence" env:"OC_CACHE_DISABLE_PERSISTENCE;GRAPH_CACHE_DISABLE_PERSISTENCE" desc:"Disables persistence of the cache. Only applies when store type 'nats-js-kv' is configured. Defaults to false." introductionVersion:"1.0.0"`
AuthUsername string `yaml:"username" env:"OC_CACHE_AUTH_USERNAME;GRAPH_CACHE_AUTH_USERNAME" desc:"The username to authenticate with the cache. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"1.0.0"`
AuthPassword string `yaml:"password" env:"OC_CACHE_AUTH_PASSWORD;GRAPH_CACHE_AUTH_PASSWORD" desc:"The password to authenticate with the cache. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"1.0.0"`
}
+179
View File
@@ -0,0 +1,179 @@
package config
import (
"context"
"time"
"github.com/qsfera/server/pkg/shared"
)
// Config combines all available configuration parts.
type Config struct {
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
Service Service `yaml:"-"`
LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;GRAPH_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"`
Cache *Cache `yaml:"cache"`
Debug Debug `yaml:"debug"`
HTTP HTTP `yaml:"http"`
API API `yaml:"api"`
Reva *shared.Reva `yaml:"reva"`
TokenManager *TokenManager `yaml:"token_manager"`
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
Application Application `yaml:"application"`
Spaces Spaces `yaml:"spaces"`
Identity Identity `yaml:"identity"`
IncludeOCMSharees bool `yaml:"include_ocm_sharees" env:"OC_ENABLE_OCM;GRAPH_INCLUDE_OCM_SHAREES" desc:"Include OCM sharees when listing users." introductionVersion:"1.0.0"`
Events Events `yaml:"events"`
UnifiedRoles UnifiedRoles `yaml:"unified_roles"`
MaxConcurrency int `yaml:"max_concurrency" env:"OC_MAX_CONCURRENCY;GRAPH_MAX_CONCURRENCY" desc:"The maximum number of concurrent requests the service will handle." introductionVersion:"1.0.0"`
Keycloak Keycloak `yaml:"keycloak"`
ServiceAccount ServiceAccount `yaml:"service_account"`
Context context.Context `yaml:"-"`
Metadata Metadata `yaml:"metadata_config"`
UserSoftDeleteRetentionTime time.Duration `yaml:"user_soft_delete_retention_time" env:"GRAPH_USER_SOFT_DELETE_RETENTION_TIME" desc:"The time after which a soft-deleted user is permanently deleted. If set to 0 (default), there is no soft delete retention time and users are deleted immediately after being soft-deleted. If set to a positive value, the user will be kept in the system for that duration before being permanently deleted." introductionVersion:"4.0.0"`
Store Store `yaml:"store"`
}
type Spaces struct {
WebDavBase string `yaml:"webdav_base" env:"OC_URL;GRAPH_SPACES_WEBDAV_BASE" desc:"The public facing URL of WebDAV." introductionVersion:"1.0.0"`
WebDavPath string `yaml:"webdav_path" env:"GRAPH_SPACES_WEBDAV_PATH" desc:"The WebDAV sub-path for spaces." introductionVersion:"1.0.0"`
DefaultQuota string `yaml:"default_quota" env:"GRAPH_SPACES_DEFAULT_QUOTA" desc:"The default quota in bytes." introductionVersion:"1.0.0"`
ExtendedSpacePropertiesCacheTTL int `yaml:"extended_space_properties_cache_ttl" env:"GRAPH_SPACES_EXTENDED_SPACE_PROPERTIES_CACHE_TTL" desc:"Max TTL in seconds for the spaces property cache." introductionVersion:"1.0.0"`
UsersCacheTTL int `yaml:"users_cache_ttl" env:"GRAPH_SPACES_USERS_CACHE_TTL" desc:"Max TTL in seconds for the spaces users cache." introductionVersion:"1.0.0"`
GroupsCacheTTL int `yaml:"groups_cache_ttl" env:"GRAPH_SPACES_GROUPS_CACHE_TTL" desc:"Max TTL in seconds for the spaces groups cache." introductionVersion:"1.0.0"`
StorageUsersAddress string `yaml:"storage_users_address" env:"GRAPH_SPACES_STORAGE_USERS_ADDRESS" desc:"The address of the storage-users service." introductionVersion:"1.0.0"`
DefaultLanguage string `yaml:"default_language" env:"OC_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details." introductionVersion:"1.0.0"`
TranslationPath string `yaml:"translation_path" env:"OC_TRANSLATION_PATH;GRAPH_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details." introductionVersion:"1.0.0"`
}
type LDAP struct {
URI string `yaml:"uri" env:"OC_LDAP_URI;GRAPH_LDAP_URI" desc:"URI of the LDAP Server to connect to. Supported URI schemes are 'ldaps://' and 'ldap://'" introductionVersion:"1.0.0"`
CACert string `yaml:"cacert" env:"OC_LDAP_CACERT;GRAPH_LDAP_CACERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the LDAP service. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idm." introductionVersion:"1.0.0"`
Insecure bool `yaml:"insecure" env:"OC_LDAP_INSECURE;GRAPH_LDAP_INSECURE" desc:"Disable TLS certificate validation for the LDAP connections. Do not set this in production environments." introductionVersion:"1.0.0"`
BindDN string `yaml:"bind_dn" env:"OC_LDAP_BIND_DN;GRAPH_LDAP_BIND_DN" desc:"LDAP DN to use for simple bind authentication with the target LDAP server." introductionVersion:"1.0.0"`
BindPassword string `yaml:"bind_password" env:"OC_LDAP_BIND_PASSWORD;GRAPH_LDAP_BIND_PASSWORD" desc:"Password to use for authenticating the 'bind_dn'." introductionVersion:"1.0.0"`
UseServerUUID bool `yaml:"use_server_uuid" env:"GRAPH_LDAP_SERVER_UUID" desc:"If set to true, rely on the LDAP Server to generate a unique ID for users and groups, like when using 'entryUUID' as the user ID attribute." introductionVersion:"1.0.0"`
UsePasswordModExOp bool `yaml:"use_password_modify_exop" env:"GRAPH_LDAP_SERVER_USE_PASSWORD_MODIFY_EXOP" desc:"Use the 'Password Modify Extended Operation' for updating user passwords." introductionVersion:"1.0.0"`
WriteEnabled bool `yaml:"write_enabled" env:"OC_LDAP_SERVER_WRITE_ENABLED;GRAPH_LDAP_SERVER_WRITE_ENABLED" desc:"Allow creating, modifying and deleting LDAP users via the GRAPH API. This can only be set to 'true' when keeping default settings for the LDAP user and group attribute types (the 'OC_LDAP_USER_SCHEMA_* and 'OC_LDAP_GROUP_SCHEMA_* variables)." introductionVersion:"1.0.0"`
RefintEnabled bool `yaml:"refint_enabled" env:"GRAPH_LDAP_REFINT_ENABLED" desc:"Signals that the server has the refint plugin enabled, which makes some actions not needed." introductionVersion:"1.0.0"`
UserBaseDN string `yaml:"user_base_dn" env:"OC_LDAP_USER_BASE_DN;GRAPH_LDAP_USER_BASE_DN" desc:"Search base DN for looking up LDAP users." introductionVersion:"1.0.0"`
UserSearchScope string `yaml:"user_search_scope" env:"OC_LDAP_USER_SCOPE;GRAPH_LDAP_USER_SCOPE" desc:"LDAP search scope to use when looking up users. Supported scopes are 'base', 'one' and 'sub'." introductionVersion:"1.0.0"`
UserFilter string `yaml:"user_filter" env:"OC_LDAP_USER_FILTER;GRAPH_LDAP_USER_FILTER" desc:"LDAP filter to add to the default filters for user search like '(objectclass=qsferaUser)'." introductionVersion:"1.0.0"`
UserObjectClass string `yaml:"user_objectclass" env:"OC_LDAP_USER_OBJECTCLASS;GRAPH_LDAP_USER_OBJECTCLASS" desc:"The object class to use for users in the default user search filter ('inetOrgPerson')." introductionVersion:"1.0.0"`
UserEmailAttribute string `yaml:"user_mail_attribute" env:"OC_LDAP_USER_SCHEMA_MAIL;GRAPH_LDAP_USER_EMAIL_ATTRIBUTE" desc:"LDAP Attribute to use for the email address of users." introductionVersion:"1.0.0"`
UserDisplayNameAttribute string `yaml:"user_displayname_attribute" env:"OC_LDAP_USER_SCHEMA_DISPLAYNAME;GRAPH_LDAP_USER_DISPLAYNAME_ATTRIBUTE" desc:"LDAP Attribute to use for the display name of users." introductionVersion:"1.0.0"`
UserNameAttribute string `yaml:"user_name_attribute" env:"OC_LDAP_USER_SCHEMA_USERNAME;GRAPH_LDAP_USER_NAME_ATTRIBUTE" desc:"LDAP Attribute to use for username of users." introductionVersion:"1.0.0"`
UserIDAttribute string `yaml:"user_id_attribute" env:"OC_LDAP_USER_SCHEMA_ID;GRAPH_LDAP_USER_UID_ATTRIBUTE" desc:"LDAP Attribute to use as the unique ID for users. This should be a stable globally unique ID like a UUID." introductionVersion:"1.0.0"`
UserIDIsOctetString bool `yaml:"user_id_is_octet_string" env:"OC_LDAP_USER_SCHEMA_ID_IS_OCTETSTRING;GRAPH_LDAP_USER_SCHEMA_ID_IS_OCTETSTRING" desc:"Set this to true if the defined 'ID' attribute for users is of the 'OCTETSTRING' syntax. This is required when using the 'objectGUID' attribute of Active Directory for the user ID's." introductionVersion:"1.0.0"`
UserTypeAttribute string `yaml:"user_type_attribute" env:"OC_LDAP_USER_SCHEMA_USER_TYPE;GRAPH_LDAP_USER_TYPE_ATTRIBUTE" desc:"LDAP Attribute to distinguish between 'Member' and 'Guest' users. Default is 'qsferaUserType'." introductionVersion:"1.0.0"`
UserEnabledAttribute string `yaml:"user_enabled_attribute" env:"OC_LDAP_USER_ENABLED_ATTRIBUTE;GRAPH_USER_ENABLED_ATTRIBUTE" desc:"LDAP Attribute to use as a flag telling if the user is enabled or disabled." introductionVersion:"1.0.0"`
DisableUserMechanism string `yaml:"disable_user_mechanism" env:"OC_LDAP_DISABLE_USER_MECHANISM;GRAPH_DISABLE_USER_MECHANISM" desc:"An option to control the behavior for disabling users. Supported options are 'none', 'attribute' and 'group'. If set to 'group', disabling a user via API will add the user to the configured group for disabled users, if set to 'attribute' this will be done in the ldap user entry, if set to 'none' the disable request is not processed. Default is 'attribute'." introductionVersion:"1.0.0"`
LdapDisabledUsersGroupDN string `yaml:"ldap_disabled_users_group_dn" env:"OC_LDAP_DISABLED_USERS_GROUP_DN;GRAPH_DISABLED_USERS_GROUP_DN" desc:"The distinguished name of the group to which added users will be classified as disabled when 'disable_user_mechanism' is set to 'group'." introductionVersion:"1.0.0"`
GroupBaseDN string `yaml:"group_base_dn" env:"OC_LDAP_GROUP_BASE_DN;GRAPH_LDAP_GROUP_BASE_DN" desc:"Search base DN for looking up LDAP groups." introductionVersion:"1.0.0"`
GroupCreateBaseDN string `yaml:"group_create_base_dn" env:"GRAPH_LDAP_GROUP_CREATE_BASE_DN" desc:"Parent DN under which new groups are created. This DN needs to be subordinate to the 'GRAPH_LDAP_GROUP_BASE_DN'. This setting is only relevant when 'GRAPH_LDAP_SERVER_WRITE_ENABLED' is 'true'. It defaults to the value of 'GRAPH_LDAP_GROUP_BASE_DN'. All groups outside of this subtree are treated as readonly groups and cannot be updated." introductionVersion:"1.0.0"`
GroupSearchScope string `yaml:"group_search_scope" env:"OC_LDAP_GROUP_SCOPE;GRAPH_LDAP_GROUP_SEARCH_SCOPE" desc:"LDAP search scope to use when looking up groups. Supported scopes are 'base', 'one' and 'sub'." introductionVersion:"1.0.0"`
GroupFilter string `yaml:"group_filter" env:"OC_LDAP_GROUP_FILTER;GRAPH_LDAP_GROUP_FILTER" desc:"LDAP filter to add to the default filters for group searches." introductionVersion:"1.0.0"`
GroupObjectClass string `yaml:"group_objectclass" env:"OC_LDAP_GROUP_OBJECTCLASS;GRAPH_LDAP_GROUP_OBJECTCLASS" desc:"The object class to use for groups in the default group search filter ('groupOfNames')." introductionVersion:"1.0.0"`
GroupNameAttribute string `yaml:"group_name_attribute" env:"OC_LDAP_GROUP_SCHEMA_GROUPNAME;GRAPH_LDAP_GROUP_NAME_ATTRIBUTE" desc:"LDAP Attribute to use for the name of groups." introductionVersion:"1.0.0"`
GroupMemberAttribute string `yaml:"group_member_attribute" env:"OC_LDAP_GROUP_SCHEMA_MEMBER;GRAPH_LDAP_GROUP_MEMBER_ATTRIBUTE" desc:"LDAP Attribute that is used for group members." introductionVersion:"1.0.0"`
GroupIDAttribute string `yaml:"group_id_attribute" env:"OC_LDAP_GROUP_SCHEMA_ID;GRAPH_LDAP_GROUP_ID_ATTRIBUTE" desc:"LDAP Attribute to use as the unique id for groups. This should be a stable globally unique ID like a UUID." introductionVersion:"1.0.0"`
GroupIDIsOctetString bool `yaml:"group_id_is_octet_string" env:"OC_LDAP_GROUP_SCHEMA_ID_IS_OCTETSTRING;GRAPH_LDAP_GROUP_SCHEMA_ID_IS_OCTETSTRING" desc:"Set this to true if the defined 'ID' attribute for groups is of the 'OCTETSTRING' syntax. This is required when using the 'objectGUID' attribute of Active Directory for the group ID's." introductionVersion:"1.0.0"`
EducationResourcesEnabled bool `yaml:"education_resources_enabled" env:"GRAPH_LDAP_EDUCATION_RESOURCES_ENABLED" desc:"Enable LDAP support for managing education related resources." introductionVersion:"1.0.0"`
EducationConfig LDAPEducationConfig
}
// LDAPEducationConfig represents the LDAP configuration for education related resources
type LDAPEducationConfig struct {
SchoolBaseDN string `yaml:"school_base_dn" env:"GRAPH_LDAP_SCHOOL_BASE_DN" desc:"Search base DN for looking up LDAP schools." introductionVersion:"1.0.0"`
SchoolSearchScope string `yaml:"school_search_scope" env:"GRAPH_LDAP_SCHOOL_SEARCH_SCOPE" desc:"LDAP search scope to use when looking up schools. Supported scopes are 'base', 'one' and 'sub'." introductionVersion:"1.0.0"`
SchoolFilter string `yaml:"school_filter" env:"GRAPH_LDAP_SCHOOL_FILTER" desc:"LDAP filter to add to the default filters for school searches." introductionVersion:"1.0.0"`
SchoolObjectClass string `yaml:"school_objectclass" env:"GRAPH_LDAP_SCHOOL_OBJECTCLASS" desc:"The object class to use for schools in the default school search filter." introductionVersion:"1.0.0"`
SchoolNameAttribute string `yaml:"school_name_attribute" env:"GRAPH_LDAP_SCHOOL_NAME_ATTRIBUTE" desc:"LDAP Attribute to use for the name of a school." introductionVersion:"1.0.0"`
SchoolNumberAttribute string `yaml:"school_number_attribute" env:"GRAPH_LDAP_SCHOOL_NUMBER_ATTRIBUTE" desc:"LDAP Attribute to use for the number of a school." introductionVersion:"1.0.0"`
SchoolIDAttribute string `yaml:"school_id_attribute" env:"GRAPH_LDAP_SCHOOL_ID_ATTRIBUTE" desc:"LDAP Attribute to use as the unique id for schools. This should be a stable globally unique ID like a UUID." introductionVersion:"1.0.0"`
SchoolTerminationGraceDays int `yaml:"school_termination_min_grace_days" env:"GRAPH_LDAP_SCHOOL_TERMINATION_MIN_GRACE_DAYS" desc:"When setting a 'terminationDate' for a school, require the date to be at least this number of days in the future." introductionVersion:"1.0.0"`
}
type Identity struct {
Backend string `yaml:"backend" env:"GRAPH_IDENTITY_BACKEND" desc:"The user identity backend to use. Supported backend types are 'ldap' and 'cs3'." introductionVersion:"1.0.0"`
LDAP LDAP `yaml:"ldap"`
}
// API represents API configuration parameters.
type API struct {
GroupMembersPatchLimit int `yaml:"group_members_patch_limit" env:"GRAPH_GROUP_MEMBERS_PATCH_LIMIT" desc:"The amount of group members allowed to be added with a single patch request." introductionVersion:"1.0.0"`
UsernameMatch string `yaml:"graph_username_match" env:"GRAPH_USERNAME_MATCH" desc:"Apply restrictions to usernames. Supported values are 'default' and 'none'. When set to 'default', user names must not start with a number and are restricted to ASCII characters. When set to 'none', no restrictions are applied. The default value is 'default'." introductionVersion:"1.0.0"`
AssignDefaultUserRole bool `yaml:"graph_assign_default_user_role" env:"GRAPH_ASSIGN_DEFAULT_USER_ROLE" desc:"Whether to assign newly created users the default role 'User'. Set this to 'false' if you want to assign roles manually, or if the role assignment should happen at first login. Set this to 'true' (the default) to assign the role 'User' when creating a new user." introductionVersion:"1.0.0"`
IdentitySearchMinLength int `yaml:"graph_identity_search_min_length" env:"GRAPH_IDENTITY_SEARCH_MIN_LENGTH" desc:"The minimum length the search term needs to have for unprivileged users when searching for users or groups." introductionVersion:"1.0.0"`
ShowUserEmailInResults bool `yaml:"show_email_in_results" env:"OC_SHOW_USER_EMAIL_IN_RESULTS" desc:"Include user email addresses in responses. If absent or set to false emails will be omitted from results. Please note that admin users can always see all email addresses." introductionVersion:"1.0.0"`
}
// Events combines the configuration options for the event bus.
type Events struct {
Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT;GRAPH_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Set to a empty string to disable emitting events." introductionVersion:"1.0.0"`
Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER;GRAPH_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"1.0.0"`
TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE;GRAPH_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"1.0.0"`
TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_EVENTS_TLS_ROOT_CA_CERTIFICATE;GRAPH_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided GRAPH_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"1.0.0"`
EnableTLS bool `yaml:"enable_tls" env:"OC_EVENTS_ENABLE_TLS;GRAPH_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the КуСфера service which receives and delivers events between the services." introductionVersion:"1.0.0"`
AuthUsername string `yaml:"username" env:"OC_EVENTS_AUTH_USERNAME;GRAPH_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the КуСфера service which receives and delivers events between the services." introductionVersion:"1.0.0"`
AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD;GRAPH_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the КуСфера service which receives and delivers events between the services." introductionVersion:"1.0.0"`
}
// CORS defines the available cors configuration.
type CORS struct {
AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;GRAPH_CORS_ALLOW_ORIGINS" desc:"A list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;GRAPH_CORS_ALLOW_METHODS" desc:"A list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;GRAPH_CORS_ALLOW_HEADERS" desc:"A list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;GRAPH_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials." introductionVersion:"1.0.0"`
}
// Keycloak configuration
type Keycloak struct {
BasePath string `yaml:"base_path" env:"OC_KEYCLOAK_BASE_PATH;GRAPH_KEYCLOAK_BASE_PATH" desc:"The URL to access keycloak." introductionVersion:"1.0.0"`
ClientID string `yaml:"client_id" env:"OC_KEYCLOAK_CLIENT_ID;GRAPH_KEYCLOAK_CLIENT_ID" desc:"The client id to authenticate with keycloak." introductionVersion:"1.0.0"`
ClientSecret string `yaml:"client_secret" env:"OC_KEYCLOAK_CLIENT_SECRET;GRAPH_KEYCLOAK_CLIENT_SECRET" desc:"The client secret to use in authentication." introductionVersion:"1.0.0"`
ClientRealm string `yaml:"client_realm" env:"OC_KEYCLOAK_CLIENT_REALM;GRAPH_KEYCLOAK_CLIENT_REALM" desc:"The realm the client is defined in." introductionVersion:"1.0.0"`
UserRealm string `yaml:"user_realm" env:"OC_KEYCLOAK_USER_REALM;GRAPH_KEYCLOAK_USER_REALM" desc:"The realm users are defined." introductionVersion:"1.0.0"`
InsecureSkipVerify bool `yaml:"insecure_skip_verify" env:"OC_KEYCLOAK_INSECURE_SKIP_VERIFY;GRAPH_KEYCLOAK_INSECURE_SKIP_VERIFY" desc:"Disable TLS certificate validation for Keycloak connections. Do not set this in production environments." introductionVersion:"1.0.0"`
}
// ServiceAccount is the configuration for the used service account
type ServiceAccount struct {
ServiceAccountID string `yaml:"service_account_id" env:"OC_SERVICE_ACCOUNT_ID;GRAPH_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"1.0.0"`
ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;GRAPH_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"1.0.0"`
}
// Metadata configures the metadata store to use
type Metadata struct {
GatewayAddress string `yaml:"gateway_addr" env:"GRAPH_STORAGE_GATEWAY_GRPC_ADDR;STORAGE_GATEWAY_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"4.0.0"`
StorageAddress string `yaml:"storage_addr" env:"GRAPH_STORAGE_GRPC_ADDR;STORAGE_GRPC_ADDR" desc:"GRPC address of the STORAGE-SYSTEM service." introductionVersion:"4.0.0"`
SystemUserID string `yaml:"system_user_id" env:"OC_SYSTEM_USER_ID;GRAPH_SYSTEM_USER_ID" desc:"ID of the КуСфера STORAGE-SYSTEM system user. Admins need to set the ID for the STORAGE-SYSTEM system user in this config option which is then used to reference the user. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"4.0.0"`
SystemUserIDP string `yaml:"system_user_idp" env:"OC_SYSTEM_USER_IDP;GRAPH_SYSTEM_USER_IDP" desc:"IDP of the КуСфера STORAGE-SYSTEM system user." introductionVersion:"4.0.0"`
SystemUserAPIKey string `yaml:"system_user_api_key" env:"OC_SYSTEM_USER_API_KEY" desc:"API key for the STORAGE-SYSTEM system user." introductionVersion:"4.0.0"`
}
// Store configures the store to use
type Store struct {
Nodes []string `yaml:"nodes" env:"OC_PERSISTENT_STORE_NODES;GRAPH_STORE_NODES" desc:"A list of nodes to access the configured store. This has no effect when 'memory' store is configured. Note that the behaviour how nodes are used is dependent on the library of the configured store. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
Database string `yaml:"database" env:"GRAPH_STORE_DATABASE" desc:"The database name the configured store should use." introductionVersion:"1.0.0"`
AuthUsername string `yaml:"username" env:"OC_PERSISTENT_STORE_AUTH_USERNAME;GRAPH_STORE_AUTH_USERNAME" desc:"The username to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"1.0.0"`
AuthPassword string `yaml:"password" env:"OC_PERSISTENT_STORE_AUTH_PASSWORD;GRAPH_STORE_AUTH_PASSWORD" desc:"The password to authenticate with the store. Only applies when store type 'nats-js-kv' is configured." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,9 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `yaml:"addr" env:"GRAPH_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"`
Token string `yaml:"token" env:"GRAPH_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"`
Pprof bool `yaml:"pprof" env:"GRAPH_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"`
Zpages bool `yaml:"zpages" env:"GRAPH_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,208 @@
package defaults
import (
"path"
"strings"
"time"
"github.com/qsfera/server/pkg/config/defaults"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/pkg/structs"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/unifiedrole"
)
var (
// _disabledByDefaultUnifiedRoleRoleIDs contains all roles that are not enabled by default,
// but can be enabled by the user.
_disabledByDefaultUnifiedRoleRoleIDs = []string{
unifiedrole.UnifiedRoleSecureViewerID,
unifiedrole.UnifiedRoleSpaceEditorWithoutVersionsID,
unifiedrole.UnifiedRoleViewerListGrantsID,
unifiedrole.UnifiedRoleEditorListGrantsID,
unifiedrole.UnifiedRoleFileEditorListGrantsID,
unifiedrole.UnifiedRoleDeniedID,
}
)
// FullDefaultConfig returns a fully initialized default configuration
func FullDefaultConfig() *config.Config {
cfg := DefaultConfig()
EnsureDefaults(cfg)
Sanitize(cfg)
return cfg
}
// DefaultConfig returns a basic default configuration
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9124",
Token: "",
},
HTTP: config.HTTP{
Addr: "127.0.0.1:9120",
Namespace: "qsfera.web",
Root: "/graph",
CORS: config.CORS{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Purge", "Restore"},
AllowCredentials: true,
},
},
Service: config.Service{
Name: "graph",
},
Application: config.Application{
DisplayName: "КуСфера",
},
API: config.API{
GroupMembersPatchLimit: 20,
UsernameMatch: "default",
AssignDefaultUserRole: true,
IdentitySearchMinLength: 3,
},
Reva: shared.DefaultRevaConfig(),
Spaces: config.Spaces{
StorageUsersAddress: "qsfera.api.storage-users",
WebDavBase: "https://localhost:9200",
WebDavPath: "/dav/spaces/",
DefaultQuota: "1000000000",
// 1 minute
ExtendedSpacePropertiesCacheTTL: 60,
// 1 minute
GroupsCacheTTL: 60,
// 1 minute
UsersCacheTTL: 60,
},
Identity: config.Identity{
Backend: "ldap",
LDAP: config.LDAP{
URI: "ldaps://localhost:9235",
Insecure: false,
CACert: path.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
BindDN: "uid=libregraph,ou=sysusers,o=libregraph-idm",
UseServerUUID: false,
UsePasswordModExOp: true,
WriteEnabled: true,
UserBaseDN: "ou=users,o=libregraph-idm",
UserSearchScope: "sub",
UserFilter: "",
UserObjectClass: "inetOrgPerson",
UserEmailAttribute: "mail",
UserDisplayNameAttribute: "displayName",
UserNameAttribute: "uid",
// FIXME: switch this to some more widely available attribute by default
// ideally this needs to be constant for the lifetime of a users
UserIDAttribute: "qsferaUUID",
UserTypeAttribute: "qsferaUserType",
UserEnabledAttribute: "qsferaUserEnabled",
DisableUserMechanism: "attribute",
LdapDisabledUsersGroupDN: "cn=DisabledUsersGroup,ou=groups,o=libregraph-idm",
GroupBaseDN: "ou=groups,o=libregraph-idm",
GroupSearchScope: "sub",
GroupFilter: "",
GroupObjectClass: "groupOfNames",
GroupNameAttribute: "cn",
GroupMemberAttribute: "member",
GroupIDAttribute: "qsferaUUID",
EducationResourcesEnabled: false,
},
},
Cache: &config.Cache{
Store: "memory",
Nodes: []string{"127.0.0.1:9233"},
Database: "cache-roles",
TTL: time.Hour * 24,
},
Events: config.Events{
Endpoint: "127.0.0.1:9233",
Cluster: "qsfera-cluster",
EnableTLS: false,
},
MaxConcurrency: 20,
UnifiedRoles: config.UnifiedRoles{
AvailableRoles: nil, // will be populated with defaults in EnsureDefaults
},
Metadata: config.Metadata{
GatewayAddress: "qsfera.api.storage-system",
StorageAddress: "qsfera.api.storage-system",
SystemUserIDP: "internal",
},
UserSoftDeleteRetentionTime: 0,
Store: config.Store{
Nodes: []string{"127.0.0.1:9233"},
Database: "graph",
},
}
}
// EnsureDefaults adds default values to the configuration if they are not set yet
func EnsureDefaults(cfg *config.Config) {
if cfg.LogLevel == "" {
cfg.LogLevel = "error"
}
if cfg.Cache == nil && cfg.Commons != nil && cfg.Commons.Cache != nil {
cfg.Cache = &config.Cache{
Store: cfg.Commons.Cache.Store,
Nodes: cfg.Commons.Cache.Nodes,
}
} else if cfg.Cache == nil {
cfg.Cache = &config.Cache{}
}
if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil {
cfg.TokenManager = &config.TokenManager{
JWTSecret: cfg.Commons.TokenManager.JWTSecret,
}
} else if cfg.TokenManager == nil {
cfg.TokenManager = &config.TokenManager{}
}
if cfg.GRPCClientTLS == nil && cfg.Commons != nil {
cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS)
}
if cfg.Commons != nil {
cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS
}
if cfg.Identity.LDAP.GroupCreateBaseDN == "" {
cfg.Identity.LDAP.GroupCreateBaseDN = cfg.Identity.LDAP.GroupBaseDN
}
// set default roles, if no roles are defined, we need to take care and provide all the default roles
if len(cfg.UnifiedRoles.AvailableRoles) == 0 {
for _, definition := range unifiedrole.GetRoles(
// filter out the roles that are disabled by default
unifiedrole.RoleFilterInvert(unifiedrole.RoleFilterIDs(_disabledByDefaultUnifiedRoleRoleIDs...)),
) {
cfg.UnifiedRoles.AvailableRoles = append(cfg.UnifiedRoles.AvailableRoles, definition.GetId())
}
}
if cfg.Metadata.SystemUserAPIKey == "" && cfg.Commons != nil && cfg.Commons.SystemUserAPIKey != "" {
cfg.Metadata.SystemUserAPIKey = cfg.Commons.SystemUserAPIKey
}
if cfg.Metadata.SystemUserID == "" && cfg.Commons != nil && cfg.Commons.SystemUserID != "" {
cfg.Metadata.SystemUserID = cfg.Commons.SystemUserID
}
}
// Sanitize sanitized the configuration
func Sanitize(cfg *config.Config) {
// sanitize config
if cfg.HTTP.Root != "/" {
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
}
// convert ttl to millisecond
// the config is in seconds, therefore we need multiply it.
cfg.Spaces.ExtendedSpacePropertiesCacheTTL = cfg.Spaces.ExtendedSpacePropertiesCacheTTL * int(time.Second)
cfg.Spaces.GroupsCacheTTL = cfg.Spaces.GroupsCacheTTL * int(time.Second)
cfg.Spaces.UsersCacheTTL = cfg.Spaces.UsersCacheTTL * int(time.Second)
}
+13
View File
@@ -0,0 +1,13 @@
package config
import "github.com/qsfera/server/pkg/shared"
// HTTP defines the available http configuration.
type HTTP struct {
Addr string `yaml:"addr" env:"GRAPH_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"`
Namespace string `yaml:"-"`
Root string `yaml:"root" env:"GRAPH_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"`
TLS shared.HTTPServiceTLS `yaml:"tls"`
APIToken string `yaml:"apitoken" env:"GRAPH_HTTP_API_TOKEN" desc:"An optional API bearer token" introductionVersion:"1.0.0"`
CORS CORS `yaml:"cors"`
}
@@ -0,0 +1,126 @@
package parser
import (
"errors"
"fmt"
"slices"
"github.com/go-ldap/ldap/v3"
occfg "github.com/qsfera/server/pkg/config"
defaults2 "github.com/qsfera/server/pkg/config/defaults"
"github.com/qsfera/server/pkg/config/envdecode"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/config/defaults"
"github.com/qsfera/server/services/graph/pkg/unifiedrole"
)
// ParseConfig loads configuration from known paths.
func ParseConfig(cfg *config.Config) error {
err := occfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
defaults.EnsureDefaults(cfg)
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
defaults.Sanitize(cfg)
return Validate(cfg)
}
func Validate(cfg *config.Config) error {
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
if !slices.Contains([]string{"ldap", "cs3"}, cfg.Identity.Backend) {
return fmt.Errorf("'%s' is not a valid identity backend for the 'graph' service", cfg.Identity.Backend)
}
// ensure that the "cs3" identity backend is used in multi-tenant setups
if cfg.Commons.MultiTenantEnabled && cfg.Identity.Backend != "cs3" {
return fmt.Errorf("Multi-tenant support is enabled. The identity backend must be set to 'cs3' for the 'graph' service.")
}
if cfg.Identity.Backend == "ldap" {
if err := validateLDAPSettings(cfg); err != nil {
return err
}
}
if cfg.Application.ID == "" {
return fmt.Errorf("The application ID has not been configured for %s. "+
"Make sure your %s config contains the proper values "+
"(e.g. by running qsfera init or setting it manually in "+
"the config/corresponding environment variable).",
"graph", defaults2.BaseConfigPath())
}
switch cfg.API.UsernameMatch {
case "default", "none":
default:
return fmt.Errorf("The username match validator is invalid for %s. "+
"Make sure your %s config contains the proper values "+
"(e.g. by running qsfera init or setting it manually in "+
"the config/corresponding environment variable).",
"graph", defaults2.BaseConfigPath())
}
if cfg.ServiceAccount.ServiceAccountID == "" {
return shared.MissingServiceAccountID(cfg.Service.Name)
}
if cfg.ServiceAccount.ServiceAccountSecret == "" {
return shared.MissingServiceAccountSecret(cfg.Service.Name)
}
// validate unified roles
{
var err error
for _, uid := range cfg.UnifiedRoles.AvailableRoles {
// check if the role is known
if len(unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(uid))) == 0 {
// collect all possible errors to return them all at once
err = errors.Join(err, fmt.Errorf("%w: %s", unifiedrole.ErrUnknownRole, uid))
}
}
if err != nil {
return err
}
}
return nil
}
func validateLDAPSettings(cfg *config.Config) error {
if cfg.Identity.LDAP.BindPassword == "" {
return shared.MissingLDAPBindPassword(cfg.Service.Name)
}
// ensure that "GroupBaseDN" is below "GroupBaseDN"
if cfg.Identity.LDAP.WriteEnabled && cfg.Identity.LDAP.GroupCreateBaseDN != cfg.Identity.LDAP.GroupBaseDN {
baseDN, err := ldap.ParseDN(cfg.Identity.LDAP.GroupBaseDN)
if err != nil {
return fmt.Errorf("Unable to parse the LDAP Group Base DN '%s': %w ", cfg.Identity.LDAP.GroupBaseDN, err)
}
createBaseDN, err := ldap.ParseDN(cfg.Identity.LDAP.GroupCreateBaseDN)
if err != nil {
return fmt.Errorf("Unable to parse the LDAP Group Create Base DN '%s': %w ", cfg.Identity.LDAP.GroupCreateBaseDN, err)
}
if !baseDN.AncestorOfFold(createBaseDN) {
return fmt.Errorf("The LDAP Group Create Base DN (%s) must be subordinate to the LDAP Group Base DN (%s)", cfg.Identity.LDAP.GroupCreateBaseDN, cfg.Identity.LDAP.GroupBaseDN)
}
}
return nil
}
@@ -0,0 +1,69 @@
package parser_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/config/defaults"
"github.com/qsfera/server/services/graph/pkg/config/parser"
)
var _ = Describe("Validate", func() {
var cfg *config.Config
BeforeEach(func() {
cfg = defaults.DefaultConfig()
cfg.Application.ID = "graph-app-id"
cfg.ServiceAccount.ServiceAccountID = "graph-service-account"
cfg.ServiceAccount.ServiceAccountSecret = "graph-service-password"
cfg.Commons = &shared.Commons{
TokenManager: &shared.TokenManager{
JWTSecret: "jwt-secret",
},
}
defaults.EnsureDefaults(cfg)
})
When("multi-tenant support is disabled", func() {
It("should accept a setup with the 'cs3' identity backend", func() {
cfg.Identity.Backend = "cs3"
err := parser.Validate(cfg)
Expect(err).ToNot(HaveOccurred())
})
It("should accept a setup with the 'ldap' identity backend", func() {
cfg.Identity.Backend = "ldap"
// we need to set a password to pass validation
cfg.Identity.LDAP.BindPassword = "bind-password"
err := parser.Validate(cfg)
Expect(err).ToNot(HaveOccurred())
})
})
When("multi-tenant support is disabled", func() {
BeforeEach(func() {
cfg.Commons.MultiTenantEnabled = true
})
It("should accept a setup with the 'cs3' identity backend", func() {
cfg.Identity.Backend = "cs3"
err := parser.Validate(cfg)
Expect(err).ToNot(HaveOccurred())
})
It("should reject a setup with the 'ldap' identity backend", func() {
cfg.Identity.Backend = "ldap"
cfg.Identity.LDAP.BindPassword = "bind-password"
err := parser.Validate(cfg)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(ContainSubstring("The identity backend must be set to 'cs3' for the 'graph' service.")))
})
})
It("rejcts a setup with an invalid identity backend", func() {
cfg.Identity.Backend = "invalid-backend"
err := parser.Validate(cfg)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(ContainSubstring("is not a valid identity backend")))
})
})
@@ -0,0 +1,13 @@
package parser_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestParser(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Parser Suite")
}
+6
View File
@@ -0,0 +1,6 @@
package config
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;GRAPH_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,6 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
}
@@ -0,0 +1,6 @@
package config
// UnifiedRoles contains all settings related to unified roles.
type UnifiedRoles struct {
AvailableRoles []string `yaml:"available_roles" env:"GRAPH_AVAILABLE_ROLES" desc:"A comma separated list of roles that are available for assignment." introductionVersion:"1.0.0"`
}
@@ -0,0 +1,83 @@
package errorcode
import (
"slices"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
// FromCS3Status converts a CS3 status code and an error into a corresponding local Error representation.
//
// It takes a *cs3rpc.Status, an error, and a variadic parameter of type cs3rpc.Code.
// If the error is not nil, it creates an Error object with the error message and a GeneralException code.
// If the error is nil, it evaluates the provided CS3 status code and returns an equivalent graph Error.
// If the CS3 status code does not have a direct equivalent within the app,
// or is ignored, a general purpose Error is returned.
//
// This function is particularly useful when dealing with CS3 responses,
// and a unified error handling within the application is necessary.
func FromCS3Status(status *cs3rpc.Status, inerr error, ignore ...cs3rpc.Code) error {
err := Error{errorCode: GeneralException, msg: "unspecified error has occurred", origin: ErrorOriginCS3}
if inerr != nil {
err.msg = inerr.Error()
return err
}
if status != nil {
err.msg = status.GetMessage()
}
code := status.GetCode()
switch {
case slices.Contains(ignore, status.GetCode()):
fallthrough
case code == cs3rpc.Code_CODE_OK:
return nil
case code == cs3rpc.Code_CODE_NOT_FOUND:
err.errorCode = ItemNotFound
case code == cs3rpc.Code_CODE_PERMISSION_DENIED:
err.errorCode = AccessDenied
case code == cs3rpc.Code_CODE_UNAUTHENTICATED:
err.errorCode = Unauthenticated
case code == cs3rpc.Code_CODE_INVALID_ARGUMENT:
err.errorCode = InvalidRequest
case code == cs3rpc.Code_CODE_ALREADY_EXISTS:
err.errorCode = NameAlreadyExists
case code == cs3rpc.Code_CODE_FAILED_PRECONDITION:
err.errorCode = InvalidRequest
case code == cs3rpc.Code_CODE_OUT_OF_RANGE:
err.errorCode = InvalidRange
case code == cs3rpc.Code_CODE_UNIMPLEMENTED:
err.errorCode = NotSupported
case code == cs3rpc.Code_CODE_UNAVAILABLE:
err.errorCode = ServiceNotAvailable
case code == cs3rpc.Code_CODE_INSUFFICIENT_STORAGE:
err.errorCode = QuotaLimitReached
case code == cs3rpc.Code_CODE_LOCKED:
err.errorCode = ItemIsLocked
}
return err
}
// FromStat transforms a *provider.StatResponse object and an error into an Error.
//
// It takes a stat of type *provider.StatResponse, an error, and a variadic parameter of type cs3rpc.Code.
// It invokes the FromCS3Status function with the StatResponse Status and the ignore codes.
func FromStat(stat *provider.StatResponse, err error, ignore ...cs3rpc.Code) error {
// TODO: look into ResourceInfo to get the postprocessing state and map that to 425 status?
return FromCS3Status(stat.GetStatus(), err, ignore...)
}
// FromUtilsStatusCodeError returns original error if `err` does not match to the statusCodeError type
func FromUtilsStatusCodeError(err error, ignore ...cs3rpc.Code) error {
stat := utils.StatusCodeErrorToCS3Status(err)
if stat == nil {
return FromCS3Status(nil, err, ignore...)
}
return FromCS3Status(stat, nil, ignore...)
}
@@ -0,0 +1,73 @@
package errorcode_test
import (
"errors"
"reflect"
"testing"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/qsfera/server/services/graph/pkg/errorcode"
)
func TestFromCS3Status(t *testing.T) {
var tests = []struct {
status *cs3rpc.Status
err error
ignore []cs3rpc.Code
expected error
}{
{nil, nil, nil, errorcode.New(errorcode.GeneralException, "unspecified error has occurred")},
{nil, errors.New("test error"), nil, errorcode.New(errorcode.GeneralException, "test error")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_OK}, nil, nil, nil},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_NOT_FOUND}, nil, []cs3rpc.Code{cs3rpc.Code_CODE_NOT_FOUND}, nil},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_PERMISSION_DENIED}, nil, []cs3rpc.Code{cs3rpc.Code_CODE_NOT_FOUND, cs3rpc.Code_CODE_PERMISSION_DENIED}, nil},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_NOT_FOUND, Message: "msg"}, nil, nil, errorcode.New(errorcode.ItemNotFound, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_PERMISSION_DENIED, Message: "msg"}, nil, nil, errorcode.New(errorcode.AccessDenied, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNAUTHENTICATED, Message: "msg"}, nil, nil, errorcode.New(errorcode.Unauthenticated, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INVALID_ARGUMENT, Message: "msg"}, nil, nil, errorcode.New(errorcode.InvalidRequest, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_ALREADY_EXISTS, Message: "msg"}, nil, nil, errorcode.New(errorcode.NameAlreadyExists, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_FAILED_PRECONDITION, Message: "msg"}, nil, nil, errorcode.New(errorcode.InvalidRequest, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNIMPLEMENTED, Message: "msg"}, nil, nil, errorcode.New(errorcode.NotSupported, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INVALID, Message: "msg"}, nil, nil, errorcode.New(errorcode.GeneralException, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_CANCELLED, Message: "msg"}, nil, nil, errorcode.New(errorcode.GeneralException, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNKNOWN, Message: "msg"}, nil, nil, errorcode.New(errorcode.GeneralException, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_RESOURCE_EXHAUSTED, Message: "msg"}, nil, nil, errorcode.New(errorcode.GeneralException, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_ABORTED, Message: "msg"}, nil, nil, errorcode.New(errorcode.GeneralException, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_OUT_OF_RANGE, Message: "msg"}, nil, nil, errorcode.New(errorcode.InvalidRange, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INTERNAL, Message: "msg"}, nil, nil, errorcode.New(errorcode.GeneralException, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_UNAVAILABLE, Message: "msg"}, nil, nil, errorcode.New(errorcode.ServiceNotAvailable, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_REDIRECTION, Message: "msg"}, nil, nil, errorcode.New(errorcode.GeneralException, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_INSUFFICIENT_STORAGE, Message: "msg"}, nil, nil, errorcode.New(errorcode.QuotaLimitReached, "msg")},
{&cs3rpc.Status{Code: cs3rpc.Code_CODE_LOCKED, Message: "msg"}, nil, nil, errorcode.New(errorcode.ItemIsLocked, "msg")},
}
for _, test := range tests {
var e errorcode.Error
if errors.As(test.expected, &e) {
test.expected = e.WithOrigin(errorcode.ErrorOriginCS3)
}
if got := errorcode.FromCS3Status(test.status, test.err, test.ignore...); !reflect.DeepEqual(got, test.expected) {
t.Error("Test Failed: {} expected, received: {}", test.expected, got)
}
}
}
func TestFromStat(t *testing.T) {
var tests = []struct {
stat *provider.StatResponse
err error
result error
}{
{nil, errors.New("some error"), errorcode.New(errorcode.GeneralException, "some error").WithOrigin(errorcode.ErrorOriginCS3)},
{&provider.StatResponse{Status: &cs3rpc.Status{Code: cs3rpc.Code_CODE_OK}}, nil, nil},
}
for _, test := range tests {
if output := errorcode.FromStat(test.stat, test.err); !reflect.DeepEqual(output, test.result) {
t.Error("Test Failed: {} expected, received: {}", test.result, output)
}
}
}
@@ -0,0 +1,208 @@
// Package errorcode allows to deal with graph error codes
package errorcode
import (
"context"
"errors"
"net/http"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
// Error defines a custom error struct, containing and MS Graph error code and a textual error message
type Error struct {
errorCode ErrorCode
msg string
origin ErrorOrigin
}
// ErrorOrigin gives information about where the error originated
type ErrorOrigin int
const (
// ErrorOriginUnknown is the default error source
// and indicates that the error does not have any information about its origin
ErrorOriginUnknown ErrorOrigin = iota
// ErrorOriginCS3 indicates that the error originated from a CS3 service
ErrorOriginCS3
)
// ErrorCode defines code as used in MS Graph - see https://docs.microsoft.com/en-us/graph/errors?context=graph%2Fapi%2F1.0&view=graph-rest-1.0
type ErrorCode int
// List taken from https://github.com/microsoft/microsoft-graph-docs-1/blob/main/concepts/errors.md#code-property
const (
// AccessDenied defines the error if the caller doesn't have permission to perform the action.
AccessDenied ErrorCode = iota
// ActivityLimitReached defines the error if the app or user has been throttled.
ActivityLimitReached
// GeneralException defines the error if an unspecified error has occurred.
GeneralException
// InvalidAuthenticationToken defines the error if the access token is missing
InvalidAuthenticationToken
// InvalidRange defines the error if the specified byte range is invalid or unavailable.
InvalidRange
// InvalidRequest defines the error if the request is malformed or incorrect.
InvalidRequest
// ItemNotFound defines the error if the resource could not be found.
ItemNotFound
// MalwareDetected defines the error if malware was detected in the requested resource.
MalwareDetected
// NameAlreadyExists defines the error if the specified item name already exists.
NameAlreadyExists
// NotAllowed defines the error if the action is not allowed by the system.
NotAllowed
// NotSupported defines the error if the request is not supported by the system.
NotSupported
// ResourceModified defines the error if the resource being updated has changed since the caller last read it, usually an eTag mismatch.
ResourceModified
// ResyncRequired defines the error if the delta token is no longer valid, and the app must reset the sync state.
ResyncRequired
// ServiceNotAvailable defines the error if the service is not available. Try the request again after a delay. There may be a Retry-After header.
ServiceNotAvailable
// SyncStateNotFound defines the error when the sync state generation is not found. The delta token is expired and data must be synchronized again.
SyncStateNotFound
// QuotaLimitReached the user has reached their quota limit.
QuotaLimitReached
// Unauthenticated the caller is not authenticated.
Unauthenticated
// PreconditionFailed the request cannot be made and this error response is sent back
PreconditionFailed
// ItemIsLocked The item is locked by another process. Try again later.
ItemIsLocked
)
var errorCodes = [...]string{
"accessDenied",
"activityLimitReached",
"generalException",
"InvalidAuthenticationToken",
"invalidRange",
"invalidRequest",
"itemNotFound",
"malwareDetected",
"nameAlreadyExists",
"notAllowed",
"notSupported",
"resourceModified",
"resyncRequired",
"serviceNotAvailable",
"syncStateNotFound",
"quotaLimitReached",
"unauthenticated",
"preconditionFailed",
"itemIsLocked",
}
// New constructs a new errorcode.Error
func New(e ErrorCode, msg string) Error {
return Error{
errorCode: e,
msg: msg,
}
}
// Render writes a Graph ErrorCode object to the response writer
func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, msg string) {
render.Status(r, status)
render.JSON(w, r, e.CreateOdataError(r.Context(), msg))
}
// CreateOdataError creates and populates a Graph ErrorCode object
func (e ErrorCode) CreateOdataError(ctx context.Context, msg string) *libregraph.OdataError {
innererror := map[string]any{
"date": time.Now().UTC().Format(time.RFC3339),
}
innererror["request-id"] = middleware.GetReqID(ctx)
return &libregraph.OdataError{
Error: libregraph.OdataErrorMain{
Code: e.String(),
Message: msg,
Innererror: innererror,
},
}
}
// Render writes a Graph Error object to the response writer
func (e Error) Render(w http.ResponseWriter, r *http.Request) {
var status int
switch e.errorCode {
case AccessDenied:
status = http.StatusForbidden
case NotSupported:
status = http.StatusNotImplemented
case InvalidRange:
status = http.StatusRequestedRangeNotSatisfiable
case InvalidRequest:
status = http.StatusBadRequest
case ItemNotFound:
status = http.StatusNotFound
case NameAlreadyExists:
status = http.StatusConflict
case NotAllowed:
status = http.StatusMethodNotAllowed
case ItemIsLocked:
status = http.StatusLocked
case PreconditionFailed:
status = http.StatusPreconditionFailed
default:
status = http.StatusInternalServerError
}
e.errorCode.Render(w, r, status, e.msg)
}
// String returns the string corresponding to the ErrorCode
func (e ErrorCode) String() string {
return errorCodes[e]
}
// Error returns the concatenation of the error string and optional message
func (e Error) Error() string {
errString := errorCodes[e.errorCode]
if e.msg != "" {
errString += ": " + e.msg
}
return errString
}
func (e Error) GetCode() ErrorCode {
return e.errorCode
}
// GetOrigin returns the source of the error
func (e Error) GetOrigin() ErrorOrigin {
return e.origin
}
// WithOrigin returns a new Error with the provided origin
func (e Error) WithOrigin(o ErrorOrigin) Error {
e.origin = o
return e
}
// RenderError render the Graph Error based on a code or default one
func RenderError(w http.ResponseWriter, r *http.Request, err error) {
e, ok := ToError(err)
if !ok {
GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
e.Render(w, r)
}
// ToError checks if the error is of type Error and returns it,
// the second parameter indicates if the error conversion was successful
func ToError(err error) (Error, bool) {
var e Error
if errors.As(err, &e) {
return e, true
}
return Error{}, false
}
@@ -0,0 +1,45 @@
package errorcode_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
"github.com/qsfera/server/services/graph/pkg/errorcode"
)
type customErr struct{}
func (customErr) Error() string {
return "some error"
}
func TestRenderError(t *testing.T) {
t.Parallel()
t.Run("errorcode.Error value error", func(t *testing.T) {
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
err := errorcode.New(errorcode.ItemNotFound, "test error")
errorcode.RenderError(w, r, err)
require.Equal(t, http.StatusNotFound, w.Code)
})
t.Run("errorcode.Error zero value error", func(t *testing.T) {
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
var err errorcode.Error
errorcode.RenderError(w, r, err)
require.Equal(t, http.StatusForbidden, w.Code)
})
t.Run("custom error", func(t *testing.T) {
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
var err customErr
errorcode.RenderError(w, r, err)
require.Equal(t, http.StatusInternalServerError, w.Code)
})
}
@@ -0,0 +1,168 @@
package identity
import (
"context"
"net/url"
"time"
"github.com/CiscoM31/godata"
cs3group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/errorcode"
)
// Errors used by the interfaces
var (
// ErrReadOnly signals that the backend is set to read only.
ErrReadOnly = errorcode.New(errorcode.NotAllowed, "server is configured read-only")
// ErrNotFound signals that the requested resource was not found.
ErrNotFound = errorcode.New(errorcode.ItemNotFound, "not found")
// ErrUnsupportedFilter signals that the requested filter is not supported by the backend.
ErrUnsupportedFilter = godata.NotImplementedError("unsupported filter")
)
const (
UserTypeMember = "Member"
UserTypeGuest = "Guest"
UserTypeFederated = "Federated"
)
// Backend defines the Interface for an IdentityBackend implementation
type Backend interface {
// CreateUser creates a given user in the identity backend.
CreateUser(ctx context.Context, user libregraph.User) (*libregraph.User, error)
// DeleteUser deletes a given user, identified by username or id, from the backend
DeleteUser(ctx context.Context, nameOrID string) error
// UpdateUser applies changes to given user, identified by username or id
UpdateUser(ctx context.Context, nameOrID string, user libregraph.UserUpdate) (*libregraph.User, error)
GetUser(ctx context.Context, nameOrID string, oreq *godata.GoDataRequest) (*libregraph.User, error)
GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.User, error)
// FilterUsers returns a list of users that match the filter
FilterUsers(ctx context.Context, oreq *godata.GoDataRequest, filter *godata.ParseNode) ([]*libregraph.User, error)
UpdateLastSignInDate(ctx context.Context, userID string, timestamp time.Time) error
// CreateGroup creates the supplied group in the identity backend.
CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error)
// DeleteGroup deletes a given group, identified by id
DeleteGroup(ctx context.Context, id string) error
// UpdateGroupName updates the group name
UpdateGroupName(ctx context.Context, groupID string, groupName string) error
GetGroup(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.Group, error)
GetGroups(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.Group, error)
// GetGroupMembers list all members of a group
GetGroupMembers(ctx context.Context, id string, oreq *godata.GoDataRequest) ([]*libregraph.User, error)
// AddMembersToGroup adds new members (reference by a slice of IDs) to supplied group in the identity backend.
AddMembersToGroup(ctx context.Context, groupID string, memberID []string) error
// RemoveMemberFromGroup removes a single member (by ID) from a group
RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error
}
// EducationBackend defines the Interface for an EducationBackend implementation
type EducationBackend interface {
// CreateEducationSchool creates the supplied school in the identity backend.
CreateEducationSchool(ctx context.Context, group libregraph.EducationSchool) (*libregraph.EducationSchool, error)
// DeleteEducationSchool deletes a given school, identified by id
DeleteEducationSchool(ctx context.Context, id string) error
// GetEducationSchool reads a given school by id
GetEducationSchool(ctx context.Context, nameOrID string) (*libregraph.EducationSchool, error)
// GetEducationSchools lists all schools
GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error)
// FilterEducationSchoolsByAttribute list all schools where an attribute matches a value, e.g. all schools with a given externalId
FilterEducationSchoolsByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationSchool, error)
// UpdateEducationSchool updates attributes of a school
UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error)
// GetEducationSchoolUsers lists all members of a school
GetEducationSchoolUsers(ctx context.Context, id string) ([]*libregraph.EducationUser, error)
// AddUsersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
AddUsersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error
// RemoveUserFromEducationSchool removes a single member (by ID) from a school
RemoveUserFromEducationSchool(ctx context.Context, schoolID string, memberID string) error
// GetEducationSchoolClasses lists all classes in a school
GetEducationSchoolClasses(ctx context.Context, schoolNumberOrID string) ([]*libregraph.EducationClass, error)
// AddClassesToEducationSchool adds new classes (referenced by a slice of IDs) to supplied school in the identity backend.
AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error
// RemoveClassFromEducationSchool removes a class from a school.
RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error
// GetEducationClasses lists all classes
GetEducationClasses(ctx context.Context) ([]*libregraph.EducationClass, error)
// GetEducationClass reads a given class by id
GetEducationClass(ctx context.Context, namedOrID string) (*libregraph.EducationClass, error)
// CreateEducationClass creates the supplied education class in the identity backend.
CreateEducationClass(ctx context.Context, class libregraph.EducationClass) (*libregraph.EducationClass, error)
// DeleteEducationClass deletes the supplied education class in the identity backend.
DeleteEducationClass(ctx context.Context, nameOrID string) error
// GetEducationClassMembers returns the EducationUser members for an EducationClass
GetEducationClassMembers(ctx context.Context, nameOrID string) ([]*libregraph.EducationUser, error)
// UpdateEducationClass updates properties of the supplied class in the identity backend.
UpdateEducationClass(ctx context.Context, id string, class libregraph.EducationClass) (*libregraph.EducationClass, error)
// CreateEducationUser creates a given education user in the identity backend.
CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error)
// DeleteEducationUser deletes a given education user, identified by username or id, from the backend
DeleteEducationUser(ctx context.Context, nameOrID string) error
// UpdateEducationUser applies changes to given education user, identified by username or id
UpdateEducationUser(ctx context.Context, nameOrID string, user libregraph.EducationUser) (*libregraph.EducationUser, error)
// GetEducationUser reads an education user by id or name
GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error)
// GetEducationUsers lists all education users
GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error)
// FilterEducationUsersByAttribute list all education users where and attribute matches a value, e.g. all users with a given externalid
FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error)
// GetEducationClassTeachers returns the EducationUser teachers for an EducationClass
GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error)
// AddTeacherToEducationClass adds a teacher (by ID) to class in the identity backend.
AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error
// RemoveTeacherFromEducationClass removes teacher (by ID) from a class
RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error
}
// CreateUserModelFromCS3 converts a cs3 User object into a libregraph.User
func CreateUserModelFromCS3(u *cs3user.User) *libregraph.User {
if u.GetId() == nil {
u.Id = &cs3user.UserId{}
}
userType := CS3UserTypeToGraph(u.GetId().GetType())
user := &libregraph.User{
Identities: []libregraph.ObjectIdentity{{
Issuer: &u.GetId().Idp,
IssuerAssignedId: &u.GetId().OpaqueId,
}},
UserType: &userType,
DisplayName: u.GetDisplayName(),
Mail: &u.Mail,
OnPremisesSamAccountName: u.GetUsername(),
Id: &u.GetId().OpaqueId,
}
if u.GetId().GetType() == cs3user.UserType_USER_TYPE_FEDERATED {
ocmUserId := u.GetId().GetOpaqueId() + "@" + u.GetId().GetIdp()
user.Id = &ocmUserId
}
return user
}
func CS3UserTypeToGraph(cs3type cs3user.UserType) string {
switch cs3type {
case cs3user.UserType_USER_TYPE_PRIMARY:
return UserTypeMember
case cs3user.UserType_USER_TYPE_FEDERATED:
return UserTypeFederated
case cs3user.UserType_USER_TYPE_GUEST:
return UserTypeGuest
}
return "unknown"
}
// CreateGroupModelFromCS3 converts a cs3 Group object into a libregraph.Group
func CreateGroupModelFromCS3(g *cs3group.Group) *libregraph.Group {
if g.GetId() == nil {
g.Id = &cs3group.GroupId{}
}
return &libregraph.Group{
Id: &g.Id.OpaqueId,
DisplayName: &g.GroupName,
}
}
+215
View File
@@ -0,0 +1,215 @@
package cache
import (
"context"
"errors"
"strings"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3Group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/jellydator/ttlcache/v3"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/errorcode"
"github.com/qsfera/server/services/graph/pkg/identity"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
revautils "github.com/opencloud-eu/reva/v2/pkg/utils"
)
// IdentityCache implements a simple ttl based cache for looking up users and groups by ID
type IdentityCache struct {
users *ttlcache.Cache[string, *cs3User.User]
groups *ttlcache.Cache[string, libregraph.Group]
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
}
type identityCacheOptions struct {
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
usersTTL time.Duration
groupsTTL time.Duration
}
// IdentityCacheOption defines a single option function.
type IdentityCacheOption func(o *identityCacheOptions)
// IdentityCacheWithGatewaySelector set the gatewaySelector for the Identity Cache
func IdentityCacheWithGatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) IdentityCacheOption {
return func(o *identityCacheOptions) {
o.gatewaySelector = gatewaySelector
}
}
// IdentityCacheWithUsersTTL sets the TTL for the users cache
func IdentityCacheWithUsersTTL(ttl time.Duration) IdentityCacheOption {
return func(o *identityCacheOptions) {
o.usersTTL = ttl
}
}
// IdentityCacheWithGroupsTTL sets the TTL for the groups cache
func IdentityCacheWithGroupsTTL(ttl time.Duration) IdentityCacheOption {
return func(o *identityCacheOptions) {
o.groupsTTL = ttl
}
}
func newOptions(opts ...IdentityCacheOption) identityCacheOptions {
opt := identityCacheOptions{}
for _, o := range opts {
o(&opt)
}
return opt
}
// NewIdentityCache instantiates a new IdentityCache and sets the supplied options
func NewIdentityCache(opts ...IdentityCacheOption) IdentityCache {
opt := newOptions(opts...)
var cache IdentityCache
cache.users = ttlcache.New(
ttlcache.WithTTL[string, *cs3user.User](opt.usersTTL),
ttlcache.WithDisableTouchOnHit[string, *cs3user.User](),
)
go cache.users.Start()
cache.groups = ttlcache.New(
ttlcache.WithTTL[string, libregraph.Group](opt.groupsTTL),
ttlcache.WithDisableTouchOnHit[string, libregraph.Group](),
)
go cache.groups.Start()
cache.gatewaySelector = opt.gatewaySelector
return cache
}
// GetUser looks up a user by id, if the user is not cached, yet it will do a lookup via the CS3 API
func (cache IdentityCache) GetUser(ctx context.Context, tenantId, userid string) (libregraph.User, error) {
// can we get the tenant from the context or do we have to pass it?
u, err := cache.GetCS3User(ctx, tenantId, userid)
if err != nil {
return libregraph.User{}, err
}
if tenantId != u.GetId().GetTenantId() {
return libregraph.User{}, identity.ErrNotFound
}
return *identity.CreateUserModelFromCS3(u), nil
}
func (cache IdentityCache) GetCS3User(ctx context.Context, tenantId, userid string) (*cs3User.User, error) {
var user *cs3User.User
if item := cache.users.Get(tenantId + "|" + userid); item == nil {
gatewayClient, err := cache.gatewaySelector.Next()
if err != nil {
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
cs3UserID := &cs3User.UserId{
OpaqueId: userid,
TenantId: tenantId,
}
user, err = revautils.GetUserNoGroups(ctx, cs3UserID, gatewayClient)
if err != nil {
if revautils.IsErrNotFound(err) {
return nil, identity.ErrNotFound
}
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
cache.users.Set(tenantId+"|"+userid, user, ttlcache.DefaultTTL)
} else {
user = item.Value()
}
return user, nil
}
// GetAcceptedUser looks up a user by id, if the user is not cached, yet it will do a lookup via the CS3 API
func (cache IdentityCache) GetAcceptedUser(ctx context.Context, userid string) (libregraph.User, error) {
u, err := cache.GetAcceptedCS3User(ctx, userid)
if err != nil {
return libregraph.User{}, err
}
return *identity.CreateUserModelFromCS3(u), nil
}
func getIDAndMeshProvider(user string) (id, provider string, err error) {
last := strings.LastIndex(user, "@")
if last == -1 {
return "", "", errors.New("not in the form <id>@<provider>")
}
if len(user[:last]) == 0 {
return "", "", errors.New("empty id")
}
if len(user[last+1:]) == 0 {
return "", "", errors.New("empty provider")
}
return user[:last], user[last+1:], nil
}
func (cache IdentityCache) GetAcceptedCS3User(ctx context.Context, userid string) (*cs3User.User, error) {
var user *cs3user.User
if item := cache.users.Get(userid); item == nil {
gatewayClient, err := cache.gatewaySelector.Next()
if err != nil {
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
id, provider, err := getIDAndMeshProvider(userid)
if err != nil {
return nil, errorcode.New(errorcode.InvalidRequest, err.Error())
}
cs3UserID := &cs3User.UserId{
Idp: provider,
OpaqueId: id,
Type: cs3User.UserType_USER_TYPE_FEDERATED,
}
user, err = revautils.GetAcceptedUserWithContext(ctx, cs3UserID, gatewayClient)
if err != nil {
if revautils.IsErrNotFound(err) {
return nil, identity.ErrNotFound
}
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
cache.users.Set(userid, user, ttlcache.DefaultTTL)
} else {
user = item.Value()
}
return user, nil
}
// GetGroup looks up a group by id, if the group is not cached, yet it will do a lookup via the CS3 API
func (cache IdentityCache) GetGroup(ctx context.Context, groupID string) (libregraph.Group, error) {
var group libregraph.Group
if item := cache.groups.Get(groupID); item == nil {
gatewayClient, err := cache.gatewaySelector.Next()
if err != nil {
return group, errorcode.New(errorcode.GeneralException, err.Error())
}
cs3GroupID := &cs3Group.GroupId{
OpaqueId: groupID,
}
req := cs3Group.GetGroupRequest{
GroupId: cs3GroupID,
SkipFetchingMembers: true,
}
res, err := gatewayClient.GetGroup(ctx, &req)
if err != nil {
return group, errorcode.New(errorcode.GeneralException, err.Error())
}
switch res.Status.Code {
case rpc.Code_CODE_OK:
g := res.GetGroup()
group = *identity.CreateGroupModelFromCS3(g)
cache.groups.Set(groupID, group, ttlcache.DefaultTTL)
case rpc.Code_CODE_NOT_FOUND:
return group, identity.ErrNotFound
default:
return group, errorcode.New(errorcode.GeneralException, res.Status.Message)
}
} else {
group = item.Value()
}
return group, nil
}
@@ -0,0 +1,13 @@
package cache_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestCache(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cache Suite")
}
+106
View File
@@ -0,0 +1,106 @@
package cache
import (
"context"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
// mockGatewaySelector is a mock implementation of pool.Selectable[gateway.GatewayAPIClient]
type mockGatewaySelector struct {
client gateway.GatewayAPIClient
}
func (m *mockGatewaySelector) Next(opts ...pool.Option) (gateway.GatewayAPIClient, error) {
return m.client, nil
}
var _ = Describe("Cache", func() {
var (
ctx context.Context
idc IdentityCache
mockGwSelector pool.Selectable[gateway.GatewayAPIClient]
)
BeforeEach(func() {
// Create a mock gateway selector (client can be nil for cached tests)
mockGwSelector = &mockGatewaySelector{
client: nil,
}
idc = NewIdentityCache(
IdentityCacheWithGatewaySelector(mockGwSelector),
)
ctx = context.Background()
})
Describe("GetUser", func() {
It("should return no error", func() {
alan := &cs3User.User{
Id: &cs3User.UserId{
OpaqueId: "alan",
TenantId: "",
},
DisplayName: "Alan",
}
// Persist the user to the cache for 1 hour
idc.users.Set(alan.GetId().GetTenantId()+"|"+alan.GetId().GetOpaqueId(), alan, time.Hour)
// getting the cache item in cache.go line 103 does not work
ru, err := idc.GetUser(ctx, "", "alan")
Expect(err).To(BeNil())
Expect(ru).ToNot(BeNil())
Expect(ru.GetId()).To(Equal(alan.GetId().GetOpaqueId()))
Expect(ru.GetDisplayName()).To(Equal(alan.GetDisplayName()))
})
It("should return the correct user if two users with the same uid and different tennant ids exist", func() {
alan1 := &cs3User.User{
Id: &cs3User.UserId{
OpaqueId: "alan",
TenantId: "1234",
},
DisplayName: "Alan1",
}
alan2 := &cs3User.User{
Id: &cs3User.UserId{
OpaqueId: "alan",
TenantId: "5678",
},
DisplayName: "Alan2",
}
// Persist the user to the cache for 1 hour
idc.users.Set(alan1.GetId().GetTenantId()+"|"+alan1.GetId().GetOpaqueId(), alan1, time.Hour)
idc.users.Set(alan2.GetId().GetTenantId()+"|"+alan2.GetId().GetOpaqueId(), alan2, time.Hour)
ru, err := idc.GetUser(ctx, "5678", "alan")
Expect(err).To(BeNil())
Expect(ru.GetDisplayName()).To(Equal(alan2.GetDisplayName()))
ru, err = idc.GetUser(ctx, "1234", "alan")
Expect(err).To(BeNil())
Expect(ru.GetDisplayName()).To(Equal(alan1.GetDisplayName()))
})
It("should not return an error, if the tenant id does match", func() {
alan := &cs3User.User{
Id: &cs3User.UserId{
OpaqueId: "alan",
TenantId: "1234",
},
DisplayName: "Alan",
}
// Persist the user to the cache for 1 hour
cu := idc.users.Set(alan.GetId().GetTenantId()+"|"+alan.GetId().GetOpaqueId(), alan, time.Hour)
// Test if element has been persisted in the cache
Expect(cu.Value().GetId().GetOpaqueId()).To(Equal(alan.GetId().GetOpaqueId()))
ru, err := idc.GetUser(ctx, "1234", "alan")
Expect(err).To(BeNil())
Expect(ru.GetDisplayName()).To(Equal(alan.GetDisplayName()))
})
})
})
+261
View File
@@ -0,0 +1,261 @@
package identity
import (
"context"
"net/url"
"time"
"github.com/CiscoM31/godata"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/shared"
"github.com/qsfera/server/services/graph/pkg/errorcode"
"github.com/qsfera/server/services/graph/pkg/odata"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
var (
errNotImplemented = errorcode.New(errorcode.NotSupported, "not implemented")
)
type CS3 struct {
Config *shared.Reva
Logger *log.Logger
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
}
// CreateUser implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) CreateUser(ctx context.Context, user libregraph.User) (*libregraph.User, error) {
return nil, errNotImplemented
}
// DeleteUser implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) DeleteUser(ctx context.Context, nameOrID string) error {
return errNotImplemented
}
// UpdateUser implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) UpdateUser(ctx context.Context, nameOrID string, user libregraph.UserUpdate) (*libregraph.User, error) {
return nil, errNotImplemented
}
// GetUser implements the Backend Interface.
func (i *CS3) GetUser(ctx context.Context, nameOrId string, _ *godata.GoDataRequest) (*libregraph.User, error) {
logger := i.Logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "cs3").Msg("GetUser")
gatewayClient, err := i.GatewaySelector.Next()
if err != nil {
logger.Error().Str("backend", "cs3").Err(err).Msg("could not get gatewayClient")
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
}
// Try to get the user by username first
res, err := gatewayClient.GetUserByClaim(ctx, &cs3user.GetUserByClaimRequest{
Claim: "username", // FIXME add consts to reva
Value: nameOrId,
})
switch {
case err != nil:
logger.Error().Str("backend", "cs3").Err(err).Str("nameOrId", nameOrId).Msg("error sending get user by claim id grpc request: transport error")
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
case res.GetStatus().GetCode() == cs3rpc.Code_CODE_OK:
return CreateUserModelFromCS3(res.GetUser()), nil
case res.GetStatus().GetCode() == cs3rpc.Code_CODE_NOT_FOUND:
// If the user was not found by username, try to get it by user ID
default:
logger.Debug().Str("backend", "cs3").Err(err).Str("nameOrId", nameOrId).Msg("error sending get user by claim id grpc request")
return nil, errorcode.New(errorcode.GeneralException, res.GetStatus().GetMessage())
}
// If the user was not found by username, try to get it by user ID
res, err = gatewayClient.GetUserByClaim(ctx, &cs3user.GetUserByClaimRequest{
Claim: "userid", // FIXME add consts to reva
Value: nameOrId,
})
switch {
case err != nil:
logger.Error().Str("backend", "cs3").Err(err).Str("nameOrId", nameOrId).Msg("error sending get user by claim id grpc request: transport error")
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
case res.GetStatus().GetCode() != cs3rpc.Code_CODE_OK:
if res.GetStatus().GetCode() == cs3rpc.Code_CODE_NOT_FOUND {
return nil, errorcode.New(errorcode.ItemNotFound, res.GetStatus().GetMessage())
}
logger.Debug().Str("backend", "cs3").Err(err).Str("nameOrId", nameOrId).Msg("error sending get user by claim id grpc request")
return nil, errorcode.New(errorcode.GeneralException, res.GetStatus().GetMessage())
}
return CreateUserModelFromCS3(res.GetUser()), nil
}
// GetUsers implements the Backend Interface.
func (i *CS3) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.User, error) {
logger := i.Logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "cs3").Msg("GetUsers")
gatewayClient, err := i.GatewaySelector.Next()
if err != nil {
logger.Error().Str("backend", "cs3").Err(err).Msg("could not get gatewayClient")
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
}
search, err := odata.GetSearchValues(oreq.Query)
if err != nil {
return nil, err
}
res, err := gatewayClient.FindUsers(ctx, &cs3user.FindUsersRequest{
// FIXME presence match is currently not implemented, an empty search currently leads to
// Unwilling To Perform": Search Error: error parsing filter: (&(objectclass=posixAccount)(|(cn=*)(displayname=*)(mail=*))), error: Present filter match for cn not implemented
Filters: []*cs3user.Filter{
{
Type: cs3user.Filter_TYPE_QUERY,
Term: &cs3user.Filter_Query{
Query: search,
},
},
},
})
switch {
case err != nil:
logger.Error().Str("backend", "cs3").Err(err).Str("search", search).Msg("error sending find users grpc request: transport error")
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
case res.GetStatus().GetCode() != cs3rpc.Code_CODE_OK:
if res.GetStatus().GetCode() == cs3rpc.Code_CODE_NOT_FOUND {
return nil, errorcode.New(errorcode.ItemNotFound, res.GetStatus().GetMessage())
}
logger.Debug().Str("backend", "cs3").Err(err).Str("search", search).Msg("error sending find users grpc request")
return nil, errorcode.New(errorcode.GeneralException, res.GetStatus().GetMessage())
}
users := make([]*libregraph.User, 0, len(res.GetUsers()))
for _, user := range res.GetUsers() {
users = append(users, CreateUserModelFromCS3(user))
}
return users, nil
}
// FilterUsers implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) FilterUsers(_ context.Context, _ *godata.GoDataRequest, _ *godata.ParseNode) ([]*libregraph.User, error) {
return nil, errNotImplemented
}
// UpdateLastSignInDate implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) UpdateLastSignInDate(ctx context.Context, userID string, timestamp time.Time) error {
return errNotImplemented
}
// GetGroups implements the Backend Interface.
func (i *CS3) GetGroups(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.Group, error) {
logger := i.Logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "cs3").Msg("GetGroups")
gatewayClient, err := i.GatewaySelector.Next()
if err != nil {
logger.Error().Str("backend", "cs3").Err(err).Msg("could not get gatewayClient")
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
}
search, err := odata.GetSearchValues(oreq.Query)
if err != nil {
return nil, err
}
res, err := gatewayClient.FindGroups(ctx, &cs3group.FindGroupsRequest{
// FIXME presence match is currently not implemented, an empty search currently leads to
// Unwilling To Perform": Search Error: error parsing filter: (&(objectclass=posixAccount)(|(cn=*)(displayname=*)(mail=*))), error: Present filter match for cn not implemented
Filters: []*cs3group.Filter{
{
Type: cs3group.Filter_TYPE_QUERY,
Term: &cs3group.Filter_Query{
Query: search,
},
},
},
})
switch {
case err != nil:
logger.Error().Str("backend", "cs3").Err(err).Str("search", search).Msg("error sending find groups grpc request: transport error")
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
return nil, errorcode.New(errorcode.ItemNotFound, res.Status.Message)
}
logger.Debug().Str("backend", "cs3").Err(err).Str("search", search).Msg("error sending find groups grpc request")
return nil, errorcode.New(errorcode.GeneralException, res.Status.Message)
}
groups := make([]*libregraph.Group, 0, len(res.GetGroups()))
for _, group := range res.GetGroups() {
groups = append(groups, CreateGroupModelFromCS3(group))
}
return groups, nil
}
// CreateGroup implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) {
return nil, errNotImplemented
}
// GetGroup implements the Backend Interface.
func (i *CS3) GetGroup(ctx context.Context, groupID string, queryParam url.Values) (*libregraph.Group, error) {
logger := i.Logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "cs3").Msg("GetGroup")
gatewayClient, err := i.GatewaySelector.Next()
if err != nil {
logger.Error().Str("backend", "cs3").Err(err).Msg("could not get gatewayClient")
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
}
res, err := gatewayClient.GetGroupByClaim(ctx, &cs3group.GetGroupByClaimRequest{
Claim: "group_id", // FIXME add consts to reva
Value: groupID,
})
switch {
case err != nil:
logger.Error().Str("backend", "cs3").Err(err).Str("groupid", groupID).Msg("error sending get group by claim id grpc request: transport error")
return nil, errorcode.New(errorcode.ServiceNotAvailable, err.Error())
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
return nil, errorcode.New(errorcode.ItemNotFound, res.Status.Message)
}
logger.Debug().Str("backend", "cs3").Err(err).Str("groupid", groupID).Msg("error sending get group by claim id grpc request")
return nil, errorcode.New(errorcode.GeneralException, res.Status.Message)
}
return CreateGroupModelFromCS3(res.GetGroup()), nil
}
// DeleteGroup implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) DeleteGroup(ctx context.Context, id string) error {
return errNotImplemented
}
// UpdateGroupName implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) UpdateGroupName(ctx context.Context, groupID string, groupName string) error {
return errNotImplemented
}
// GetGroupMembers implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) GetGroupMembers(ctx context.Context, groupID string, _ *godata.GoDataRequest) ([]*libregraph.User, error) {
return nil, errNotImplemented
}
// AddMembersToGroup implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) AddMembersToGroup(ctx context.Context, groupID string, memberID []string) error {
return errNotImplemented
}
// RemoveMemberFromGroup implements the Backend Interface. It's currently not supported for the CS3 backend
func (i *CS3) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error {
return errNotImplemented
}
@@ -0,0 +1,145 @@
package identity
import (
"context"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
// ErrEducationBackend is a dummy EducationBackend, doing nothing
type ErrEducationBackend struct{}
// CreateEducationSchool creates the supplied school in the identity backend.
func (i *ErrEducationBackend) CreateEducationSchool(ctx context.Context, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
return nil, errNotImplemented
}
// DeleteEducationSchool deletes a given school, identified by id
func (i *ErrEducationBackend) DeleteEducationSchool(ctx context.Context, id string) error {
return errNotImplemented
}
// GetEducationSchool implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) GetEducationSchool(ctx context.Context, nameOrID string) (*libregraph.EducationSchool, error) {
return nil, errNotImplemented
}
// GetEducationSchools implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error) {
return nil, errNotImplemented
}
// FilterEducationSchoolsByAttribute implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) FilterEducationSchoolsByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationSchool, error) {
return nil, errNotImplemented
}
// UpdateEducationSchool implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
return nil, errNotImplemented
}
// GetEducationSchoolUsers implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) GetEducationSchoolUsers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// GetEducationSchoolClasses implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) GetEducationSchoolClasses(ctx context.Context, schoolNumberOrID string) ([]*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// AddClassesToEducationSchool implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error {
return errNotImplemented
}
// RemoveClassFromEducationSchool implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error {
return errNotImplemented
}
// AddUsersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
func (i *ErrEducationBackend) AddUsersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error {
return errNotImplemented
}
// RemoveUserFromEducationSchool removes a single member (by ID) from a school
func (i *ErrEducationBackend) RemoveUserFromEducationSchool(ctx context.Context, schoolID string, memberID string) error {
return errNotImplemented
}
// GetEducationClasses implements the EducationBackend interface
func (i *ErrEducationBackend) GetEducationClasses(ctx context.Context) ([]*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// GetEducationClass implements the EducationBackend interface
func (i *ErrEducationBackend) GetEducationClass(ctx context.Context, namedOrID string) (*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// CreateEducationClass implements the EducationBackend interface
func (i *ErrEducationBackend) CreateEducationClass(ctx context.Context, class libregraph.EducationClass) (*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// DeleteEducationClass implements the EducationBackend interface
func (i *ErrEducationBackend) DeleteEducationClass(ctx context.Context, nameOrID string) error {
return errNotImplemented
}
// GetEducationClassMembers implements the EducationBackend interface
func (i *ErrEducationBackend) GetEducationClassMembers(ctx context.Context, nameOrID string) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// UpdateEducationClass implements the EducationBackend interface
func (i *ErrEducationBackend) UpdateEducationClass(ctx context.Context, id string, class libregraph.EducationClass) (*libregraph.EducationClass, error) {
return nil, errNotImplemented
}
// CreateEducationUser creates a given education user in the identity backend.
func (i *ErrEducationBackend) CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// DeleteEducationUser deletes a given education user, identified by username or id, from the backend
func (i *ErrEducationBackend) DeleteEducationUser(ctx context.Context, nameOrID string) error {
return errNotImplemented
}
// UpdateEducationUser applies changes to given education user, identified by username or id
func (i *ErrEducationBackend) UpdateEducationUser(ctx context.Context, nameOrID string, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// GetEducationUser implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// GetEducationUsers implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// FilterEducationUsersByAttribute implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// GetEducationClassTeachers implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// AddTeacherToEducationClass implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error {
return errNotImplemented
}
// RemoveTeacherFromEducationClass implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error {
return errNotImplemented
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,464 @@
package identity
import (
"context"
"errors"
"fmt"
"github.com/go-ldap/ldap/v3"
"github.com/libregraph/idm/pkg/ldapdn"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/errorcode"
)
type educationClassAttributeMap struct {
externalID string
classification string
teachers string
}
func newEducationClassAttributeMap() educationClassAttributeMap {
return educationClassAttributeMap{
externalID: "qsferaEducationExternalId",
classification: "qsferaEducationClassType",
teachers: "qsferaEducationTeacherMember",
}
}
// GetEducationClasses implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationClasses(ctx context.Context) ([]*libregraph.EducationClass, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetEducationClasses")
classFilter := fmt.Sprintf("(&%s(objectClass=%s))", i.groupFilter, i.educationConfig.classObjectClass)
classAttrs := i.getEducationClassAttrTypes(false)
searchRequest := ldap.NewSearchRequest(
i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, 0, 0, false,
classFilter,
classAttrs,
nil,
)
logger.Debug().Str("backend", "ldap").
Str("base", searchRequest.BaseDN).
Str("filter", searchRequest.Filter).
Int("scope", searchRequest.Scope).
Int("sizelimit", searchRequest.SizeLimit).
Interface("attributes", searchRequest.Attributes).
Msg("GetEducationClasses")
res, err := i.conn.Search(searchRequest)
if err != nil {
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
}
classes := make([]*libregraph.EducationClass, 0, len(res.Entries))
var c *libregraph.EducationClass
for _, e := range res.Entries {
if c = i.createEducationClassModelFromLDAP(e); c == nil {
continue
}
classes = append(classes, c)
}
return classes, nil
}
// CreateEducationClass implements the EducationBackend interface for the LDAP backend.
// An EducationClass is mapped to an LDAP entry of the "groupOfNames" structural ObjectClass.
// With a few additional Attributes added on top via the "qsferaEducationClass" auxiliary ObjectClass.
func (i *LDAP) CreateEducationClass(ctx context.Context, class libregraph.EducationClass) (*libregraph.EducationClass, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("create educationClass")
if !i.writeEnabled {
return nil, errorcode.New(errorcode.NotAllowed, "server is configured read-only")
}
ar, err := i.educationClassToAddRequest(class)
if err != nil {
return nil, err
}
if err := i.conn.Add(ar); err != nil {
var lerr *ldap.Error
logger.Debug().Err(err).Msg("error adding class")
if errors.As(err, &lerr) {
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
}
}
return nil, err
}
// Read back group from LDAP to get the generated UUID
e, err := i.getEducationClassByDN(ar.DN)
if err != nil {
return nil, err
}
return i.createEducationClassModelFromLDAP(e), nil
}
// GetEducationClass implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationClass(ctx context.Context, id string) (*libregraph.EducationClass, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetEducationClass")
e, err := i.getEducationClassByID(id, false)
if err != nil {
return nil, err
}
var class *libregraph.EducationClass
if class = i.createEducationClassModelFromLDAP(e); class == nil {
return nil, errorcode.New(errorcode.ItemNotFound, "not found")
}
return class, nil
}
// DeleteEducationClass implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) DeleteEducationClass(ctx context.Context, id string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("DeleteEducationClass")
if !i.writeEnabled {
return ErrReadOnly
}
e, err := i.getEducationClassByID(id, false)
if err != nil {
return err
}
dr := ldap.DelRequest{DN: e.DN}
if err = i.conn.Del(&dr); err != nil {
return err
}
// TODO update any users that are member of this school
return nil
}
// UpdateEducationClass implements the EducationBackend interface for the LDAP backend.
// Only the displayName and externalID are supported to change at this point.
func (i *LDAP) UpdateEducationClass(ctx context.Context, id string, class libregraph.EducationClass) (*libregraph.EducationClass, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("UpdateEducationClass")
if !i.writeEnabled {
return nil, ErrReadOnly
}
g, err := i.getLDAPGroupByID(id, false)
if err != nil {
return nil, err
}
var updateNeeded bool
if class.GetId() != "" {
id, err := i.ldapUUIDtoString(g, i.groupAttributeMap.id, i.groupIDisOctetString)
if err != nil {
i.logger.Warn().Str("dn", g.DN).Str(i.userAttributeMap.id, g.GetEqualFoldAttributeValue(i.userAttributeMap.id)).Msg("Invalid class. Cannot convert UUID")
return nil, errorcode.New(errorcode.GeneralException, "error converting uuid")
}
if id != class.GetId() {
return nil, errorcode.New(errorcode.NotAllowed, "changing the GroupID is not allowed")
}
}
if class.GetDescription() != "" {
return nil, errorcode.New(errorcode.NotSupported, "changing the description is currently not supported")
}
if len(class.GetMembers()) != 0 {
return nil, errorcode.New(errorcode.NotSupported, "changing the members is currently not supported")
}
if class.GetClassification() != "" {
return nil, errorcode.New(errorcode.NotSupported, "changing the classification is currently not supported")
}
dn := g.DN
if eID := class.GetExternalId(); eID != "" {
if g.GetEqualFoldAttributeValue(i.educationConfig.classAttributeMap.externalID) != eID {
dn, err = i.updateClassExternalID(ctx, dn, eID)
if err != nil {
return nil, err
}
}
}
mr := ldap.ModifyRequest{DN: dn}
if dName := class.GetDisplayName(); dName != "" {
if g.GetEqualFoldAttributeValue(i.groupAttributeMap.name) != dName {
mr.Replace(i.groupAttributeMap.name, []string{dName})
updateNeeded = true
}
}
if updateNeeded {
if err := i.conn.Modify(&mr); err != nil {
return nil, err
}
}
g, err = i.getEducationClassByDN(dn)
if err != nil {
return nil, err
}
return i.createEducationClassModelFromLDAP(g), nil
}
func (i *LDAP) updateClassExternalID(ctx context.Context, dn, externalID string) (string, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
newDN := fmt.Sprintf("qsferaEducationExternalId=%s", externalID)
mrdn := ldap.NewModifyDNRequest(dn, newDN, true, "")
i.logger.Debug().Str("Backend", "ldap").
Str("dn", mrdn.DN).
Str("newrdn", mrdn.NewRDN).
Msg("updating class external ID")
if err := i.conn.ModifyDN(mrdn); err != nil {
var lerr *ldap.Error
logger.Debug().Err(err).Msg("error updating class external ID")
if errors.As(err, &lerr) {
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
}
}
return "", err
}
return fmt.Sprintf("%s,%s", newDN, i.groupBaseDN), nil
}
// GetEducationClassMembers implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationClassMembers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetEducationClassMembers")
e, err := i.getEducationClassByID(id, true)
if err != nil {
return nil, err
}
memberEntries, err := i.expandLDAPAttributeEntries(ctx, e, i.groupAttributeMap.member, "")
result := make([]*libregraph.EducationUser, 0, len(memberEntries))
if err != nil {
return nil, err
}
for _, member := range memberEntries {
if u := i.createEducationUserModelFromLDAP(member); u != nil {
result = append(result, u)
}
}
return result, nil
}
func (i *LDAP) educationClassToAddRequest(class libregraph.EducationClass) (*ldap.AddRequest, error) {
plainGroup := i.educationClassToGroup(class)
ldapAttrs, err := i.groupToLDAPAttrValues(*plainGroup)
if err != nil {
return nil, err
}
ldapAttrs, err = i.educationClassToLDAPAttrValues(class, ldapAttrs)
if err != nil {
return nil, err
}
ar := ldap.NewAddRequest(i.getEducationClassLDAPDN(class), nil)
for attrType, values := range ldapAttrs {
ar.Attribute(attrType, values)
}
return ar, nil
}
func (i *LDAP) educationClassToGroup(class libregraph.EducationClass) *libregraph.Group {
group := libregraph.NewGroup()
group.SetDisplayName(class.DisplayName)
return group
}
func (i *LDAP) educationClassToLDAPAttrValues(class libregraph.EducationClass, attrs ldapAttributeValues) (ldapAttributeValues, error) {
if externalID, ok := class.GetExternalIdOk(); ok {
attrs[i.educationConfig.classAttributeMap.externalID] = []string{*externalID}
}
if classification, ok := class.GetClassificationOk(); ok {
attrs[i.educationConfig.classAttributeMap.classification] = []string{*classification}
}
attrs["objectClass"] = append(attrs["objectClass"], i.educationConfig.classObjectClass)
return attrs, nil
}
func (i *LDAP) getEducationClassAttrTypes(requestMembers bool) []string {
attrs := []string{
i.groupAttributeMap.name,
i.groupAttributeMap.id,
i.educationConfig.classAttributeMap.classification,
i.educationConfig.classAttributeMap.externalID,
i.educationConfig.memberOfSchoolAttribute,
i.educationConfig.classAttributeMap.teachers,
}
if requestMembers {
attrs = append(attrs, i.groupAttributeMap.member)
}
return attrs
}
func (i *LDAP) getEducationClassByDN(dn string) (*ldap.Entry, error) {
filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.classObjectClass)
if i.groupFilter != "" {
filter = fmt.Sprintf("(&%s(%s))", filter, i.groupFilter)
}
return i.getEntryByDN(dn, i.getEducationClassAttrTypes(false), filter)
}
func (i *LDAP) createEducationClassModelFromLDAP(e *ldap.Entry) *libregraph.EducationClass {
group := i.createGroupModelFromLDAP(e)
return i.groupToEducationClass(*group, e)
}
func (i *LDAP) groupToEducationClass(group libregraph.Group, e *ldap.Entry) *libregraph.EducationClass {
class := libregraph.NewEducationClass(group.GetDisplayName(), "")
class.SetId(group.GetId())
if e != nil {
// Set the education User specific Attributes from the supplied LDAP Entry
if externalID := e.GetEqualFoldAttributeValue(i.educationConfig.classAttributeMap.externalID); externalID != "" {
class.SetExternalId(externalID)
}
if classification := e.GetEqualFoldAttributeValue(i.educationConfig.classAttributeMap.classification); classification != "" {
class.SetClassification(classification)
}
}
return class
}
func (i *LDAP) getEducationClassLDAPDN(class libregraph.EducationClass) string {
attributeTypeAndValue := ldap.AttributeTypeAndValue{
Type: "qsferaEducationExternalId",
Value: class.GetExternalId(),
}
return fmt.Sprintf("%s,%s", attributeTypeAndValue.String(), i.groupBaseDN)
}
func (i *LDAP) getEducationClassByID(nameOrID string, requestMembers bool) (*ldap.Entry, error) {
return i.getEducationObjectByNameOrID(
nameOrID,
i.userAttributeMap.id,
i.educationConfig.classAttributeMap.externalID,
i.groupFilter,
i.educationConfig.classObjectClass,
i.groupBaseDN,
i.getEducationClassAttrTypes(requestMembers),
)
}
// GetEducationClassTeachers returns the EducationUser teachers for an EducationClass
func (i *LDAP) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
class, err := i.getEducationClassByID(classID, false)
if err != nil {
logger.Debug().Err(err).Msg("could not get class: backend error")
return nil, err
}
teacherEntries, err := i.expandLDAPAttributeEntries(ctx, class, i.educationConfig.classAttributeMap.teachers, "")
result := make([]*libregraph.EducationUser, 0, len(teacherEntries))
if err != nil {
return nil, err
}
for _, teacher := range teacherEntries {
if u := i.createEducationUserModelFromLDAP(teacher); u != nil {
result = append(result, u)
}
}
return result, nil
}
// AddTeacherToEducationClass adds a teacher (by ID) to class in the identity backend.
func (i *LDAP) AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
class, err := i.getEducationClassByID(classID, false)
if err != nil {
logger.Debug().Err(err).Msg("could not get class: backend error")
return err
}
logger.Debug().Str("classDn", class.DN).Msg("got a class")
teacher, err := i.getEducationUserByNameOrID(teacherID)
if err != nil {
logger.Debug().Err(err).Msg("could not get education user: error fetching education user from backend")
return err
}
logger.Debug().Str("userDn", teacher.DN).Msg("got a user")
mr := ldap.ModifyRequest{DN: class.DN}
// Handle empty teacher list
current := class.GetEqualFoldAttributeValues(i.educationConfig.classAttributeMap.teachers)
if len(current) == 1 && current[0] == "" {
mr.Delete(i.educationConfig.classAttributeMap.teachers, []string{""})
}
// Create a Set of current teachers
currentSet := make(map[string]struct{}, len(current))
for _, currentTeacher := range current {
if currentTeacher == "" {
continue
}
nCurrentTeacher, err := ldapdn.ParseNormalize(currentTeacher)
if err != nil {
// Couldn't parse teacher value as a DN, skipping
logger.Warn().Str("teacherDN", currentTeacher).Err(err).Msg("Couldn't parse DN")
continue
}
currentSet[nCurrentTeacher] = struct{}{}
}
var newTeacherDN []string
nDN, err := ldapdn.ParseNormalize(teacher.DN)
if err != nil {
logger.Error().Str("new teacher", teacher.DN).Err(err).Msg("Couldn't parse DN")
return err
}
if _, present := currentSet[nDN]; !present {
newTeacherDN = append(newTeacherDN, teacher.DN)
} else {
logger.Debug().Str("teacherDN", teacher.DN).Msg("Member already present in group. Skipping")
}
if len(newTeacherDN) > 0 {
mr.Add(i.educationConfig.classAttributeMap.teachers, newTeacherDN)
if err := i.conn.Modify(&mr); err != nil {
return err
}
}
return nil
}
// RemoveTeacherFromEducationClass removes teacher (by ID) from a class
func (i *LDAP) RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
class, err := i.getEducationClassByID(classID, false)
if err != nil {
logger.Debug().Err(err).Msg("could not get class: backend error")
return err
}
teacher, err := i.getEducationUserByNameOrID(teacherID)
if err != nil {
logger.Debug().Err(err).Msg("could not get education user: error fetching education user from backend")
return err
}
return i.removeEntryByDNAndAttributeFromEntry(class, teacher.DN, i.educationConfig.classAttributeMap.teachers)
}
@@ -0,0 +1,551 @@
package identity
import (
"context"
"errors"
"testing"
"github.com/go-ldap/ldap/v3"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/identity/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var classEntry = ldap.NewEntry("qsferaEducationExternalId=Math0123",
map[string][]string{
"cn": {"Math"},
"qsferaEducationExternalId": {"Math0123"},
"qsferaEducationClassType": {"course"},
"entryUUID": {"abcd-defg"},
})
var classEntryWithSchool = ldap.NewEntry("qsferaEducationExternalId=Math0123",
map[string][]string{
"cn": {"Math"},
"qsferaEducationExternalId": {"Math0123"},
"qsferaEducationClassType": {"course"},
"entryUUID": {"abcd-defg"},
"qsferaMemberOfSchool": {"abcd-defg"},
})
var classEntryWithMember = ldap.NewEntry("qsferaEducationExternalId=Math0123",
map[string][]string{
"cn": {"Math"},
"qsferaEducationExternalId": {"Math0123"},
"qsferaEducationClassType": {"course"},
"entryUUID": {"abcd-defg"},
"member": {"uid=user"},
})
func TestCreateEducationClass(t *testing.T) {
lm := &mocks.Client{}
lm.On("Add", mock.Anything).
Return(nil)
lm.On("Search", mock.Anything).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{classEntry},
},
nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
assert.NotEqual(t, "", b.educationConfig.classObjectClass)
class := libregraph.NewEducationClass("Math", "course")
class.SetExternalId("Math0123")
class.SetId("abcd-defg")
resClass, err := b.CreateEducationClass(context.Background(), *class)
lm.AssertNumberOfCalls(t, "Add", 1)
lm.AssertNumberOfCalls(t, "Search", 1)
assert.Nil(t, err)
assert.NotNil(t, resClass)
assert.Equal(t, resClass.GetDisplayName(), class.GetDisplayName())
assert.Equal(t, resClass.GetId(), class.GetId())
assert.Equal(t, resClass.GetExternalId(), class.GetExternalId())
assert.Equal(t, resClass.GetClassification(), class.GetClassification())
}
func TestGetEducationClasses(t *testing.T) {
lm := &mocks.Client{}
lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
b, _ := getMockedBackend(lm, lconfig, &logger)
_, err := b.GetEducationClasses(context.Background())
assert.ErrorContains(t, err, "itemNotFound:")
lm = &mocks.Client{}
lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
b, _ = getMockedBackend(lm, lconfig, &logger)
g, err := b.GetEducationClasses(context.Background())
if err != nil {
t.Errorf("Expected success, got '%s'", err.Error())
} else if g == nil || len(g) != 0 {
t.Errorf("Expected zero length user slice")
}
lm = &mocks.Client{}
lm.On("Search", mock.Anything).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{classEntry},
}, nil)
b, _ = getMockedBackend(lm, lconfig, &logger)
g, err = b.GetEducationClasses(context.Background())
if err != nil {
t.Errorf("Expected GetEducationClasses to succeed. Got %s", err.Error())
} else if *g[0].Id != classEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
t.Errorf("Expected GetEducationClasses to return a valid group")
}
}
func TestGetEducationClass(t *testing.T) {
tests := []struct {
name string
id string
filter string
expectedItemNotFound bool
}{
{
name: "Test search class using id",
id: "abcd-defg",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=abcd-defg)(qsferaEducationExternalId=abcd-defg)))",
expectedItemNotFound: false,
},
{
name: "Test search class using unknown Id",
id: "xxxx-xxxx",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=xxxx-xxxx)(qsferaEducationExternalId=xxxx-xxxx)))",
expectedItemNotFound: true,
},
{
name: "Test search class using external ID",
id: "Math0123",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=Math0123)(qsferaEducationExternalId=Math0123)))",
expectedItemNotFound: false,
},
{
name: "Test search school using unknown externalID",
id: "Unknown3210",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=Unknown3210)(qsferaEducationExternalId=Unknown3210)))",
expectedItemNotFound: true,
},
}
for _, tt := range tests {
lm := &mocks.Client{}
sr := &ldap.SearchRequest{
BaseDN: "ou=groups,dc=test",
Scope: 2,
SizeLimit: 1,
Filter: tt.filter,
Attributes: []string{"cn", "entryUUID", "qsferaEducationClassType", "qsferaEducationExternalId", "qsferaMemberOfSchool", "qsferaEducationTeacherMember"},
Controls: []ldap.Control(nil),
}
if tt.expectedItemNotFound {
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
} else {
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{classEntry}}, nil)
}
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
class, err := b.GetEducationClass(context.Background(), tt.id)
lm.AssertNumberOfCalls(t, "Search", 1)
if tt.expectedItemNotFound {
assert.NotNil(t, err)
assert.Equal(t, "itemNotFound: not found", err.Error())
} else {
assert.Nil(t, err)
assert.Equal(t, "Math", class.GetDisplayName())
assert.Equal(t, "abcd-defg", class.GetId())
assert.Equal(t, "Math0123", class.GetExternalId())
}
}
}
func TestDeleteEducationClass(t *testing.T) {
tests := []struct {
name string
id string
filter string
expectedItemNotFound bool
}{
{
name: "Test search class using id",
id: "abcd-defg",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=abcd-defg)(qsferaEducationExternalId=abcd-defg)))",
expectedItemNotFound: false,
},
{
name: "Test search class using unknown Id",
id: "xxxx-xxxx",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=xxxx-xxxx)(qsferaEducationExternalId=xxxx-xxxx)))",
expectedItemNotFound: true,
},
{
name: "Test search class using external ID",
id: "Math0123",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=Math0123)(qsferaEducationExternalId=Math0123)))",
expectedItemNotFound: false,
},
{
name: "Test search school using unknown externalID",
id: "Unknown3210",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=Unknown3210)(qsferaEducationExternalId=Unknown3210)))",
expectedItemNotFound: true,
},
}
for _, tt := range tests {
lm := &mocks.Client{}
sr := &ldap.SearchRequest{
BaseDN: "ou=groups,dc=test",
Scope: 2,
SizeLimit: 1,
Filter: tt.filter,
Attributes: []string{"cn", "entryUUID", "qsferaEducationClassType", "qsferaEducationExternalId", "qsferaMemberOfSchool", "qsferaEducationTeacherMember"},
Controls: []ldap.Control(nil),
}
if tt.expectedItemNotFound {
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
} else {
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{classEntry}}, nil)
}
dr := &ldap.DelRequest{
DN: "qsferaEducationExternalId=Math0123",
}
lm.On("Del", dr).Return(nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
err = b.DeleteEducationClass(context.Background(), tt.id)
lm.AssertNumberOfCalls(t, "Search", 1)
if tt.expectedItemNotFound {
lm.AssertNumberOfCalls(t, "Del", 0)
assert.NotNil(t, err)
assert.Equal(t, "itemNotFound: not found", err.Error())
} else {
assert.Nil(t, err)
}
}
}
func TestGetEducationClassMembers(t *testing.T) {
tests := []struct {
name string
id string
filter string
expectedItemNotFound bool
}{
{
name: "Test search class using id",
id: "abcd-defg",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=abcd-defg)(qsferaEducationExternalId=abcd-defg)))",
expectedItemNotFound: false,
},
{
name: "Test search class using unknown Id",
id: "xxxx-xxxx",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=xxxx-xxxx)(qsferaEducationExternalId=xxxx-xxxx)))",
expectedItemNotFound: true,
},
{
name: "Test search class using external ID",
id: "Math0123",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=Math0123)(qsferaEducationExternalId=Math0123)))",
expectedItemNotFound: false,
},
{
name: "Test search school using unknown externalID",
id: "Unknown3210",
filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=Unknown3210)(qsferaEducationExternalId=Unknown3210)))",
expectedItemNotFound: true,
},
}
for _, tt := range tests {
lm := &mocks.Client{}
userSr := &ldap.SearchRequest{
BaseDN: "uid=user",
Scope: 0,
SizeLimit: 1,
Filter: "(objectClass=inetOrgPerson)",
Attributes: ldapUserAttributes,
Controls: []ldap.Control(nil),
}
lm.On("Search", userSr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil)
sr := &ldap.SearchRequest{
BaseDN: "ou=groups,dc=test",
Scope: 2,
SizeLimit: 1,
Filter: tt.filter,
Attributes: []string{"cn", "entryUUID", "qsferaEducationClassType", "qsferaEducationExternalId", "qsferaMemberOfSchool", "qsferaEducationTeacherMember", "member"},
Controls: []ldap.Control(nil),
}
if tt.expectedItemNotFound {
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
} else {
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{classEntryWithMember}}, nil)
}
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
users, err := b.GetEducationClassMembers(context.Background(), tt.id)
if tt.expectedItemNotFound {
lm.AssertNumberOfCalls(t, "Search", 1)
assert.NotNil(t, err)
assert.Equal(t, "itemNotFound: not found", err.Error())
} else {
lm.AssertNumberOfCalls(t, "Search", 2)
assert.Nil(t, err)
assert.Equal(t, len(users), 1)
}
}
}
func TestLDAP_UpdateEducationClass(t *testing.T) {
externalIDs := []string{"Math3210"}
changeString := "xxxx-xxxx"
type args struct {
id string
class libregraph.EducationClass
}
type modifyData struct {
arg *ldap.ModifyRequest
ret error
}
type modifyDNData struct {
arg *ldap.ModifyDNRequest
ret error
}
type searchData struct {
res *ldap.SearchResult
err error
}
tests := []struct {
name string
args args
modifyDNData modifyDNData
modifyData modifyData
searchData searchData
assertion func(assert.TestingT, error, ...any) bool
}{
{
name: "Change name",
args: args{
id: "abcd-defg",
class: libregraph.EducationClass{
DisplayName: "Math-2",
},
},
assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Nil(tt, err) },
modifyData: modifyData{
arg: &ldap.ModifyRequest{
DN: "qsferaEducationExternalId=Math0123",
Changes: []ldap.Change{
{
Operation: ldap.ReplaceAttribute,
Modification: ldap.PartialAttribute{
Type: "cn",
Vals: []string{"Math-2"},
},
},
},
},
},
modifyDNData: modifyDNData{
arg: &ldap.ModifyDNRequest{},
ret: nil,
},
searchData: searchData{
res: &ldap.SearchResult{
Entries: []*ldap.Entry{classEntry},
},
err: nil,
},
},
{
name: "Change external ID",
args: args{
id: "abcd-defg",
class: libregraph.EducationClass{
ExternalId: &externalIDs[0],
},
},
assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Nil(tt, err) },
modifyData: modifyData{
arg: &ldap.ModifyRequest{},
},
modifyDNData: modifyDNData{
arg: &ldap.ModifyDNRequest{
DN: "qsferaEducationExternalId=Math0123",
NewRDN: "qsferaEducationExternalId=Math3210",
DeleteOldRDN: true,
NewSuperior: "",
},
ret: nil,
},
searchData: searchData{
res: &ldap.SearchResult{
Entries: []*ldap.Entry{classEntry},
},
err: nil,
},
},
{
name: "Change both name and external ID",
args: args{
id: "abcd-defg",
class: libregraph.EducationClass{
DisplayName: "Math-2",
ExternalId: &externalIDs[0],
},
},
assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Nil(tt, err) },
modifyData: modifyData{
arg: &ldap.ModifyRequest{
DN: "qsferaEducationExternalId=Math3210,ou=groups,dc=test",
Changes: []ldap.Change{
{
Operation: ldap.ReplaceAttribute,
Modification: ldap.PartialAttribute{
Type: "cn",
Vals: []string{"Math-2"},
},
},
},
},
},
modifyDNData: modifyDNData{
arg: &ldap.ModifyDNRequest{
DN: "qsferaEducationExternalId=Math0123",
NewRDN: "qsferaEducationExternalId=Math3210",
DeleteOldRDN: true,
NewSuperior: "",
},
ret: nil,
},
searchData: searchData{
res: &ldap.SearchResult{
Entries: []*ldap.Entry{classEntry},
},
err: nil,
},
},
{
name: "Check error: attempt at changing ID",
args: args{
id: "abcd-defg",
class: libregraph.EducationClass{
Id: &changeString,
},
},
assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Error(tt, err) },
modifyData: modifyData{
arg: &ldap.ModifyRequest{},
},
modifyDNData: modifyDNData{
arg: &ldap.ModifyDNRequest{},
ret: nil,
},
searchData: searchData{
res: &ldap.SearchResult{
Entries: []*ldap.Entry{classEntry},
},
err: nil,
},
},
{
name: "Check error: attempt at changing description",
args: args{
id: "abcd-defg",
class: libregraph.EducationClass{
Description: &changeString,
},
},
assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Error(tt, err) },
modifyData: modifyData{
arg: &ldap.ModifyRequest{},
},
modifyDNData: modifyDNData{
arg: &ldap.ModifyDNRequest{},
ret: nil,
},
searchData: searchData{
res: &ldap.SearchResult{
Entries: []*ldap.Entry{classEntry},
},
err: nil,
},
},
{
name: "Check error: attempt at changing classification",
args: args{
id: "abcd-defg",
class: libregraph.EducationClass{
Classification: changeString,
},
},
assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Error(tt, err) },
modifyData: modifyData{
arg: &ldap.ModifyRequest{},
},
modifyDNData: modifyDNData{
arg: &ldap.ModifyDNRequest{},
ret: nil,
},
searchData: searchData{
res: &ldap.SearchResult{
Entries: []*ldap.Entry{classEntry},
},
err: nil,
},
},
{
name: "Check error: attempt at changing members",
args: args{
id: "abcd-defg",
class: libregraph.EducationClass{
Members: []libregraph.User{*libregraph.NewUser("display name", "username")},
},
},
assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Error(tt, err) },
modifyData: modifyData{
arg: &ldap.ModifyRequest{},
},
modifyDNData: modifyDNData{
arg: &ldap.ModifyDNRequest{},
ret: nil,
},
searchData: searchData{
res: &ldap.SearchResult{
Entries: []*ldap.Entry{classEntry},
},
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lm := &mocks.Client{}
b, err := getMockedBackend(lm, eduConfig, &logger)
if err != nil {
panic(err)
}
lm.On("Modify", tt.modifyData.arg).Return(tt.modifyData.ret)
lm.On("ModifyDN", tt.modifyDNData.arg).Return(tt.modifyDNData.ret)
lm.On("Search", mock.Anything).Return(tt.searchData.res, tt.searchData.err)
ctx := context.Background()
_, err = b.UpdateEducationClass(ctx, tt.args.id, tt.args.class)
tt.assertion(t, err)
})
}
}
@@ -0,0 +1,812 @@
package identity
import (
"context"
"errors"
"fmt"
"slices"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/google/uuid"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/errorcode"
)
type educationConfig struct {
schoolBaseDN string
schoolFilter string
schoolObjectClass string
schoolScope int
// memberOfSchoolAttribute defines the AttributeType on the user/group objects
// which contains the school Id to which the user/group is assigned
memberOfSchoolAttribute string
schoolAttributeMap schoolAttributeMap
userObjectClass string
userAttributeMap educationUserAttributeMap
classObjectClass string
classAttributeMap educationClassAttributeMap
}
type schoolAttributeMap struct {
displayName string
schoolNumber string
externalId string
id string
terminationDate string
}
type schoolUpdateOperation uint8
const (
tooManyValues schoolUpdateOperation = iota
schoolUnchanged
schoolRenamed
schoolPropertiesUpdated
)
var (
errNotSet = errors.New("attribute not set")
errSchoolNameExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that name is already present")
errSchoolNumberExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that number is already present")
)
func defaultEducationConfig() educationConfig {
return educationConfig{
schoolObjectClass: "qsferaEducationSchool",
schoolScope: ldap.ScopeWholeSubtree,
memberOfSchoolAttribute: "qsferaMemberOfSchool",
schoolAttributeMap: newSchoolAttributeMap(),
userObjectClass: "qsferaEducationUser",
userAttributeMap: newEducationUserAttributeMap(),
classObjectClass: "qsferaEducationClass",
classAttributeMap: newEducationClassAttributeMap(),
}
}
func newEducationConfig(config config.LDAP) (educationConfig, error) {
if config.EducationResourcesEnabled {
var err error
eduCfg := defaultEducationConfig()
eduCfg.schoolBaseDN = config.EducationConfig.SchoolBaseDN
if config.EducationConfig.SchoolSearchScope != "" {
if eduCfg.schoolScope, err = stringToScope(config.EducationConfig.SchoolSearchScope); err != nil {
return educationConfig{}, fmt.Errorf("error configuring school search scope: %w", err)
}
}
if config.EducationConfig.SchoolFilter != "" {
eduCfg.schoolFilter = config.EducationConfig.SchoolFilter
}
if config.EducationConfig.SchoolObjectClass != "" {
eduCfg.schoolObjectClass = config.EducationConfig.SchoolObjectClass
}
// Attribute mapping config
if config.EducationConfig.SchoolNameAttribute != "" {
eduCfg.schoolAttributeMap.displayName = config.EducationConfig.SchoolNameAttribute
}
if config.EducationConfig.SchoolNumberAttribute != "" {
eduCfg.schoolAttributeMap.schoolNumber = config.EducationConfig.SchoolNumberAttribute
}
if config.EducationConfig.SchoolIDAttribute != "" {
eduCfg.schoolAttributeMap.id = config.EducationConfig.SchoolIDAttribute
}
return eduCfg, nil
}
return educationConfig{}, nil
}
func newSchoolAttributeMap() schoolAttributeMap {
return schoolAttributeMap{
displayName: "ou",
schoolNumber: "qsferaEducationSchoolNumber",
externalId: "qsferaEducationExternalId",
id: "qsferaUUID",
terminationDate: "qsferaEducationSchoolTerminationTimestamp",
}
}
// CreateEducationSchool creates the supplied school in the identity backend.
func (i *LDAP) CreateEducationSchool(ctx context.Context, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("CreateEducationSchool")
if !i.writeEnabled {
return nil, ErrReadOnly
}
// Check that the school number is not already used
if school.HasSchoolNumber() {
_, err := i.getSchoolByNumber(school.GetSchoolNumber())
switch {
case err == nil:
logger.Debug().Err(errSchoolNumberExists).Str("schoolNumber", school.GetSchoolNumber()).Msg("duplicate school number")
return nil, errSchoolNumberExists
case errors.Is(err, ErrNotFound):
break
default:
logger.Error().Err(err).Str("schoolNumber", school.GetSchoolNumber()).Msg("error looking up school by number")
return nil, errorcode.New(errorcode.GeneralException, "error looking up school by number")
}
}
attributeTypeAndValue := ldap.AttributeTypeAndValue{
Type: i.educationConfig.schoolAttributeMap.displayName,
Value: school.GetDisplayName(),
}
dn := fmt.Sprintf("%s,%s",
attributeTypeAndValue.String(),
i.educationConfig.schoolBaseDN,
)
ar := ldap.NewAddRequest(dn, nil)
ar.Attribute(i.educationConfig.schoolAttributeMap.displayName, []string{school.GetDisplayName()})
if school.HasSchoolNumber() {
ar.Attribute(i.educationConfig.schoolAttributeMap.schoolNumber, []string{school.GetSchoolNumber()})
}
if school.HasExternalId() {
ar.Attribute(i.educationConfig.schoolAttributeMap.externalId, []string{school.GetExternalId()})
}
if !i.useServerUUID {
ar.Attribute(i.educationConfig.schoolAttributeMap.id, []string{uuid.NewString()})
}
objectClasses := []string{"organizationalUnit", i.educationConfig.schoolObjectClass, "top"}
ar.Attribute("objectClass", objectClasses)
if err := i.conn.Add(ar); err != nil {
var lerr *ldap.Error
logger.Debug().Err(err).Msg("error adding school")
if errors.As(err, &lerr) {
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
err = errSchoolNameExists
}
}
return nil, err
}
// Read back school from LDAP to get the generated UUID
e, err := i.getSchoolByDN(ar.DN)
if err != nil {
return nil, err
}
return i.createSchoolModelFromLDAP(e), nil
}
// UpdateEducationSchoolOperation contains the logic for which update operation to apply to a school
func (i *LDAP) updateEducationSchoolOperation(
schoolUpdate libregraph.EducationSchool,
currentSchool libregraph.EducationSchool,
) schoolUpdateOperation {
providedDisplayName, displayNameIsSet := schoolUpdate.GetDisplayNameOk()
if displayNameIsSet {
if *providedDisplayName == "" || *providedDisplayName == currentSchool.GetDisplayName() {
// The school name hasn't changed
displayNameIsSet = false
}
}
var propertiesUpdated bool
switch {
case schoolUpdate.HasSchoolNumber():
if schoolUpdate.GetSchoolNumber() != "" && schoolUpdate.GetSchoolNumber() != currentSchool.GetSchoolNumber() {
propertiesUpdated = true
}
case schoolUpdate.HasTerminationDate():
if schoolUpdate.GetTerminationDate() != currentSchool.GetTerminationDate() {
propertiesUpdated = true
}
}
if propertiesUpdated && displayNameIsSet {
return tooManyValues
}
if displayNameIsSet {
return schoolRenamed
}
if propertiesUpdated {
return schoolPropertiesUpdated
}
return schoolUnchanged
}
// updateDisplayName updates the school OU in the identity backend
func (i *LDAP) updateDisplayName(ctx context.Context, dn string, providedDisplayName string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
attributeTypeAndValue := ldap.AttributeTypeAndValue{
Type: i.educationConfig.schoolAttributeMap.displayName,
Value: providedDisplayName,
}
mrdn := ldap.NewModifyDNRequest(dn, attributeTypeAndValue.String(), true, "")
i.logger.Debug().Str("backend", "ldap").
Str("dn", mrdn.DN).
Str("newrdn", mrdn.NewRDN).
Msg("updateDisplayName")
if err := i.conn.ModifyDN(mrdn); err != nil {
var lerr *ldap.Error
logger.Debug().Err(err).Msg("error updating school name")
if errors.As(err, &lerr) {
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
err = errSchoolNameExists
}
}
return err
}
return nil
}
// updateSchoolProperties updates the properties (other that displayName) of a school.
// It checks if a school number is already taken, before updating the school number
func (i *LDAP) updateSchoolProperties(ctx context.Context, dn string, currentSchool, updatedSchool libregraph.EducationSchool) error {
logger := i.logger.SubloggerWithRequestID(ctx)
mr := ldap.NewModifyRequest(dn, nil)
if updatedSchoolNumber, ok := updatedSchool.GetSchoolNumberOk(); ok {
if *updatedSchoolNumber != "" && currentSchool.GetSchoolNumber() != *updatedSchoolNumber {
_, err := i.getSchoolByNumber(*updatedSchoolNumber)
if err == nil {
return errSchoolNumberExists
}
mr.Replace(i.educationConfig.schoolAttributeMap.schoolNumber, []string{*updatedSchoolNumber})
}
}
if updatedTerminationDate, ok := updatedSchool.GetTerminationDateOk(); ok {
if updatedTerminationDate == nil && currentSchool.HasTerminationDate() {
// Delete the termination date
mr.Delete(i.educationConfig.schoolAttributeMap.terminationDate, []string{})
}
if updatedTerminationDate != nil && *updatedTerminationDate != currentSchool.GetTerminationDate() {
ldapDateTime := updatedTerminationDate.UTC().Format(ldapDateFormat)
mr.Replace(i.educationConfig.schoolAttributeMap.terminationDate, []string{ldapDateTime})
}
}
if err := i.conn.Modify(mr); err != nil {
logger.Debug().Err(err).Msg("error updating school number")
return err
}
return nil
}
// UpdateEducationSchool updates the supplied school in the identity backend
func (i *LDAP) UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("UpdateEducationSchool")
if !i.writeEnabled {
return nil, ErrReadOnly
}
e, err := i.getSchoolByNumberOrID(numberOrID)
if err != nil {
return nil, err
}
currentSchool := i.createSchoolModelFromLDAP(e)
switch i.updateEducationSchoolOperation(school, *currentSchool) {
case tooManyValues:
return nil, fmt.Errorf("school name and school number cannot be updated in the same request")
case schoolUnchanged:
logger.Debug().Str("backend", "ldap").Msg("UpdateEducationSchool: Nothing changed")
return currentSchool, nil
case schoolRenamed:
if err := i.updateDisplayName(ctx, e.DN, school.GetDisplayName()); err != nil {
return nil, err
}
case schoolPropertiesUpdated:
if err := i.updateSchoolProperties(ctx, e.DN, *currentSchool, school); err != nil {
return nil, err
}
}
// Read back school from LDAP
e, err = i.getSchoolByNumberOrID(i.getID(e))
if err != nil {
return nil, err
}
return i.createSchoolModelFromLDAP(e), nil
}
// DeleteEducationSchool deletes a given school, identified by id
func (i *LDAP) DeleteEducationSchool(ctx context.Context, id string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("DeleteEducationSchool")
if !i.writeEnabled {
return ErrReadOnly
}
e, err := i.getSchoolByNumberOrID(id)
if err != nil {
return err
}
dr := ldap.DelRequest{DN: e.DN}
if err = i.conn.Del(&dr); err != nil {
return err
}
// TODO update any users that are member of this school
return nil
}
// GetEducationSchool implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrID string) (*libregraph.EducationSchool, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetEducationSchool")
e, err := i.getSchoolByNumberOrID(numberOrID)
if err != nil {
return nil, err
}
return i.createSchoolModelFromLDAP(e), nil
}
// GetEducationSchools implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error) {
filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass)
if i.educationConfig.schoolFilter != "" {
filter = fmt.Sprintf("(&%s%s)", i.educationConfig.schoolFilter, filter)
}
return i.searchEducationSchools(ctx, filter)
}
// FilterEducationSchoolsByAttribute implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) FilterEducationSchoolsByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationSchool, error) {
logger := i.logger.SubloggerWithRequestID(ctx).With().Str("func", "FilterEducationSchoolsByAttribute").Logger()
logger.Debug().Str("backend", "ldap").Str("attribute", attr).Str("value", value).Msg("")
var ldapAttr string
switch attr {
case "externalId":
ldapAttr = i.educationConfig.schoolAttributeMap.externalId
default:
return nil, errorcode.New(errorcode.InvalidRequest, fmt.Sprintf("filtering by attribute '%s' is not supported", attr))
}
filter := fmt.Sprintf("(&%s(objectClass=%s)(%s=%s))",
i.educationConfig.schoolFilter,
i.educationConfig.schoolObjectClass,
ldap.EscapeFilter(ldapAttr),
ldap.EscapeFilter(value),
)
return i.searchEducationSchools(ctx, filter)
}
// searchEducationSchools builds and executes an LDAP search for education schools and converts the results to EducationSchool models.
func (i *LDAP) searchEducationSchools(ctx context.Context, filter string) ([]*libregraph.EducationSchool, error) {
searchRequest := ldap.NewSearchRequest(
i.educationConfig.schoolBaseDN,
i.educationConfig.schoolScope,
ldap.NeverDerefAliases, 0, 0, false,
filter,
i.getEducationSchoolAttrTypes(),
nil,
)
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").
Str("base", searchRequest.BaseDN).
Str("filter", searchRequest.Filter).
Int("scope", searchRequest.Scope).
Int("sizelimit", searchRequest.SizeLimit).
Interface("attributes", searchRequest.Attributes).
Msg("searchEducationSchools")
res, err := i.conn.Search(searchRequest)
if err != nil {
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
}
schools := make([]*libregraph.EducationSchool, 0, len(res.Entries))
for _, e := range res.Entries {
school := i.createSchoolModelFromLDAP(e)
// Skip invalid LDAP entries
if school == nil {
continue
}
schools = append(schools, school)
}
return schools, nil
}
// GetEducationSchoolUsers implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationSchoolUsers(ctx context.Context, schoolNumberOrID string) ([]*libregraph.EducationUser, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetEducationSchoolUsers")
entries, err := i.getEducationSchoolEntries(
schoolNumberOrID, i.userFilter, i.educationConfig.userObjectClass, i.userBaseDN, i.userScope, i.getEducationUserAttrTypes(), logger,
)
if err != nil {
return nil, err
}
users := make([]*libregraph.EducationUser, 0, len(entries))
for _, e := range entries {
u := i.createEducationUserModelFromLDAP(e)
// Skip invalid LDAP users
if u == nil {
continue
}
users = append(users, u)
}
return users, nil
}
// AddUsersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
func (i *LDAP) AddUsersToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("AddUsersToEducationSchool")
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
if err != nil {
return err
}
if schoolEntry == nil {
return ErrNotFound
}
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
userEntries := make([]*ldap.Entry, 0, len(memberIDs))
for _, memberID := range memberIDs {
user, err := i.getEducationUserByNameOrID(memberID)
if err != nil {
i.logger.Warn().Str("userid", memberID).Msg("User does not exist")
return errorcode.New(errorcode.ItemNotFound, fmt.Sprintf("user '%s' not found", memberID))
}
userEntries = append(userEntries, user)
}
for _, userEntry := range userEntries {
if err := i.addEntryToSchool(userEntry, schoolID); err != nil {
return err
}
}
return nil
}
// addEntryToSchool adds the schoolID to the entry's memberOfSchool attribute if not already present.
func (i *LDAP) addEntryToSchool(entry *ldap.Entry, schoolID string) error {
currentSchools := entry.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
if slices.Contains(currentSchools, schoolID) {
return nil
}
mr := ldap.ModifyRequest{DN: entry.DN}
mr.Add(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
return i.conn.Modify(&mr)
}
// RemoveUserFromEducationSchool removes a single member (by ID) from a school
func (i *LDAP) RemoveUserFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("RemoveUserFromEducationSchool")
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
if err != nil {
return err
}
if schoolEntry == nil {
return ErrNotFound
}
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
user, err := i.getEducationUserByNameOrID(memberID)
if err != nil {
i.logger.Warn().Str("userid", memberID).Msg("User does not exist")
return err
}
currentSchools := user.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
for _, currentSchool := range currentSchools {
if currentSchool == schoolID {
mr := ldap.ModifyRequest{DN: user.DN}
mr.Delete(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
if err := i.conn.Modify(&mr); err != nil {
return err
}
break
}
}
return nil
}
// GetEducationSchoolClasses implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationSchoolClasses(ctx context.Context, schoolNumberOrID string) ([]*libregraph.EducationClass, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetEducationSchoolClasses")
entries, err := i.getEducationSchoolEntries(
schoolNumberOrID, i.groupFilter, i.educationConfig.classObjectClass, i.groupBaseDN, i.groupScope, i.getEducationClassAttrTypes(false), logger,
)
if err != nil {
return nil, err
}
classes := make([]*libregraph.EducationClass, 0, len(entries))
for _, e := range entries {
class := i.createEducationClassModelFromLDAP(e)
// Skip invalid LDAP classes
if class == nil {
continue
}
classes = append(classes, class)
}
return classes, nil
}
func (i *LDAP) getEducationSchoolEntries(
schoolNumberOrID, filter, objectClass, baseDN string,
scope int,
attributes []string,
logger log.Logger,
) ([]*ldap.Entry, error) {
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
if err != nil {
return nil, err
}
if schoolEntry == nil {
return nil, ErrNotFound
}
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
schoolID = ldap.EscapeFilter(schoolID)
idFilter := fmt.Sprintf("(%s=%s)", i.educationConfig.memberOfSchoolAttribute, schoolID)
searchFilter := fmt.Sprintf("(&%s(objectClass=%s)%s)", filter, objectClass, idFilter)
searchRequest := ldap.NewSearchRequest(
baseDN,
scope,
ldap.NeverDerefAliases, 0, 0, false,
searchFilter,
attributes,
nil,
)
logger.Debug().Str("backend", "ldap").
Str("base", searchRequest.BaseDN).
Str("filter", searchRequest.Filter).
Int("scope", searchRequest.Scope).
Int("sizelimit", searchRequest.SizeLimit).
Interface("attributes", searchRequest.Attributes).
Msg("GetEducationClasses")
res, err := i.conn.Search(searchRequest)
if err != nil {
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
}
return res.Entries, nil
}
// AddClassesToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
func (i *LDAP) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("AddClassesToEducationSchool")
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
if err != nil {
return err
}
if schoolEntry == nil {
return ErrNotFound
}
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
classEntries := make([]*ldap.Entry, 0, len(memberIDs))
for _, memberID := range memberIDs {
class, err := i.getEducationClassByID(memberID, false)
if err != nil {
i.logger.Warn().Str("userid", memberID).Msg("Class does not exist")
return err
}
classEntries = append(classEntries, class)
}
for _, classEntry := range classEntries {
if err := i.addEntryToSchool(classEntry, schoolID); err != nil {
return err
}
}
return nil
}
// RemoveClassFromEducationSchool removes a single member (by ID) from a school
func (i *LDAP) RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("RemoveClassFromEducationSchool")
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
if err != nil {
return err
}
if schoolEntry == nil {
return ErrNotFound
}
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
class, err := i.getEducationClassByID(memberID, false)
if err != nil {
i.logger.Warn().Str("userid", memberID).Msg("Class does not exist")
return err
}
currentSchools := class.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
for _, currentSchool := range currentSchools {
if currentSchool == schoolID {
mr := ldap.ModifyRequest{DN: class.DN}
mr.Delete(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
if err := i.conn.Modify(&mr); err != nil {
return err
}
break
}
}
return nil
}
func (i *LDAP) getSchoolByDN(dn string) (*ldap.Entry, error) {
filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass)
if i.educationConfig.schoolFilter != "" {
filter = fmt.Sprintf("(&%s(%s))", filter, i.educationConfig.schoolFilter)
}
return i.getEntryByDN(dn, i.getEducationSchoolAttrTypes(), filter)
}
func (i *LDAP) getSchoolByNumberOrID(numberOrID string) (*ldap.Entry, error) {
numberOrID = ldap.EscapeFilter(numberOrID)
filter := fmt.Sprintf(
"(|(%s=%s)(%s=%s))",
i.educationConfig.schoolAttributeMap.id,
numberOrID,
i.educationConfig.schoolAttributeMap.schoolNumber,
numberOrID,
)
return i.getSchoolByFilter(filter)
}
func (i *LDAP) getSchoolByNumber(schoolNumber string) (*ldap.Entry, error) {
schoolNumber = ldap.EscapeFilter(schoolNumber)
filter := fmt.Sprintf(
"(%s=%s)",
i.educationConfig.schoolAttributeMap.schoolNumber,
schoolNumber,
)
return i.getSchoolByFilter(filter)
}
func (i *LDAP) getSchoolByFilter(filter string) (*ldap.Entry, error) {
filter = fmt.Sprintf("(&%s(objectClass=%s)%s)",
i.educationConfig.schoolFilter,
i.educationConfig.schoolObjectClass,
filter,
)
searchRequest := ldap.NewSearchRequest(
i.educationConfig.schoolBaseDN,
i.educationConfig.schoolScope,
ldap.NeverDerefAliases, 1, 0, false,
filter,
i.getEducationSchoolAttrTypes(),
nil,
)
i.logger.Debug().Str("backend", "ldap").
Str("base", searchRequest.BaseDN).
Str("filter", searchRequest.Filter).
Int("scope", searchRequest.Scope).
Int("sizelimit", searchRequest.SizeLimit).
Interface("attributes", searchRequest.Attributes).
Msg("getSchoolByFilter")
res, err := i.conn.Search(searchRequest)
if err != nil {
var errmsg string
if lerr, ok := err.(*ldap.Error); ok {
if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
errmsg = fmt.Sprintf("too many results searching for school '%s'", filter)
i.logger.Debug().Str("backend", "ldap").Err(lerr).
Str("schoolfilter", filter).Msg("too many results searching for school")
}
}
return nil, errorcode.New(errorcode.ItemNotFound, errmsg)
}
if len(res.Entries) == 0 {
return nil, ErrNotFound
}
return res.Entries[0], nil
}
func (i *LDAP) createSchoolModelFromLDAP(e *ldap.Entry) *libregraph.EducationSchool {
if e == nil {
return nil
}
displayName := i.getDisplayName(e)
id := i.getID(e)
schoolNumber := i.getSchoolNumber(e)
externalId := i.getExternalId(e)
t, err := i.getTerminationDate(e)
if err != nil && !errors.Is(err, errNotSet) {
i.logger.Error().Err(err).Str("dn", e.DN).Msg("Error reading termination date for LDAP entry")
}
if id == "" || displayName == "" {
i.logger.Warn().Str("dn", e.DN).Str("id", id).Str("displayName", displayName).Msg("Invalid School. Missing required attribute")
return nil
}
school := libregraph.NewEducationSchool()
school.SetDisplayName(displayName)
school.SetId(id)
if schoolNumber != "" {
school.SetSchoolNumber(schoolNumber)
}
if externalId != "" {
school.SetExternalId(externalId)
}
if t != nil {
school.SetTerminationDate(*t)
}
return school
}
func (i *LDAP) getSchoolNumber(e *ldap.Entry) string {
schoolNumber := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.schoolNumber)
return schoolNumber
}
func (i *LDAP) getExternalId(e *ldap.Entry) string {
externalId := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.externalId)
return externalId
}
func (i *LDAP) getID(e *ldap.Entry) string {
id := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
return id
}
func (i *LDAP) getDisplayName(e *ldap.Entry) string {
displayName := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.displayName)
return displayName
}
func (i *LDAP) getTerminationDate(e *ldap.Entry) (*time.Time, error) {
dateString := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.terminationDate)
if dateString == "" {
return nil, errNotSet
}
t, err := time.Parse(ldapDateFormat, dateString)
if err != nil {
err = fmt.Errorf("error parsing LDAP date: '%s': %w", dateString, err)
return nil, err
}
return &t, nil
}
func (i *LDAP) getEducationSchoolAttrTypes() []string {
return []string{
i.educationConfig.schoolAttributeMap.displayName,
i.educationConfig.schoolAttributeMap.id,
i.educationConfig.schoolAttributeMap.externalId,
i.educationConfig.schoolAttributeMap.schoolNumber,
i.educationConfig.schoolAttributeMap.terminationDate,
}
}
@@ -0,0 +1,722 @@
package identity
import (
"context"
"errors"
"testing"
"time"
"github.com/go-ldap/ldap/v3"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/errorcode"
"github.com/qsfera/server/services/graph/pkg/identity/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var eduConfig = config.LDAP{
UserBaseDN: "ou=people,dc=test",
UserObjectClass: "inetOrgPerson",
UserSearchScope: "sub",
UserFilter: "",
UserDisplayNameAttribute: "displayname",
UserIDAttribute: "entryUUID",
UserEmailAttribute: "mail",
UserNameAttribute: "uid",
UserEnabledAttribute: "userEnabledAttribute",
DisableUserMechanism: "attribute",
UserTypeAttribute: "userTypeAttribute",
GroupBaseDN: "ou=groups,dc=test",
GroupObjectClass: "groupOfNames",
GroupSearchScope: "sub",
GroupFilter: "",
GroupNameAttribute: "cn",
GroupMemberAttribute: "member",
GroupIDAttribute: "entryUUID",
WriteEnabled: true,
EducationResourcesEnabled: true,
}
var schoolEntry = ldap.NewEntry("ou=Test School",
map[string][]string{
"ou": {"Test School"},
"qsferaEducationSchoolNumber": {"0123"},
"qsferaUUID": {"abcd-defg"},
"qsferaEducationExternalId": {"abcd-defg"},
})
var schoolEntry1 = ldap.NewEntry("ou=Test School1",
map[string][]string{
"ou": {"Test School1"},
"qsferaEducationSchoolNumber": {"0042"},
"qsferaUUID": {"hijk-defg"},
"qsferaEducationExternalId": {"hijk-defg"},
})
var schoolEntryWithTermination = ldap.NewEntry("ou=Test School",
map[string][]string{
"ou": {"Test School"},
"qsferaEducationSchoolNumber": {"0123"},
"qsferaUUID": {"abcd-defg"},
"qsferaEducationExternalId": {"abcd-defg"},
"qsferaEducationSchoolTerminationTimestamp": {"20420131120000Z"},
})
var (
filterSchoolSearchByIdExisting = "(&(objectClass=qsferaEducationSchool)(|(qsferaUUID=abcd-defg)(qsferaEducationSchoolNumber=abcd-defg)))"
filterSchoolSearchByIdNonexistant = "(&(objectClass=qsferaEducationSchool)(|(qsferaUUID=xxxx-xxxx)(qsferaEducationSchoolNumber=xxxx-xxxx)))"
filterSchoolSearchByNumberExisting = "(&(objectClass=qsferaEducationSchool)(|(qsferaUUID=0123)(qsferaEducationSchoolNumber=0123)))"
filterSchoolSearchByNumberNonexistant = "(&(objectClass=qsferaEducationSchool)(|(qsferaUUID=3210)(qsferaEducationSchoolNumber=3210)))"
schoolLDAPAttributeTypes = []string{"ou", "qsferaUUID", "qsferaEducationExternalId", "qsferaEducationSchoolNumber", "qsferaEducationSchoolTerminationTimestamp"}
)
func TestCreateEducationSchool(t *testing.T) {
tests := []struct {
name string
schoolNumber string
schoolName string
expectedError error
}{
{
name: "Create a Education School succeeds",
schoolNumber: "0123",
schoolName: "Test School",
expectedError: nil,
}, {
name: "Create a Education School with a duplicated Schoolnumber fails with an error",
schoolNumber: "0666",
schoolName: "Test School",
expectedError: errorcode.New(errorcode.NameAlreadyExists, "A school with that number is already present"),
}, {
name: "Create a Education School with a duplicated Name fails with an error",
schoolNumber: "0123",
schoolName: "Existing Test School",
expectedError: errorcode.New(errorcode.NameAlreadyExists, "A school with that name is already present"),
}, {
name: "Create a Education School fails, when there is a backend error",
schoolNumber: "1111",
schoolName: "Test School",
expectedError: errorcode.New(errorcode.GeneralException, "error looking up school by number"),
},
}
for _, tt := range tests {
lm := &mocks.Client{}
ldapSchoolGoodAddRequestMatcher := func(ar *ldap.AddRequest) bool {
if ar.DN != "ou=Test School," {
return false
}
for _, attr := range ar.Attributes {
if attr.Type == "qsferaEducationSchoolTerminationTimestamp" {
return false
}
}
return true
}
lm.On("Add", mock.MatchedBy(ldapSchoolGoodAddRequestMatcher)).Return(nil)
ldapExistingSchoolAddRequestMatcher := func(ar *ldap.AddRequest) bool {
if ar.DN == "ou=Existing Test School," {
return true
}
return false
}
lm.On("Add", mock.MatchedBy(ldapExistingSchoolAddRequestMatcher)).Return(ldap.NewError(ldap.LDAPResultEntryAlreadyExists, errors.New("")))
schoolNumberSearchRequest := &ldap.SearchRequest{
BaseDN: "",
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=qsferaEducationSchool)(qsferaEducationSchoolNumber=0123))",
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
lm.On("Search", schoolNumberSearchRequest).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{},
},
nil)
existingSchoolNumberSearchRequest := &ldap.SearchRequest{
BaseDN: "",
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=qsferaEducationSchool)(qsferaEducationSchoolNumber=0666))",
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
lm.On("Search", existingSchoolNumberSearchRequest).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{schoolEntry},
},
nil)
schoolNumberSearchRequestError := &ldap.SearchRequest{
BaseDN: "",
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=qsferaEducationSchool)(qsferaEducationSchoolNumber=1111))",
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
lm.On("Search", schoolNumberSearchRequestError).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{},
},
ldap.NewError(ldap.LDAPResultOther, errors.New("some error")))
schoolLookupAfterCreate := &ldap.SearchRequest{
BaseDN: "ou=Test School,",
Scope: 0,
SizeLimit: 1,
Filter: "(objectClass=qsferaEducationSchool)",
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
lm.On("Search", schoolLookupAfterCreate).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{schoolEntry},
},
nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
assert.NotEqual(t, "", b.educationConfig.schoolObjectClass)
school := libregraph.NewEducationSchool()
school.SetDisplayName(tt.schoolName)
school.SetSchoolNumber(tt.schoolNumber)
school.SetId("abcd-defg")
resSchool, err := b.CreateEducationSchool(context.Background(), *school)
if tt.expectedError == nil {
assert.Nil(t, err)
lm.AssertNumberOfCalls(t, "Add", 1)
assert.NotNil(t, resSchool)
assert.Equal(t, resSchool.GetDisplayName(), school.GetDisplayName())
assert.Equal(t, resSchool.GetId(), school.GetId())
assert.Equal(t, resSchool.GetSchoolNumber(), school.GetSchoolNumber())
assert.False(t, resSchool.HasTerminationDate())
} else {
assert.Equal(t, err, tt.expectedError)
assert.Nil(t, resSchool)
}
}
}
func TestUpdateEducationSchoolTerminationDate(t *testing.T) {
lm := &mocks.Client{}
ldapSchoolTerminationRequestMatcher := func(mr *ldap.ModifyRequest) bool {
if mr.DN != "ou=Test School" {
return false
}
for _, mod := range mr.Changes {
if mod.Operation == ldap.ReplaceAttribute &&
mod.Modification.Type == "qsferaEducationSchoolTerminationTimestamp" &&
mod.Modification.Vals[0] == "20420131120000Z" {
return true
}
}
return false
}
lm.On("Modify", mock.MatchedBy(ldapSchoolTerminationRequestMatcher)).Return(nil)
lm.On("Search", mock.Anything).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{schoolEntry},
},
nil).
Once()
lm.On("Search", mock.Anything).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{schoolEntryWithTermination},
},
nil).
Once()
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
assert.NotEqual(t, "", b.educationConfig.schoolObjectClass)
school := libregraph.NewEducationSchool()
terminationTime := time.Date(2042, time.January, 31, 12, 0, 0, 0, time.UTC)
school.SetTerminationDate(terminationTime)
resSchool, err := b.UpdateEducationSchool(context.Background(), "abcd-defg", *school)
lm.AssertNumberOfCalls(t, "Search", 2)
assert.Nil(t, err)
assert.NotNil(t, resSchool)
assert.Equal(t, "Test School", resSchool.GetDisplayName())
assert.Equal(t, "abcd-defg", resSchool.GetId())
assert.Equal(t, "0123", resSchool.GetSchoolNumber())
assert.True(t, resSchool.HasTerminationDate())
assert.True(t, terminationTime.Equal(resSchool.GetTerminationDate()))
}
func TestUpdateEducationSchoolOperation(t *testing.T) {
testSchoolName := "A name"
testSchoolNumber := "1234"
tests := []struct {
name string
displayName string
schoolNumber string
expectedOperation schoolUpdateOperation
}{
{
name: "Test using school with both number and name, unchanged",
displayName: testSchoolName,
schoolNumber: testSchoolNumber,
expectedOperation: schoolUnchanged,
},
{
name: "Test using school with both number and name, unchanged",
displayName: "A new name",
schoolNumber: "9876",
expectedOperation: tooManyValues,
},
{
name: "Test with unchanged number",
schoolNumber: testSchoolNumber,
expectedOperation: schoolUnchanged,
},
{
name: "Test with unchanged name",
displayName: testSchoolName,
expectedOperation: schoolUnchanged,
},
{
name: "Test new name",
displayName: "Something new",
expectedOperation: schoolRenamed,
},
{
name: "Test new number",
schoolNumber: "9876",
expectedOperation: schoolPropertiesUpdated,
},
}
for _, tt := range tests {
lm := &mocks.Client{}
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
displayName := "A name"
schoolNumber := "1234"
currentSchool := libregraph.EducationSchool{
DisplayName: &displayName,
SchoolNumber: &schoolNumber,
}
schoolUpdate := libregraph.EducationSchool{
DisplayName: &tt.displayName,
SchoolNumber: &tt.schoolNumber,
}
operation := b.updateEducationSchoolOperation(schoolUpdate, currentSchool)
assert.Equal(t, tt.expectedOperation, operation)
}
}
func TestDeleteEducationSchool(t *testing.T) {
tests := []struct {
name string
numberOrId string
filter string
expectedItemNotFound bool
}{
{
name: "Test delete school using schoolId",
numberOrId: "abcd-defg",
filter: filterSchoolSearchByIdExisting,
expectedItemNotFound: false,
},
{
name: "Test delete school using unknown schoolId",
numberOrId: "xxxx-xxxx",
filter: filterSchoolSearchByIdNonexistant,
expectedItemNotFound: true,
},
{
name: "Test delete school using schoolNumber",
numberOrId: "0123",
filter: filterSchoolSearchByNumberExisting,
expectedItemNotFound: false,
},
{
name: "Test delete school using unknown schoolNumber",
numberOrId: "3210",
filter: filterSchoolSearchByNumberNonexistant,
expectedItemNotFound: true,
},
}
for _, tt := range tests {
lm := &mocks.Client{}
sr := &ldap.SearchRequest{
BaseDN: "",
Scope: 2,
SizeLimit: 1,
Filter: tt.filter,
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
if tt.expectedItemNotFound {
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
} else {
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
}
dr := &ldap.DelRequest{
DN: "ou=Test School",
}
lm.On("Del", dr).Return(nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
err = b.DeleteEducationSchool(context.Background(), tt.numberOrId)
lm.AssertNumberOfCalls(t, "Search", 1)
if tt.expectedItemNotFound {
lm.AssertNumberOfCalls(t, "Del", 0)
assert.NotNil(t, err)
assert.Equal(t, "itemNotFound: not found", err.Error())
} else {
assert.Nil(t, err)
}
}
}
func TestGetEducationSchool(t *testing.T) {
tests := []struct {
name string
numberOrId string
filter string
expectedItemNotFound bool
}{
{
name: "Test search school using schoolId",
numberOrId: "abcd-defg",
filter: filterSchoolSearchByIdExisting,
expectedItemNotFound: false,
},
{
name: "Test search school using unknown schoolId",
numberOrId: "xxxx-xxxx",
filter: filterSchoolSearchByIdNonexistant,
expectedItemNotFound: true,
},
{
name: "Test search school using schoolNumber",
numberOrId: "0123",
filter: filterSchoolSearchByNumberExisting,
expectedItemNotFound: false,
},
{
name: "Test search school using unknown schoolNumber",
numberOrId: "3210",
filter: filterSchoolSearchByNumberNonexistant,
expectedItemNotFound: true,
},
}
for _, tt := range tests {
lm := &mocks.Client{}
sr := &ldap.SearchRequest{
BaseDN: "",
Scope: 2,
SizeLimit: 1,
Filter: tt.filter,
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
if tt.expectedItemNotFound {
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
} else {
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
}
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
school, err := b.GetEducationSchool(context.Background(), tt.numberOrId)
lm.AssertNumberOfCalls(t, "Search", 1)
if tt.expectedItemNotFound {
assert.NotNil(t, err)
assert.Equal(t, "itemNotFound: not found", err.Error())
} else {
assert.Nil(t, err)
assert.Equal(t, "Test School", school.GetDisplayName())
assert.Equal(t, "abcd-defg", school.GetId())
assert.Equal(t, "0123", school.GetSchoolNumber())
}
}
}
func TestGetEducationSchools(t *testing.T) {
lm := &mocks.Client{}
sr1 := &ldap.SearchRequest{
BaseDN: "",
Scope: 2,
SizeLimit: 0,
Filter: "(objectClass=qsferaEducationSchool)",
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry, schoolEntry1}}, nil)
// lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
_, err = b.GetEducationSchools(context.Background())
lm.AssertNumberOfCalls(t, "Search", 1)
assert.Nil(t, err)
}
var schoolByIDSearch1 *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: "",
Scope: 2,
SizeLimit: 1,
Filter: filterSchoolSearchByIdExisting,
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
var schoolByNumberSearch *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: "",
Scope: 2,
SizeLimit: 1,
Filter: filterSchoolSearchByNumberExisting,
Attributes: schoolLDAPAttributeTypes,
Controls: []ldap.Control(nil),
}
var userByIDSearch1 *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: "ou=people,dc=test",
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=qsferaEducationUser)(|(uid=abcd-defg)(entryUUID=abcd-defg)))",
Attributes: eduUserAttrs,
Controls: []ldap.Control(nil),
}
var userByIDSearch2 *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: "ou=people,dc=test",
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=qsferaEducationUser)(|(uid=does-not-exist)(entryUUID=does-not-exist)))",
Attributes: eduUserAttrs,
Controls: []ldap.Control(nil),
}
var userToSchoolModRequest *ldap.ModifyRequest = &ldap.ModifyRequest{
DN: "uid=user,ou=people,dc=test",
Changes: []ldap.Change{
{
Operation: ldap.AddAttribute,
Modification: ldap.PartialAttribute{
Type: "qsferaMemberOfSchool",
Vals: []string{"abcd-defg"},
},
},
},
}
var userFromSchoolModRequest *ldap.ModifyRequest = &ldap.ModifyRequest{
DN: "uid=user,ou=people,dc=test",
Changes: []ldap.Change{
{
Operation: ldap.DeleteAttribute,
Modification: ldap.PartialAttribute{
Type: "qsferaMemberOfSchool",
Vals: []string{"abcd-defg"},
},
},
},
}
var classToSchoolModRequest *ldap.ModifyRequest = &ldap.ModifyRequest{
DN: "qsferaEducationExternalId=Math0123",
Changes: []ldap.Change{
{
Operation: ldap.AddAttribute,
Modification: ldap.PartialAttribute{
Type: "qsferaMemberOfSchool",
Vals: []string{"abcd-defg"},
},
},
},
}
var classFromSchoolModRequest *ldap.ModifyRequest = &ldap.ModifyRequest{
DN: "qsferaEducationExternalId=Math0123",
Changes: []ldap.Change{
{
Operation: ldap.DeleteAttribute,
Modification: ldap.PartialAttribute{
Type: "qsferaMemberOfSchool",
Vals: []string{"abcd-defg"},
},
},
},
}
func TestAddUsersToEducationSchool(t *testing.T) {
lm := &mocks.Client{}
lm.On("Search", schoolByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", schoolByNumberSearch).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", userByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
lm.On("Search", userByIDSearch2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
lm.On("Modify", userToSchoolModRequest).Return(nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
err = b.AddUsersToEducationSchool(context.Background(), "abcd-defg", []string{"does-not-exist"})
lm.AssertNumberOfCalls(t, "Search", 2)
assert.NotNil(t, err)
err = b.AddUsersToEducationSchool(context.Background(), "abcd-defg", []string{"abcd-defg", "does-not-exist"})
lm.AssertNumberOfCalls(t, "Search", 5)
assert.NotNil(t, err)
err = b.AddUsersToEducationSchool(context.Background(), "abcd-defg", []string{"abcd-defg"})
lm.AssertNumberOfCalls(t, "Search", 7)
assert.Nil(t, err)
// try to add by school number (instead or id)
err = b.AddUsersToEducationSchool(context.Background(), "0123", []string{"abcd-defg"})
lm.AssertNumberOfCalls(t, "Search", 9)
assert.Nil(t, err)
}
func TestRemoveMemberFromEducationSchool(t *testing.T) {
lm := &mocks.Client{}
lm.On("Search", schoolByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", schoolByNumberSearch).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", userByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntryWithSchool}}, nil)
lm.On("Search", userByIDSearch2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
lm.On("Modify", userFromSchoolModRequest).Return(nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
err = b.RemoveUserFromEducationSchool(context.Background(), "abcd-defg", "does-not-exist")
lm.AssertNumberOfCalls(t, "Search", 2)
assert.NotNil(t, err)
assert.Equal(t, "itemNotFound: not found", err.Error())
err = b.RemoveUserFromEducationSchool(context.Background(), "abcd-defg", "abcd-defg")
lm.AssertNumberOfCalls(t, "Search", 4)
lm.AssertNumberOfCalls(t, "Modify", 1)
// try to remove by school number (instead or id)
err = b.RemoveUserFromEducationSchool(context.Background(), "0123", "abcd-defg")
lm.AssertNumberOfCalls(t, "Search", 6)
lm.AssertNumberOfCalls(t, "Modify", 2)
assert.Nil(t, err)
}
var usersBySchoolIDSearch *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: "ou=people,dc=test",
Scope: 2,
SizeLimit: 0,
Filter: "(&(objectClass=qsferaEducationUser)(qsferaMemberOfSchool=abcd-defg))",
Attributes: eduUserAttrs,
Controls: []ldap.Control(nil),
}
func TestGetEducationSchoolUsers(t *testing.T) {
lm := &mocks.Client{}
lm.On("Search", schoolByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", schoolByNumberSearch).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", usersBySchoolIDSearch).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntryWithSchool}}, nil)
b, _ := getMockedBackend(lm, eduConfig, &logger)
users, err := b.GetEducationSchoolUsers(context.Background(), "abcd-defg")
assert.Nil(t, err)
assert.Equal(t, 1, len(users))
users, err = b.GetEducationSchoolUsers(context.Background(), "0123")
assert.Nil(t, err)
assert.Equal(t, 1, len(users))
}
var classesBySchoolIDSearch *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: "ou=groups,dc=test",
Scope: 2,
SizeLimit: 0,
Filter: "(&(objectClass=qsferaEducationClass)(qsferaMemberOfSchool=abcd-defg))",
Attributes: []string{"cn", "entryUUID", "qsferaEducationClassType", "qsferaEducationExternalId", "qsferaMemberOfSchool", "qsferaEducationTeacherMember"},
Controls: []ldap.Control(nil),
}
func TestGetEducationSchoolClasses(t *testing.T) {
lm := &mocks.Client{}
lm.On("Search", schoolByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", schoolByNumberSearch).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", classesBySchoolIDSearch).Return(&ldap.SearchResult{Entries: []*ldap.Entry{classEntry}}, nil)
b, _ := getMockedBackend(lm, eduConfig, &logger)
users, err := b.GetEducationSchoolClasses(context.Background(), "abcd-defg")
assert.Nil(t, err)
assert.Equal(t, 1, len(users))
users, err = b.GetEducationSchoolClasses(context.Background(), "0123")
assert.Nil(t, err)
assert.Equal(t, 1, len(users))
}
var classesByUUIDSearchNotFound *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: "ou=groups,dc=test",
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=does-not-exist)(qsferaEducationExternalId=does-not-exist)))",
Attributes: []string{"cn", "entryUUID", "qsferaEducationClassType", "qsferaEducationExternalId", "qsferaMemberOfSchool", "qsferaEducationTeacherMember"},
Controls: []ldap.Control(nil),
}
var classesByUUIDSearchFound *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: "ou=groups,dc=test",
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=qsferaEducationClass)(|(entryUUID=abcd-defg)(qsferaEducationExternalId=abcd-defg)))",
Attributes: []string{"cn", "entryUUID", "qsferaEducationClassType", "qsferaEducationExternalId", "qsferaMemberOfSchool", "qsferaEducationTeacherMember"},
Controls: []ldap.Control(nil),
}
func TestAddClassesToEducationSchool(t *testing.T) {
lm := &mocks.Client{}
lm.On("Search", classesByUUIDSearchNotFound).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
lm.On("Search", classesByUUIDSearchFound).Return(&ldap.SearchResult{Entries: []*ldap.Entry{classEntry}}, nil)
lm.On("Search", schoolByNumberSearch).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", schoolByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Modify", classToSchoolModRequest).Return(nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
err = b.AddClassesToEducationSchool(context.Background(), "abcd-defg", []string{"does-not-exist"})
lm.AssertNumberOfCalls(t, "Search", 2)
assert.NotNil(t, err)
err = b.AddClassesToEducationSchool(context.Background(), "abcd-defg", []string{"abcd-defg", "does-not-exist"})
lm.AssertNumberOfCalls(t, "Search", 5)
assert.NotNil(t, err)
err = b.AddClassesToEducationSchool(context.Background(), "abcd-defg", []string{"abcd-defg"})
lm.AssertNumberOfCalls(t, "Search", 7)
assert.Nil(t, err)
// try to add by school number (instead or id)
err = b.AddClassesToEducationSchool(context.Background(), "0123", []string{"abcd-defg"})
lm.AssertNumberOfCalls(t, "Search", 9)
assert.Nil(t, err)
}
func TestRemoveClassFromEducationSchool(t *testing.T) {
lm := &mocks.Client{}
lm.On("Search", schoolByIDSearch1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", schoolByNumberSearch).Return(&ldap.SearchResult{Entries: []*ldap.Entry{schoolEntry}}, nil)
lm.On("Search", classesByUUIDSearchFound).Return(&ldap.SearchResult{Entries: []*ldap.Entry{classEntryWithSchool}}, nil)
lm.On("Search", classesByUUIDSearchNotFound).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
lm.On("Modify", classFromSchoolModRequest).Return(nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
err = b.RemoveClassFromEducationSchool(context.Background(), "abcd-defg", "does-not-exist")
lm.AssertNumberOfCalls(t, "Search", 2)
assert.NotNil(t, err)
assert.Equal(t, "itemNotFound: not found", err.Error())
err = b.RemoveClassFromEducationSchool(context.Background(), "abcd-defg", "abcd-defg")
lm.AssertNumberOfCalls(t, "Search", 4)
lm.AssertNumberOfCalls(t, "Modify", 1)
// try to remove by school number (instead or id)
err = b.RemoveClassFromEducationSchool(context.Background(), "0123", "abcd-defg")
lm.AssertNumberOfCalls(t, "Search", 6)
lm.AssertNumberOfCalls(t, "Modify", 2)
assert.Nil(t, err)
}
@@ -0,0 +1,400 @@
package identity
import (
"context"
"errors"
"fmt"
"github.com/go-ldap/ldap/v3"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/errorcode"
)
type educationUserAttributeMap struct {
primaryRole string
externalID string
}
func newEducationUserAttributeMap() educationUserAttributeMap {
return educationUserAttributeMap{
primaryRole: "userClass",
externalID: "qsferaEducationExternalId",
}
}
// CreateEducationUser creates a given education user in the identity backend.
func (i *LDAP) CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("CreateEducationUser")
if !i.writeEnabled {
return nil, ErrReadOnly
}
ar, err := i.educationUserToAddRequest(user)
if err != nil {
return nil, err
}
if err = i.conn.Add(ar); err != nil {
var lerr *ldap.Error
logger.Debug().Err(err).Msg("error adding user")
if errors.As(err, &lerr) {
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
}
}
return nil, err
}
// Read back user from LDAP to get the generated UUID
e, err := i.getEducationUserByDN(ar.DN)
if err != nil {
return nil, err
}
return i.createEducationUserModelFromLDAP(e), nil
}
// DeleteEducationUser deletes a given education user, identified by username or id, from the backend
func (i *LDAP) DeleteEducationUser(ctx context.Context, nameOrID string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("DeleteEducationUser")
if !i.writeEnabled {
return ErrReadOnly
}
// TODO, implement a proper lookup for education Users here
e, err := i.getEducationUserByNameOrID(nameOrID)
if err != nil {
return err
}
dr := ldap.DelRequest{DN: e.DN}
if err = i.conn.Del(&dr); err != nil {
return err
}
return nil
}
// UpdateEducationUser applies changes to given education user, identified by username or id
func (i *LDAP) UpdateEducationUser(ctx context.Context, nameOrID string, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("UpdateEducationUser")
if !i.writeEnabled {
return nil, ErrReadOnly
}
e, err := i.getEducationUserByNameOrID(nameOrID)
if err != nil {
return nil, err
}
var updateNeeded bool
// Don't allow updates of the ID
if user.GetId() != "" {
id, err := i.ldapUUIDtoString(e, i.userAttributeMap.id, i.userIDisOctetString)
if err != nil {
i.logger.Warn().Str("dn", e.DN).Str(i.userAttributeMap.id, e.GetEqualFoldAttributeValue(i.userAttributeMap.id)).Msg("Invalid User. Cannot convert UUID")
return nil, errorcode.New(errorcode.GeneralException, "error converting uuid")
}
if id != user.GetId() {
return nil, errorcode.New(errorcode.NotAllowed, "changing the UserId is not allowed")
}
}
if user.GetOnPremisesSamAccountName() != "" {
if eu := e.GetEqualFoldAttributeValue(i.userAttributeMap.userName); eu != user.GetOnPremisesSamAccountName() {
e, err = i.changeUserName(ctx, e.DN, eu, user.GetOnPremisesSamAccountName())
if err != nil {
return nil, err
}
e, err = i.getEducationUserByDN(e.DN)
if err != nil {
return nil, err
}
}
}
mr := ldap.ModifyRequest{DN: e.DN}
properties := map[string]string{
i.userAttributeMap.displayName: user.GetDisplayName(),
i.userAttributeMap.mail: user.GetMail(),
i.userAttributeMap.surname: user.GetSurname(),
i.userAttributeMap.givenName: user.GetGivenName(),
i.userAttributeMap.userType: user.GetUserType(),
i.educationConfig.userAttributeMap.primaryRole: user.GetPrimaryRole(),
i.educationConfig.userAttributeMap.externalID: user.GetExternalId(),
}
for attribute, value := range properties {
if value != "" {
if e.GetEqualFoldAttributeValue(attribute) != value {
mr.Replace(attribute, []string{value})
updateNeeded = true
}
}
}
if user.AccountEnabled != nil {
un, err := i.updateAccountEnabledState(logger, user.GetAccountEnabled(), e, &mr)
if err != nil {
return nil, err
}
if un {
updateNeeded = true
}
}
if user.PasswordProfile != nil && user.PasswordProfile.GetPassword() != "" {
if i.usePwModifyExOp {
if err := i.updateUserPassword(ctx, e.DN, user.PasswordProfile.GetPassword()); err != nil {
return nil, err
}
} else {
// password are hashed server side there is no need to check if the new password
// is actually different from the old one.
mr.Replace("userPassword", []string{user.PasswordProfile.GetPassword()})
updateNeeded = true
}
}
if identities, ok := user.GetIdentitiesOk(); ok {
attrValues := make([]string, 0, len(identities))
for _, identity := range identities {
identityStr, err := i.identityToLDAPAttrValue(identity)
if err != nil {
return nil, err
}
attrValues = append(attrValues, identityStr)
}
mr.Replace(i.userAttributeMap.identities, attrValues)
updateNeeded = true
}
if updateNeeded {
if err := i.conn.Modify(&mr); err != nil {
return nil, err
}
}
// Read back user from LDAP to get the generated UUID
e, err = i.getEducationUserByDN(e.DN)
if err != nil {
return nil, err
}
returnUser := i.createEducationUserModelFromLDAP(e)
// To avoid a ldap lookup for group membership, set the enabled flag to same as input value
// since this would have been updated with group membership from the input anyway.
if user.AccountEnabled != nil && i.disableUserMechanism == DisableMechanismGroup {
returnUser.AccountEnabled = user.AccountEnabled
}
return returnUser, nil
}
// GetEducationUser implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetEducationUser")
e, err := i.getEducationUserByNameOrID(nameOrID)
if err != nil {
return nil, err
}
u := i.createEducationUserModelFromLDAP(e)
if u == nil {
return nil, ErrNotFound
}
return u, nil
}
// GetEducationUsers implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error) {
var filter string
if i.userFilter == "" {
filter = fmt.Sprintf("(objectClass=%s)", i.educationConfig.userObjectClass)
} else {
filter = fmt.Sprintf("(&%s(objectClass=%s))", i.userFilter, i.educationConfig.userObjectClass)
}
return i.searchEducationUsers(ctx, filter)
}
func (i *LDAP) FilterEducationUsersByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationUser, error) {
logger := i.logger.SubloggerWithRequestID(ctx).With().Str("func", "FilterEducationUsersByAttribute").Logger()
logger.Debug().Str("backend", "ldap").Str("attribute", attr).Str("value", value).Msg("")
var ldapAttr string
switch attr {
case "displayname":
ldapAttr = i.userAttributeMap.displayName
case "mail":
ldapAttr = i.userAttributeMap.mail
case "userType":
ldapAttr = i.userAttributeMap.userType
case "primaryRole":
ldapAttr = i.educationConfig.userAttributeMap.primaryRole
case "externalId":
ldapAttr = i.educationConfig.userAttributeMap.externalID
default:
return nil, errorcode.New(errorcode.InvalidRequest, fmt.Sprintf("filtering by attribute '%s' is not supported", attr))
}
filter := fmt.Sprintf("(&%s(objectClass=%s)(%s=%s))", i.userFilter, i.educationConfig.userObjectClass, ldap.EscapeFilter(ldapAttr), ldap.EscapeFilter(value))
return i.searchEducationUsers(ctx, filter)
}
// searchEducationUsers builds and executes an LDAP search for education users and converts the results to EducationUser models.
func (i *LDAP) searchEducationUsers(ctx context.Context, filter string) ([]*libregraph.EducationUser, error) {
searchRequest := ldap.NewSearchRequest(
i.userBaseDN,
i.userScope,
ldap.NeverDerefAliases, 0, 0, false,
filter,
i.getEducationUserAttrTypes(),
nil,
)
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").
Str("base", searchRequest.BaseDN).
Str("filter", searchRequest.Filter).
Int("scope", searchRequest.Scope).
Int("sizelimit", searchRequest.SizeLimit).
Interface("attributes", searchRequest.Attributes).
Msg("searchEducationUsers")
res, err := i.conn.Search(searchRequest)
if err != nil {
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
}
users := make([]*libregraph.EducationUser, 0, len(res.Entries))
for _, e := range res.Entries {
u := i.createEducationUserModelFromLDAP(e)
// Skip invalid LDAP users
if u == nil {
continue
}
users = append(users, u)
}
return users, nil
}
func (i *LDAP) educationUserToUser(eduUser libregraph.EducationUser) *libregraph.User {
user := libregraph.NewUser(*eduUser.DisplayName, *eduUser.OnPremisesSamAccountName)
user.Surname = eduUser.Surname
user.AccountEnabled = eduUser.AccountEnabled
user.GivenName = eduUser.GivenName
user.Mail = eduUser.Mail
user.UserType = eduUser.UserType
user.Identities = eduUser.Identities
return user
}
func (i *LDAP) userToEducationUser(user libregraph.User, e *ldap.Entry) *libregraph.EducationUser {
eduUser := libregraph.NewEducationUser()
eduUser.Id = user.Id
eduUser.OnPremisesSamAccountName = &user.OnPremisesSamAccountName
eduUser.Surname = user.Surname
eduUser.AccountEnabled = user.AccountEnabled
eduUser.GivenName = user.GivenName
eduUser.DisplayName = &user.DisplayName
eduUser.Mail = user.Mail
eduUser.UserType = user.UserType
if e != nil {
// Set the education User specific Attributes from the supplied LDAP Entry
if primaryRole := e.GetEqualFoldAttributeValue(i.educationConfig.userAttributeMap.primaryRole); primaryRole != "" {
eduUser.SetPrimaryRole(primaryRole)
}
if externalID := e.GetEqualFoldAttributeValue(i.educationConfig.userAttributeMap.externalID); externalID != "" {
eduUser.SetExternalId(externalID)
}
}
return eduUser
}
func (i *LDAP) educationUserToLDAPAttrValues(user libregraph.EducationUser, attrs ldapAttributeValues) (ldapAttributeValues, error) {
if role, ok := user.GetPrimaryRoleOk(); ok {
attrs[i.educationConfig.userAttributeMap.primaryRole] = []string{*role}
}
if externalID, ok := user.GetExternalIdOk(); ok {
attrs[i.educationConfig.userAttributeMap.externalID] = []string{*externalID}
}
attrs["objectClass"] = append(attrs["objectClass"], i.educationConfig.userObjectClass)
return attrs, nil
}
func (i *LDAP) educationUserToAddRequest(user libregraph.EducationUser) (*ldap.AddRequest, error) {
plainUser := i.educationUserToUser(user)
ldapAttrs, err := i.userToLDAPAttrValues(*plainUser)
if err != nil {
return nil, err
}
ldapAttrs, err = i.educationUserToLDAPAttrValues(user, ldapAttrs)
if err != nil {
return nil, err
}
ar := ldap.NewAddRequest(i.getUserLDAPDN(*plainUser), nil)
for attrType, values := range ldapAttrs {
ar.Attribute(attrType, values)
}
return ar, nil
}
func (i *LDAP) createEducationUserModelFromLDAP(e *ldap.Entry) *libregraph.EducationUser {
user := i.createUserModelFromLDAP(e)
return i.userToEducationUser(*user, e)
}
func (i *LDAP) getEducationUserAttrTypes() []string {
return []string{
i.userAttributeMap.displayName,
i.userAttributeMap.id,
i.userAttributeMap.mail,
i.userAttributeMap.userName,
i.userAttributeMap.surname,
i.userAttributeMap.givenName,
i.userAttributeMap.accountEnabled,
i.userAttributeMap.userType,
i.userAttributeMap.identities,
i.educationConfig.userAttributeMap.primaryRole,
i.educationConfig.userAttributeMap.externalID,
i.educationConfig.memberOfSchoolAttribute,
}
}
func (i *LDAP) getEducationUserByDN(dn string) (*ldap.Entry, error) {
filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.userObjectClass)
if i.userFilter != "" {
filter = fmt.Sprintf("(&%s(%s))", filter, i.userFilter)
}
return i.getEntryByDN(dn, i.getEducationUserAttrTypes(), filter)
}
func (i *LDAP) getEducationUserByNameOrID(nameOrID string) (*ldap.Entry, error) {
return i.getEducationObjectByNameOrID(
nameOrID,
i.userAttributeMap.userName,
i.userAttributeMap.id,
i.userFilter,
i.educationConfig.userObjectClass,
i.userBaseDN,
i.getEducationUserAttrTypes(),
)
}
func (i *LDAP) getEducationObjectByNameOrID(nameOrID, nameAttribute, idAttribute, objectFilter, objectClass, baseDN string, attributes []string) (*ldap.Entry, error) {
nameOrID = ldap.EscapeFilter(nameOrID)
filter := fmt.Sprintf("(|(%s=%s)(%s=%s))", nameAttribute, nameOrID, idAttribute, nameOrID)
return i.getEducationObjectByFilter(filter, baseDN, objectFilter, objectClass, attributes)
}
func (i *LDAP) getEducationObjectByFilter(filter, baseDN, objectFilter, objectClass string, attributes []string) (*ldap.Entry, error) {
filter = fmt.Sprintf("(&%s(objectClass=%s)%s)", objectFilter, objectClass, filter)
return i.searchLDAPEntryByFilter(baseDN, attributes, filter)
}
@@ -0,0 +1,303 @@
package identity
import (
"context"
"testing"
"github.com/go-ldap/ldap/v3"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/identity/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
const peopleBaseDN = "ou=people,dc=test"
var eduUserAttrs = []string{
"displayname",
"entryUUID",
"mail",
"uid",
"sn",
"givenname",
"userEnabledAttribute",
"userTypeAttribute",
"qsferaExternalIdentity",
"userClass",
"qsferaEducationExternalId",
"qsferaMemberOfSchool",
}
var eduUserEntry = ldap.NewEntry("uid=user,ou=people,dc=test",
map[string][]string{
"uid": {"testuser"},
"displayname": {"Test User"},
"mail": {"user@example"},
"entryuuid": {"abcd-defg"},
"userClass": {"student"},
"qsferaExternalIdentity": {
"$ http://idp $ testuser",
"xxx $ http://idpnew $ xxxxx-xxxxx-xxxxx",
},
"userTypeAttribute": {"Member"},
"userEnabledAttribute": {"FALSE"},
})
var renamedEduUserEntry = ldap.NewEntry("uid=newtestuser,ou=people,dc=test",
map[string][]string{
"uid": {"newtestuser"},
"displayname": {"Test User"},
"mail": {"user@example"},
"entryuuid": {"abcd-defg"},
"userClass": {"student"},
"qsferaExternalIdentity": {
"$ http://idp $ testuser",
"xxx $ http://idpnew $ xxxxx-xxxxx-xxxxx",
},
"userTypeAttribute": {"Guest"},
"userEnabledAttribute": {"TRUE"},
})
var eduUserEntryWithSchool = ldap.NewEntry("uid=user,ou=people,dc=test",
map[string][]string{
"uid": {"testuser"},
"displayname": {"Test User"},
"mail": {"user@example"},
"entryuuid": {"abcd-defg"},
"userClass": {"student"},
"qsferaMemberOfSchool": {"abcd-defg"},
"qsferaExternalIdentity": {
"$ http://idp $ testuser",
"xxx $ http://idpnew $ xxxxx-xxxxx-xxxxx",
},
})
var sr1 *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: peopleBaseDN,
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=qsferaEducationUser)(|(uid=abcd-defg)(entryUUID=abcd-defg)))",
Attributes: eduUserAttrs,
Controls: []ldap.Control(nil),
}
var sr2 *ldap.SearchRequest = &ldap.SearchRequest{
BaseDN: peopleBaseDN,
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=qsferaEducationUser)(|(uid=xxxx-xxxx)(entryUUID=xxxx-xxxx)))",
Attributes: eduUserAttrs,
Controls: []ldap.Control(nil),
}
func TestCreateEducationUser(t *testing.T) {
lm := &mocks.Client{}
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
//assert.NotEqual(t, "", b.educationConfig.schoolObjectClass)
lm.On("Add", mock.Anything).Return(nil)
lm.On("Search", mock.Anything).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{
eduUserEntry,
},
},
nil)
user := libregraph.NewEducationUser()
user.SetDisplayName("Test User")
user.SetOnPremisesSamAccountName("testuser")
user.SetMail("testuser@example.org")
user.SetPrimaryRole("student")
user.SetUserType(("Member"))
user.SetAccountEnabled(false)
eduUser, err := b.CreateEducationUser(context.Background(), *user)
lm.AssertNumberOfCalls(t, "Add", 1)
lm.AssertNumberOfCalls(t, "Search", 1)
assert.NotNil(t, eduUser)
assert.Nil(t, err)
assert.Equal(t, eduUser.GetDisplayName(), user.GetDisplayName())
assert.Equal(t, eduUser.GetOnPremisesSamAccountName(), user.GetOnPremisesSamAccountName())
assert.Equal(t, "abcd-defg", eduUser.GetId())
assert.Equal(t, eduUser.GetPrimaryRole(), user.GetPrimaryRole())
assert.Equal(t, eduUser.GetUserType(), user.GetUserType())
assert.Equal(t, eduUser.GetAccountEnabled(), false)
}
func TestDeleteEducationUser(t *testing.T) {
lm := &mocks.Client{}
lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
dr1 := &ldap.DelRequest{
DN: "uid=user,ou=people,dc=test",
}
lm.On("Del", dr1).Return(nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
err = b.DeleteEducationUser(context.Background(), "abcd-defg")
lm.AssertNumberOfCalls(t, "Search", 1)
lm.AssertNumberOfCalls(t, "Del", 1)
assert.Nil(t, err)
err = b.DeleteEducationUser(context.Background(), "xxxx-xxxx")
lm.AssertNumberOfCalls(t, "Search", 2)
lm.AssertNumberOfCalls(t, "Del", 1)
assert.NotNil(t, err)
assert.Equal(t, "itemNotFound: not found", err.Error())
}
func TestGetEducationUser(t *testing.T) {
lm := &mocks.Client{}
lm.On("Search", sr1).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
user, err := b.GetEducationUser(context.Background(), "abcd-defg")
lm.AssertNumberOfCalls(t, "Search", 1)
assert.Nil(t, err)
assert.Equal(t, "Test User", user.GetDisplayName())
assert.Equal(t, "abcd-defg", user.GetId())
_, err = b.GetEducationUser(context.Background(), "xxxx-xxxx")
lm.AssertNumberOfCalls(t, "Search", 2)
assert.NotNil(t, err)
assert.Equal(t, "itemNotFound: not found", err.Error())
}
func TestGetEducationUsers(t *testing.T) {
lm := &mocks.Client{}
sr := &ldap.SearchRequest{
BaseDN: peopleBaseDN,
Scope: 2,
SizeLimit: 0,
Filter: "(objectClass=qsferaEducationUser)",
Attributes: eduUserAttrs,
Controls: []ldap.Control(nil),
}
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
_, err = b.GetEducationUsers(context.Background())
lm.AssertNumberOfCalls(t, "Search", 1)
assert.Nil(t, err)
}
func TestFilterEducationUsersByAttr(t *testing.T) {
lm := &mocks.Client{}
sr := &ldap.SearchRequest{
BaseDN: peopleBaseDN,
Scope: 2,
SizeLimit: 0,
Filter: "(&(objectClass=qsferaEducationUser)(qsferaEducationExternalId=id1234))",
Attributes: eduUserAttrs,
Controls: []ldap.Control(nil),
}
lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{eduUserEntry}}, nil)
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
_, err = b.FilterEducationUsersByAttribute(context.Background(), "externalId", "id1234")
lm.AssertNumberOfCalls(t, "Search", 1)
assert.Nil(t, err)
}
func TestUpdateEducationUser(t *testing.T) {
lm := &mocks.Client{}
b, err := getMockedBackend(lm, eduConfig, &logger)
assert.Nil(t, err)
userSearchReq := &ldap.SearchRequest{
BaseDN: peopleBaseDN,
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=qsferaEducationUser)(|(uid=testuser)(entryUUID=testuser)))",
Attributes: eduUserAttrs,
}
userLookupReq := &ldap.SearchRequest{
BaseDN: "uid=newtestuser,ou=people,dc=test",
Scope: 0,
SizeLimit: 1,
Filter: "(objectClass=inetOrgPerson)",
Attributes: ldapUserAttributes,
}
eduUserLookupReq := &ldap.SearchRequest{
BaseDN: "uid=newtestuser,ou=people,dc=test",
Scope: 0,
SizeLimit: 1,
Filter: "(objectClass=qsferaEducationUser)",
Attributes: eduUserAttrs,
}
groupSearchReq := &ldap.SearchRequest{
BaseDN: "ou=groups,dc=test",
Scope: 2,
Filter: "(&(objectClass=groupOfNames)(member=uid=user,ou=people,dc=test))",
Attributes: []string{
"cn",
"entryUUID",
},
}
lm.On("Search", userLookupReq).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{
renamedEduUserEntry,
},
},
nil)
lm.On("Search", eduUserLookupReq).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{
renamedEduUserEntry,
},
},
nil)
lm.On("Search", userSearchReq).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{
eduUserEntry,
},
},
nil)
lm.On("Search", groupSearchReq).
Return(
&ldap.SearchResult{
Entries: []*ldap.Entry{},
},
nil)
modReq := ldap.ModifyRequest{
DN: "uid=newtestuser,ou=people,dc=test",
Changes: []ldap.Change{
{
Operation: ldap.ReplaceAttribute,
Modification: ldap.PartialAttribute{
Type: "mail",
Vals: []string{"new@mail.org"},
},
},
{
Operation: ldap.ReplaceAttribute,
Modification: ldap.PartialAttribute{
Type: "userEnabledAttribute",
Vals: []string{"TRUE"},
},
},
},
}
modDNReq := ldap.ModifyDNRequest{
DN: "uid=user,ou=people,dc=test",
NewRDN: "uid=newtestuser",
DeleteOldRDN: true,
}
lm.On("ModifyDN", &modDNReq).Return(nil)
lm.On("Modify", &modReq).Return(nil)
user := libregraph.NewEducationUser()
user.SetOnPremisesSamAccountName("newtestuser")
user.SetMail("new@mail.org")
user.SetAccountEnabled(true)
eduUser, err := b.UpdateEducationUser(context.Background(), "testuser", *user)
assert.NotNil(t, eduUser)
assert.Nil(t, err)
assert.Equal(t, eduUser.GetOnPremisesSamAccountName(), "newtestuser")
assert.Equal(t, "abcd-defg", eduUser.GetId())
assert.Equal(t, "Guest", eduUser.GetUserType())
assert.Equal(t, eduUser.GetAccountEnabled(), true)
}
@@ -0,0 +1,607 @@
package identity
import (
"context"
"errors"
"fmt"
"net/url"
"slices"
"strings"
"github.com/CiscoM31/godata"
"github.com/go-ldap/ldap/v3"
"github.com/google/uuid"
"github.com/libregraph/idm/pkg/ldapdn"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/errorcode"
"github.com/qsfera/server/services/graph/pkg/odata"
)
type groupAttributeMap struct {
name string
id string
member string
}
// GetGroup implements the Backend Interface for the LDAP Backend
func (i *LDAP) GetGroup(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.Group, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetGroup")
e, err := i.getLDAPGroupByNameOrID(nameOrID, true)
if err != nil {
return nil, err
}
sel := strings.Split(queryParam.Get("$select"), ",")
exp := strings.Split(queryParam.Get("$expand"), ",")
var g *libregraph.Group
if g = i.createGroupModelFromLDAP(e); g == nil {
return nil, errorcode.New(errorcode.ItemNotFound, "not found")
}
if slices.Contains(sel, "members") || slices.Contains(exp, "members") {
members, err := i.expandLDAPAttributeEntries(ctx, e, i.groupAttributeMap.member, "")
if err != nil {
return nil, err
}
g.Members = make([]libregraph.User, 0, len(members))
if len(members) > 0 {
for _, ue := range members {
if u := i.createUserModelFromLDAP(ue); u != nil {
g.Members = append(g.Members, *u)
}
}
}
}
return g, nil
}
// GetGroups implements the Backend Interface for the LDAP Backend
func (i *LDAP) GetGroups(ctx context.Context, oreq *godata.GoDataRequest) ([]*libregraph.Group, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetGroups")
search, err := odata.GetSearchValues(oreq.Query)
if err != nil {
return nil, err
}
var expandMembers bool
exp, err := odata.GetExpandValues(oreq.Query)
if err != nil {
return nil, err
}
sel, err := odata.GetSelectValues(oreq.Query)
if err != nil {
return nil, err
}
if slices.Contains(exp, "members") || slices.Contains(sel, "members") {
expandMembers = true
}
var groupFilter string
if search != "" {
search = ldap.EscapeFilter(search)
groupFilter = fmt.Sprintf(
"(%s=*%s*)",
i.groupAttributeMap.name, search,
)
}
groupFilter = fmt.Sprintf("(&%s(objectClass=%s)%s)", i.groupFilter, i.groupObjectClass, groupFilter)
groupAttrs := []string{
i.groupAttributeMap.name,
i.groupAttributeMap.id,
}
if expandMembers {
groupAttrs = append(groupAttrs, i.groupAttributeMap.member)
}
searchRequest := ldap.NewSearchRequest(
i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, 0, 0, false,
groupFilter,
groupAttrs,
nil,
)
logger.Debug().Str("backend", "ldap").
Str("base", searchRequest.BaseDN).
Str("filter", searchRequest.Filter).
Int("scope", searchRequest.Scope).
Int("sizelimit", searchRequest.SizeLimit).
Interface("attributes", searchRequest.Attributes).
Msg("GetGroups")
res, err := i.conn.Search(searchRequest)
if err != nil {
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
}
groups := make([]*libregraph.Group, 0, len(res.Entries))
var g *libregraph.Group
for _, e := range res.Entries {
if g = i.createGroupModelFromLDAP(e); g == nil {
continue
}
if expandMembers {
members, err := i.expandLDAPAttributeEntries(ctx, e, i.groupAttributeMap.member, "")
if err != nil {
return nil, err
}
g.Members = make([]libregraph.User, 0, len(members))
if len(members) > 0 {
for _, ue := range members {
if u := i.createUserModelFromLDAP(ue); u != nil {
g.Members = append(g.Members, *u)
}
}
}
}
groups = append(groups, g)
}
return groups, nil
}
// GetGroupMembers implements the Backend Interface for the LDAP Backend
func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string, req *godata.GoDataRequest) ([]*libregraph.User, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("GetGroupMembers")
exp, err := odata.GetExpandValues(req.Query)
if err != nil {
return nil, err
}
e, err := i.getLDAPGroupByNameOrID(groupID, true)
if err != nil {
return nil, err
}
searchTerm, err := odata.GetSearchValues(req.Query)
if err != nil {
return nil, err
}
memberEntries, err := i.expandLDAPAttributeEntries(ctx, e, i.groupAttributeMap.member, searchTerm)
result := make([]*libregraph.User, 0, len(memberEntries))
if err != nil {
return nil, err
}
for _, member := range memberEntries {
if u := i.createUserModelFromLDAP(member); u != nil {
if slices.Contains(exp, "memberOf") {
userGroups, err := i.getGroupsForUser(member.DN)
if err != nil {
return nil, err
}
u.MemberOf = i.groupsFromLDAPEntries(userGroups)
}
result = append(result, u)
}
}
return result, nil
}
// CreateGroup implements the Backend Interface for the LDAP Backend
// It is currently restricted to managing groups based on the "groupOfNames" ObjectClass.
// As "groupOfNames" requires a "member" Attribute to be present. Empty Groups (groups
// without a member) a represented by adding an empty DN as the single member.
func (i *LDAP) CreateGroup(ctx context.Context, group libregraph.Group) (*libregraph.Group, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("create group")
if !i.writeEnabled && i.groupCreateBaseDN == i.groupBaseDN {
return nil, errorcode.New(errorcode.NotAllowed, "server is configured read-only")
}
ar, err := i.groupToAddRequest(group)
if err != nil {
return nil, err
}
if err := i.conn.Add(ar); err != nil {
var lerr *ldap.Error
logger.Debug().Str("backend", "ldap").Str("dn", group.GetDisplayName()).Err(err).Msg("Failed to create group")
if errors.As(err, &lerr) {
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
err = errorcode.New(errorcode.NameAlreadyExists, "group already exists")
}
}
return nil, err
}
// Read back group from LDAP to get the generated UUID
e, err := i.getGroupByDN(ar.DN)
if err != nil {
return nil, err
}
return i.createGroupModelFromLDAP(e), nil
}
// DeleteGroup implements the Backend Interface.
func (i *LDAP) DeleteGroup(ctx context.Context, id string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("DeleteGroup")
if !i.writeEnabled && i.groupCreateBaseDN == i.groupBaseDN {
return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
}
e, err := i.getLDAPGroupByID(id, false)
if err != nil {
return err
}
if i.isLDAPGroupReadOnly(e) {
return errorcode.New(errorcode.NotAllowed, "group is read-only")
}
dr := ldap.DelRequest{DN: e.DN}
if err = i.conn.Del(&dr); err != nil {
return err
}
return nil
}
// UpdateGroupName implements the Backend Interface.
func (i *LDAP) UpdateGroupName(ctx context.Context, groupID string, groupName string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("AddMembersToGroup")
if !i.writeEnabled && i.groupCreateBaseDN == i.groupBaseDN {
return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
}
ge, err := i.getLDAPGroupByID(groupID, true)
if err != nil {
return err
}
if i.isLDAPGroupReadOnly(ge) {
return errorcode.New(errorcode.NotAllowed, "group is read-only")
}
if ge.GetEqualFoldAttributeValue(i.groupAttributeMap.name) == groupName {
return nil
}
attributeTypeAndValue := ldap.AttributeTypeAndValue{
Type: i.groupAttributeMap.name,
Value: groupName,
}
newDNString := attributeTypeAndValue.String()
logger.Debug().Str("originalDN", ge.DN).Str("newDN", newDNString).Msg("Modifying DN")
mrdn := ldap.NewModifyDNRequest(ge.DN, newDNString, true, "")
if err := i.conn.ModifyDN(mrdn); err != nil {
var lerr *ldap.Error
logger.Debug().Str("originalDN", ge.DN).Str("newDN", newDNString).Err(err).Msg("Failed to modify DN")
if errors.As(err, &lerr) {
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
err = errorcode.New(errorcode.NameAlreadyExists, "Group name already in use")
}
}
return err
}
return nil
}
// AddMembersToGroup implements the Backend Interface for the LDAP backend.
// Currently, it is limited to adding Users as Group members. Adding other groups
// as members is not yet implemented
func (i *LDAP) AddMembersToGroup(ctx context.Context, groupID string, memberIDs []string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("AddMembersToGroup")
if !i.writeEnabled && i.groupCreateBaseDN == i.groupBaseDN {
return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
}
ge, err := i.getLDAPGroupByNameOrID(groupID, true)
if err != nil {
return err
}
if i.isLDAPGroupReadOnly(ge) {
return errorcode.New(errorcode.NotAllowed, "group is read-only")
}
mr := ldap.ModifyRequest{DN: ge.DN}
// Handle empty groups (using the empty member attribute)
current := ge.GetEqualFoldAttributeValues(i.groupAttributeMap.member)
if len(current) == 1 && current[0] == "" {
mr.Delete(i.groupAttributeMap.member, []string{""})
}
// Create a Set of current members for faster lookups
currentSet := make(map[string]struct{}, len(current))
for _, currentMember := range current {
// We can ignore any empty member value here
if currentMember == "" {
continue
}
nCurrentMember, err := ldapdn.ParseNormalize(currentMember)
if err != nil {
// We couldn't parse the member value as a DN. Let's skip it, but log a warning
logger.Warn().Str("memberDN", currentMember).Err(err).Msg("Couldn't parse DN")
continue
}
currentSet[nCurrentMember] = struct{}{}
}
var newMemberDN []string
for _, memberID := range memberIDs {
me, err := i.getLDAPUserByID(memberID)
if err != nil {
return err
}
nDN, err := ldapdn.ParseNormalize(me.DN)
if err != nil {
logger.Error().Str("new member", me.DN).Err(err).Msg("Couldn't parse DN")
return err
}
if _, present := currentSet[nDN]; !present {
newMemberDN = append(newMemberDN, me.DN)
} else {
logger.Debug().Str("memberDN", me.DN).Msg("Member already present in group. Skipping")
}
}
if len(newMemberDN) > 0 {
// Small retry loop. It might be that, when reading the group we found the empty group member ("",
// line 289 above). Our modify operation tries to delete that value. However, another go-routine
// might have done that in parallel. In that case
// (LDAPResultNoSuchAttribute) we need to retry the modification
// without to delete.
for j := 0; j < 2; j++ {
mr.Add(i.groupAttributeMap.member, newMemberDN)
if err := i.conn.Modify(&mr); err != nil {
if lerr, ok := err.(*ldap.Error); ok {
switch lerr.ResultCode {
case ldap.LDAPResultAttributeOrValueExists:
err = fmt.Errorf("duplicate member entries in request")
case ldap.LDAPResultNoSuchAttribute:
if len(mr.Changes) == 2 {
// We tried the special case for adding the first group member, but some
// other request running in parallel did that already. Retry with a "normal"
// modification
logger.Debug().Err(err).
Msg("Failed to add first group member. Retrying once, without deleting the empty member value.")
mr.Changes = make([]ldap.Change, 0, 1)
continue
}
default:
logger.Info().Err(err).Msg("Failed to modify group member entries on PATCH group")
err = fmt.Errorf("unknown error when trying to modify group member entries")
}
}
return err
}
// succeeded
break
}
}
return nil
}
// RemoveMemberFromGroup implements the Backend Interface.
func (i *LDAP) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("RemoveMemberFromGroup")
if !i.writeEnabled && i.groupCreateBaseDN == i.groupBaseDN {
return errorcode.New(errorcode.NotAllowed, "server is configured read-only")
}
ge, err := i.getLDAPGroupByID(groupID, true)
if err != nil {
logger.Debug().Str("backend", "ldap").Str("groupID", groupID).Msg("Error looking up group")
return err
}
if i.isLDAPGroupReadOnly(ge) {
return errorcode.New(errorcode.NotAllowed, "group is read-only")
}
me, err := i.getLDAPUserByID(memberID)
if err != nil {
logger.Debug().Str("backend", "ldap").Str("memberID", memberID).Msg("Error looking up group member")
return err
}
logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN).Msg("remove member")
if err = i.removeEntryByDNAndAttributeFromEntry(ge, me.DN, i.groupAttributeMap.member); err != nil {
logger.Error().Err(err).Str("backend", "ldap").Str("group", groupID).Str("member", memberID).Msg("Failed to remove member from group.")
}
return err
}
func (i *LDAP) groupToAddRequest(group libregraph.Group) (*ldap.AddRequest, error) {
ar := ldap.NewAddRequest(i.getGroupCreateLDAPDN(group), nil)
attrMap, err := i.groupToLDAPAttrValues(group)
if err != nil {
return nil, err
}
for attrType, values := range attrMap {
ar.Attribute(attrType, values)
}
return ar, nil
}
func (i *LDAP) getGroupCreateLDAPDN(group libregraph.Group) string {
attributeTypeAndValue := ldap.AttributeTypeAndValue{
Type: "cn",
Value: group.GetDisplayName(),
}
return fmt.Sprintf("%s,%s", attributeTypeAndValue.String(), i.groupCreateBaseDN)
}
func (i *LDAP) groupToLDAPAttrValues(group libregraph.Group) (map[string][]string, error) {
attrs := map[string][]string{
i.groupAttributeMap.name: {group.GetDisplayName()},
"objectClass": {"groupOfNames", "top"},
// This is a crutch to allow groups without members for LDAP servers
// that apply strict Schema checking. The RFCs define "member/uniqueMember"
// as required attribute for groupOfNames/groupOfUniqueNames. So we
// add an empty string (which is a valid DN) as the initial member.
// It will be replaced once real members are added.
// We might want to use the newer, but not so broadly used "groupOfMembers"
// objectclass (RFC2307bis-02) where "member" is optional.
i.groupAttributeMap.member: {""},
}
if !i.useServerUUID {
attrs["qsferaUUID"] = []string{uuid.NewString()}
attrs["objectClass"] = append(attrs["objectClass"], "qsferaObject")
}
return attrs, nil
}
func (i *LDAP) getLDAPGroupByID(id string, requestMembers bool) (*ldap.Entry, error) {
idString, err := filterEscapeAttribute(i.groupAttributeMap.id, i.groupIDisOctetString, id)
if err != nil {
return nil, fmt.Errorf("invalid group id: %w", err)
}
filter := fmt.Sprintf("(%s=%s)", i.groupAttributeMap.id, idString)
return i.getLDAPGroupByFilter(filter, requestMembers)
}
func (i *LDAP) getLDAPGroupByNameOrID(nameOrID string, requestMembers bool) (*ldap.Entry, error) {
idString, err := filterEscapeAttribute(i.groupAttributeMap.id, i.groupIDisOctetString, nameOrID)
// err != nil just means that this is not an uuid, so we can skip the uuid filter part
// and just filter by name
filter := ""
if err == nil {
filter = fmt.Sprintf("(|(%s=%s)(%s=%s))", i.groupAttributeMap.name, ldap.EscapeFilter(nameOrID), i.groupAttributeMap.id, idString)
} else {
filter = fmt.Sprintf("(%s=%s)", i.userAttributeMap.userName, ldap.EscapeFilter(nameOrID))
}
return i.getLDAPGroupByFilter(filter, requestMembers)
}
func (i *LDAP) getLDAPGroupByFilter(filter string, requestMembers bool) (*ldap.Entry, error) {
e, err := i.getLDAPGroupsByFilter(filter, requestMembers, true)
if err != nil {
return nil, err
}
if len(e) == 0 {
return nil, errorcode.New(errorcode.ItemNotFound, "not found")
}
return e[0], nil
}
// Search for LDAP Groups matching the specified filter, if requestMembers is true the groupMemberShip
// attribute will be part of the result attributes. The LDAP filter is combined with the configured groupFilter
// resulting in a filter like "(&(LDAP.groupFilter)(objectClass=LDAP.groupObjectClass)(<filter_from_args>))"
func (i *LDAP) getLDAPGroupsByFilter(filter string, requestMembers, single bool) ([]*ldap.Entry, error) {
attrs := []string{
i.groupAttributeMap.name,
i.groupAttributeMap.id,
}
if requestMembers {
attrs = append(attrs, i.groupAttributeMap.member)
}
sizelimit := 0
if single {
sizelimit = 1
}
searchRequest := ldap.NewSearchRequest(
i.groupBaseDN, i.groupScope, ldap.NeverDerefAliases, sizelimit, 0, false,
fmt.Sprintf("(&%s(objectClass=%s)%s)", i.groupFilter, i.groupObjectClass, filter),
attrs,
nil,
)
i.logger.Debug().Str("backend", "ldap").
Str("base", searchRequest.BaseDN).
Str("filter", searchRequest.Filter).
Int("scope", searchRequest.Scope).
Int("sizelimit", searchRequest.SizeLimit).
Interface("attributes", searchRequest.Attributes).
Msg("getLDAPGroupsByFilter")
res, err := i.conn.Search(searchRequest)
if err != nil {
var errmsg string
if lerr, ok := err.(*ldap.Error); ok {
if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
errmsg = fmt.Sprintf("too many results searching for group '%s'", filter)
i.logger.Debug().Str("backend", "ldap").Err(lerr).Msg(errmsg)
}
}
return nil, errorcode.New(errorcode.ItemNotFound, errmsg)
}
return res.Entries, nil
}
func (i *LDAP) getGroupByDN(dn string) (*ldap.Entry, error) {
attrs := []string{
i.groupAttributeMap.id,
i.groupAttributeMap.name,
}
filter := fmt.Sprintf("(objectClass=%s)", i.groupObjectClass)
if i.groupFilter != "" {
filter = fmt.Sprintf("(&%s(%s))", filter, i.groupFilter)
}
return i.getEntryByDN(dn, attrs, filter)
}
func (i *LDAP) getGroupsForUser(dn string) ([]*ldap.Entry, error) {
groupFilter := fmt.Sprintf(
"(%s=%s)",
i.groupAttributeMap.member, ldap.EscapeFilter(dn),
)
userGroups, err := i.getLDAPGroupsByFilter(groupFilter, false, false)
if err != nil {
return nil, err
}
return userGroups, nil
}
func (i *LDAP) createGroupModelFromLDAP(e *ldap.Entry) *libregraph.Group {
name := e.GetEqualFoldAttributeValue(i.groupAttributeMap.name)
id, err := i.ldapUUIDtoString(e, i.groupAttributeMap.id, i.groupIDisOctetString)
if err != nil {
i.logger.Warn().Str("dn", e.DN).Str(i.groupAttributeMap.id, e.GetEqualFoldAttributeValue(i.groupAttributeMap.id)).Msg("Invalid User. Cannot convert UUID")
}
groupTypes := []string{}
if i.isLDAPGroupReadOnly(e) {
groupTypes = []string{"ReadOnly"}
}
if id != "" && name != "" {
return &libregraph.Group{
DisplayName: &name,
Id: &id,
GroupTypes: groupTypes,
}
}
i.logger.Warn().Str("dn", e.DN).Msg("Group is missing name or id")
return nil
}
func (i *LDAP) isLDAPGroupReadOnly(e *ldap.Entry) bool {
groupDN, err := ldap.ParseDN(e.DN)
if err != nil {
i.logger.Warn().Err(err).Str("dn", e.DN).Msg("Failed to parse DN")
return false
}
baseDN, err := ldap.ParseDN(i.groupCreateBaseDN)
if err != nil {
i.logger.Warn().Err(err).Str("dn", i.groupCreateBaseDN).Msg("Failed to parse DN")
return false
}
return !baseDN.AncestorOfFold(groupDN)
}
func (i *LDAP) groupsFromLDAPEntries(e []*ldap.Entry) []libregraph.Group {
groups := make([]libregraph.Group, 0, len(e))
for _, g := range e {
if grp := i.createGroupModelFromLDAP(g); grp != nil {
groups = append(groups, *grp)
}
}
return groups
}
@@ -0,0 +1,453 @@
package identity
import (
"context"
"errors"
"net/url"
"testing"
"github.com/CiscoM31/godata"
"github.com/go-ldap/ldap/v3"
"github.com/qsfera/server/services/graph/pkg/identity/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var groupEntry = ldap.NewEntry("cn=group",
map[string][]string{
"cn": {"group"},
"entryuuid": {"abcd-defg"},
"member": {
"uid=user,ou=people,dc=test",
"uid=invalid,ou=people,dc=test",
},
})
var invalidGroupEntry = ldap.NewEntry("cn=invalid",
map[string][]string{
"cn": {"invalid"},
})
var queryParamExpand = url.Values{
"$expand": []string{"members"},
}
var queryParamSelect = url.Values{
"$select": []string{"members"},
}
var groupLookupSearchRequest = &ldap.SearchRequest{
BaseDN: "ou=groups,dc=test",
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=groupOfNames)(|(cn=group)(entryUUID=group)))",
Attributes: []string{"cn", "entryUUID", "member"},
Controls: []ldap.Control(nil),
}
var groupListSearchRequest = &ldap.SearchRequest{
BaseDN: "ou=groups,dc=test",
Scope: 2,
Filter: "(&(objectClass=groupOfNames))",
Attributes: []string{"cn", "entryUUID", "member"},
Controls: []ldap.Control(nil),
}
func TestGetGroup(t *testing.T) {
// Mock a Sizelimit Error
lm := &mocks.Client{}
lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock")))
b, _ := getMockedBackend(lm, lconfig, &logger)
_, err := b.GetGroup(context.Background(), "group", nil)
assert.ErrorContains(t, err, "itemNotFound:")
_, err = b.GetGroup(context.Background(), "group", queryParamExpand)
assert.ErrorContains(t, err, "itemNotFound:")
_, err = b.GetGroup(context.Background(), "group", queryParamSelect)
assert.ErrorContains(t, err, "itemNotFound:")
// Mock an empty Search Result
lm = &mocks.Client{}
lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
b, _ = getMockedBackend(lm, lconfig, &logger)
_, err = b.GetGroup(context.Background(), "group", nil)
assert.ErrorContains(t, err, "itemNotFound:")
_, err = b.GetGroup(context.Background(), "group", queryParamExpand)
assert.ErrorContains(t, err, "itemNotFound:")
_, err = b.GetGroup(context.Background(), "group", queryParamSelect)
assert.ErrorContains(t, err, "itemNotFound:")
// Mock an invalid Search Result
lm = &mocks.Client{}
lm.On("Search", mock.Anything).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{invalidGroupEntry},
}, nil)
b, _ = getMockedBackend(lm, lconfig, &logger)
_, err = b.GetGroup(context.Background(), "group", nil)
assert.ErrorContains(t, err, "itemNotFound:")
_, err = b.GetGroup(context.Background(), "group", queryParamExpand)
assert.ErrorContains(t, err, "itemNotFound:")
_, err = b.GetGroup(context.Background(), "group", queryParamSelect)
assert.ErrorContains(t, err, "itemNotFound:")
// Mock a valid Search Result
lm = &mocks.Client{}
sr2 := &ldap.SearchRequest{
BaseDN: "uid=user,ou=people,dc=test",
SizeLimit: 1,
Filter: "(objectClass=inetOrgPerson)",
Attributes: ldapUserAttributes,
Controls: []ldap.Control(nil),
}
sr3 := &ldap.SearchRequest{
BaseDN: "uid=invalid,ou=people,dc=test",
SizeLimit: 1,
Filter: "(objectClass=inetOrgPerson)",
Attributes: ldapUserAttributes,
Controls: []ldap.Control(nil),
}
lm.On("Search", groupLookupSearchRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil)
lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{invalidUserEntry}}, nil)
b, _ = getMockedBackend(lm, lconfig, &logger)
g, err := b.GetGroup(context.Background(), "group", nil)
if err != nil {
t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
} else if *g.Id != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
t.Errorf("Expected GetGroup to return a valid group")
}
g, err = b.GetGroup(context.Background(), "group", queryParamExpand)
switch {
case err != nil:
t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
t.Errorf("Expected GetGroup to return a valid group")
case len(g.Members) != 1:
t.Errorf("Expected GetGroup with expand to return one member")
case g.Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id):
t.Errorf("Expected GetGroup with expand to return correct member")
}
g, err = b.GetGroup(context.Background(), "group", queryParamSelect)
switch {
case err != nil:
t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
t.Errorf("Expected GetGroup to return a valid group")
case len(g.Members) != 1:
t.Errorf("Expected GetGroup with expand to return one member")
case g.Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id):
t.Errorf("Expected GetGroup with expand to return correct member")
}
}
func TestGetGroupReadOnlyBackend(t *testing.T) {
readOnlyConfig := lconfig
readOnlyConfig.WriteEnabled = false
readOnlyConfig.GroupBaseDN = "ou=groups,dc=test"
readOnlyConfig.GroupCreateBaseDN = "ou=local,ou=group,dc=test"
localGroupEntry := groupEntry
localGroupEntry.DN = "cn=local,ou=local,o=base"
lm := &mocks.Client{}
lm.On("Search", groupLookupSearchRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
b, _ := getMockedBackend(lm, readOnlyConfig, &logger)
g, err := b.GetGroup(context.Background(), "group", url.Values{})
switch {
case err != nil:
t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
t.Errorf("Expected GetGroup to return a valid group")
}
types := g.GetGroupTypes()
switch {
case len(types) == 0:
t.Errorf("No groupTypes attribute on readonly Group")
case len(types) > 1:
t.Errorf("Expected a single groupTypes value on readonly Group")
case types[0] != "ReadOnly":
t.Errorf("Expected a groupTypes 'ReadOnly' on readonly Group")
}
}
func TestGetGroupReadOnlySubtree(t *testing.T) {
readOnlyTreeConfig := lconfig
readOnlyTreeConfig.GroupCreateBaseDN = "ou=write,ou=groups,dc=test"
var writeGroupEntry = ldap.NewEntry("cn=group,ou=write,ou=groups,dc=test",
map[string][]string{
"cn": {"group"},
"entryuuid": {"abcd-defg"},
"member": {
"uid=user,ou=people,dc=test",
"uid=invalid,ou=people,dc=test",
},
})
lm := &mocks.Client{}
lm.On("Search", groupLookupSearchRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
b, _ := getMockedBackend(lm, readOnlyTreeConfig, &logger)
g, err := b.GetGroup(context.Background(), "group", url.Values{})
switch {
case err != nil:
t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
t.Errorf("Expected GetGroup to return a valid group")
}
types := g.GetGroupTypes()
switch {
case len(types) == 0:
t.Errorf("No groupTypes attribute on readonly Group")
case len(types) > 1:
t.Errorf("Expected a single groupTypes value on readonly Group")
case types[0] != "ReadOnly":
t.Errorf("Expected a groupTypes 'ReadOnly' on readonly Group")
}
lm = &mocks.Client{}
lm.On("Search", groupLookupSearchRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{writeGroupEntry}}, nil)
b, _ = getMockedBackend(lm, readOnlyTreeConfig, &logger)
g, err = b.GetGroup(context.Background(), "group", url.Values{})
switch {
case err != nil:
t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
case g.GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
t.Errorf("Expected GetGroup to return a valid group")
}
types = g.GetGroupTypes()
if len(types) != 0 {
t.Errorf("No groupTypes attribute expected on writeable Group")
}
}
func TestGetGroups(t *testing.T) {
lm := &mocks.Client{}
oDataReq, err := godata.ParseRequest(context.Background(), "", url.Values{})
if err != nil {
t.Errorf("Expected success, got '%s'", err.Error())
}
lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
b, _ := getMockedBackend(lm, lconfig, &logger)
_, err = b.GetGroups(context.Background(), oDataReq)
assert.ErrorContains(t, err, "itemNotFound:")
lm = &mocks.Client{}
lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
b, _ = getMockedBackend(lm, lconfig, &logger)
g, err := b.GetGroups(context.Background(), oDataReq)
if err != nil {
t.Errorf("Expected success, got '%s'", err.Error())
} else if g == nil || len(g) != 0 {
t.Errorf("Expected zero length user slice")
}
lm = &mocks.Client{}
lm.On("Search", mock.Anything).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{groupEntry},
}, nil)
b, _ = getMockedBackend(lm, lconfig, &logger)
g, err = b.GetGroups(context.Background(), oDataReq)
if err != nil {
t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
} else if *g[0].Id != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
t.Errorf("Expected GetGroup to return a valid group")
}
// Mock a valid Search Result with expanded group members
lm = &mocks.Client{}
sr2 := &ldap.SearchRequest{
BaseDN: "uid=user,ou=people,dc=test",
SizeLimit: 1,
Filter: "(objectClass=inetOrgPerson)",
Attributes: ldapUserAttributes,
Controls: []ldap.Control(nil),
}
sr3 := &ldap.SearchRequest{
BaseDN: "uid=invalid,ou=people,dc=test",
SizeLimit: 1,
Filter: "(objectClass=inetOrgPerson)",
Attributes: ldapUserAttributes,
Controls: []ldap.Control(nil),
}
for _, param := range []url.Values{queryParamSelect, queryParamExpand} {
oDataReq, err := godata.ParseRequest(context.Background(), "", param)
if err != nil {
t.Errorf("Expected success, got '%s'", err.Error())
}
lm.On("Search", groupListSearchRequest).Return(&ldap.SearchResult{Entries: []*ldap.Entry{groupEntry}}, nil)
lm.On("Search", sr2).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil)
lm.On("Search", sr3).Return(&ldap.SearchResult{Entries: []*ldap.Entry{invalidUserEntry}}, nil)
b, _ = getMockedBackend(lm, lconfig, &logger)
g, err = b.GetGroups(context.Background(), oDataReq)
switch {
case err != nil:
t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
case g[0].GetId() != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id):
t.Errorf("Expected GetGroup to return a valid group")
case len(g[0].Members) != 1:
t.Errorf("Expected GetGroup to return group with one member")
case g[0].Members[0].GetId() != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id):
t.Errorf("Expected GetGroup to return group with correct member")
}
}
}
func TestGetGroupsSearch(t *testing.T) {
lm := &mocks.Client{}
odataReqDefault, err := godata.ParseRequest(context.Background(), "",
url.Values{
"$search": []string{"\"term\""},
},
)
if err != nil {
t.Errorf("Expected success got '%s'", err.Error())
}
// only match if the filter contains the search term unquoted
lm.On("Search", mock.MatchedBy(
func(req *ldap.SearchRequest) bool {
return req.Filter == "(&(objectClass=groupOfNames)(cn=*term*))"
})).
Return(&ldap.SearchResult{}, nil)
b, _ := getMockedBackend(lm, lconfig, &logger)
g, err := b.GetGroups(context.Background(), odataReqDefault)
if err != nil {
t.Errorf("Expected success, got '%s'", err.Error())
} else if g == nil || len(g) != 0 {
t.Errorf("Expected zero length user slice")
}
}
func TestUpdateGroupName(t *testing.T) {
groupDn := "cn=TheGroup,ou=groups,dc=example,dc=org"
type args struct {
groupId string
groupName string
newName string
}
type mockInputs struct {
funcName string
args []any
returns []any
}
tests := []struct {
name string
args args
assertion assert.ErrorAssertionFunc
ldapMocks []mockInputs
}{
{
name: "Test with no name change",
args: args{
groupId: "some-uuid-string",
newName: "TheGroup",
},
assertion: func(t assert.TestingT, err error, args ...any) bool {
return assert.Nil(t, err, args...)
},
ldapMocks: []mockInputs{
{
funcName: "Search",
args: []any{
ldap.NewSearchRequest(
"ou=groups,dc=test",
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases, 1, 0, false,
"(&(objectClass=groupOfNames)(entryUUID=some-uuid-string))",
[]string{"cn", "entryUUID", "member"},
nil,
),
},
returns: []any{
&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: groupDn,
Attributes: []*ldap.EntryAttribute{
{
Name: "cn",
Values: []string{"TheGroup"},
},
},
},
},
},
nil,
},
},
},
},
{
name: "Test with name change",
args: args{
groupId: "some-uuid-string",
newName: "TheGroupWithShinyNewName",
},
assertion: func(t assert.TestingT, err error, args ...any) bool {
return assert.Nil(t, err, args...)
},
ldapMocks: []mockInputs{
{
funcName: "Search",
args: []any{
ldap.NewSearchRequest(
"ou=groups,dc=test",
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases, 1, 0, false,
"(&(objectClass=groupOfNames)(entryUUID=some-uuid-string))",
[]string{"cn", "entryUUID", "member"},
nil,
),
},
returns: []any{
&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "cn=TheGroup,ou=groups,dc=example,dc=org",
Attributes: []*ldap.EntryAttribute{
{
Name: "cn",
Values: []string{"TheGroup"},
},
},
},
},
},
nil,
},
},
{
funcName: "ModifyDN",
args: []any{
&ldap.ModifyDNRequest{
DN: groupDn,
NewRDN: "cn=TheGroupWithShinyNewName",
DeleteOldRDN: true,
NewSuperior: "",
Controls: []ldap.Control(nil),
},
},
returns: []any{
nil,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lm := &mocks.Client{}
for _, ldapMock := range tt.ldapMocks {
lm.On(ldapMock.funcName, ldapMock.args...).Return(ldapMock.returns...)
}
ldapConfig := lconfig
i, _ := getMockedBackend(lm, ldapConfig, &logger)
err := i.UpdateGroupName(context.Background(), tt.args.groupId, tt.args.newName)
tt.assertion(t, err)
})
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com
[o:qsfera-eu:p:qsfera-eu:r:qsfera-graph]
file_filter = locale/<lang>/LC_MESSAGES/graph.po
minimum_perc = 75
resource_name = qsfera-graph
source_file = graph.pot
source_lang = en
type = PO
@@ -0,0 +1,135 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Ivan Fustero, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Ivan Fustero, 2025\n"
"Language-Team: Catalan (https://app.transifex.com/qsfera-eu/teams/204053/ca/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ca\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Pot editar"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Pot editar sense versions"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Pot gestionar"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Pot pujar"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Pot veure"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Pot veure (de forma segura)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "No pot accedir"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Denega tot accés."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Aquí podeu afegir una descripció per a aquest espai."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Visualitza i descarrega."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr "View only documents, images and PDFs. Watermarks will be applied."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Visualitza, descarrega i edita."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Visualitza, descarrega i mostra totes les persones convidades."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Visualitza, descarrega i puja."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "Visualitza, descarrega, edita i mostra totes les persones convidades."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Visualitza, descarrega, puja, edita, afegeix i suprimeix."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Visualitza, descarrega, puja, edita, afegeix, suprimeix i gestiona els "
"membres."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Visualitza, descarregar, pujar, editar, afegir, esborrar i mostrar totes les"
" persones convidades."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Visualitza, baixa, puja, edita, afegeix, suprimeix incloent l'historial."
@@ -0,0 +1,139 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jörn Friedrich Dreyer <jfd@butonic.de>, 2025
# Jannick Kuhr, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-02 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jannick Kuhr, 2026\n"
"Language-Team: German (https://app.transifex.com/qsfera-eu/teams/204053/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Kann bearbeiten"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Kann bearbeiten ohne Historie"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Kann verwalten"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Kann hochladen"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Kann anzeigen"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Kann anzeigen (sichere Ansicht)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Kann nicht zugreifen"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Jeder Zugriff verboten"
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Hier können Sie eine Beschreibung für diesen Space einfügen."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Ansehen und herunterladen."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
"Ansehen von Dokumenten, Bildern und PDFs. Wasserzeichen werden angewendet."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Ansehen, herunterladen und bearbeiten."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Ansehen, herunterladen und anzeigen aller eingeladenen Personen."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Ansehen, herunterladen und hochladen."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr ""
"Ansehen, herunterladen, bearbeiten und anzeigen aller eingeladenen Personen."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Ansehen, herunterladen, hochladen, bearbeiten, hinzufügen, löschen."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Ansehen, herunterladen, hochladen, bearbeiten, hinzufügen, löschen, teilen "
"und Mitglieder verwalten."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Ansehen, herunterladen, hochladen, bearbeiten, löschen und anzeigen aller "
"eingeladenen Benutzer."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Ansehen, herunterladen, hochladen, bearbeiten, hinzufügen, löschen - "
"inklusive des Verlaufs."
@@ -0,0 +1,138 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Efstathios Iosifidis <eiosifidis@gmail.com>, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-23 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Efstathios Iosifidis <eiosifidis@gmail.com>, 2026\n"
"Language-Team: Greek (https://app.transifex.com/qsfera-eu/teams/204053/el/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: el\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Δυνατότητα επεξεργασίας"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Δυνατότητα επεξεργασίας χωρίς εκδόσεις"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Δυνατότητα διαχείρισης"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Δυνατότητα μεταφόρτωσης"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Δυνατότητα προβολής"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Δυνατότητα προβολής (ασφαλής)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Δεν υπάρχει πρόσβαση"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Άρνηση κάθε πρόσβασης."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Εδώ μπορείτε να προσθέσετε μια περιγραφή για αυτόν τον Χώρο."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Προβολή και λήψη."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
"Προβολή μόνο εγγράφων, εικόνων και PDF. Θα εφαρμοστούν υδατογραφήματα."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Προβολή, λήψη και επεξεργασία."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Προβολή, λήψη και εμφάνιση όλων των προσκεκλημένων ατόμων."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Προβολή, λήψη και μεταφόρτωση."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr ""
"Προβολή, λήψη, επεξεργασία και εμφάνιση όλων των προσκεκλημένων ατόμων."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Προβολή, λήψη, μεταφόρτωση, επεξεργασία, προσθήκη και διαγραφή."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Προβολή, λήψη, μεταφόρτωση, επεξεργασία, προσθήκη, διαγραφή και διαχείριση "
"μελών."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Προβολή, λήψη, μεταφόρτωση, επεξεργασία, προσθήκη, διαγραφή και εμφάνιση "
"όλων των προσκεκλημένων ατόμων."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Προβολή, λήψη, μεταφόρτωση, επεξεργασία, προσθήκη, διαγραφή "
"συμπεριλαμβανομένου του ιστορικού."
@@ -0,0 +1,135 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Elías Martín, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Elías Martín, 2025\n"
"Language-Team: Spanish (https://app.transifex.com/qsfera-eu/teams/204053/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: es\n"
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Puede editar"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Se puede editar sin versiones"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Se puede gestionar"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Se puede subir"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Se puede visualizar"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Se puede visualizar (de forma segura)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "No se puede acceder"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Prohibir todo el acceso"
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Aquí puedes agregar una descripción a este Espacio."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Ver y descargar"
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
"Ver solamente documentos, imágenes y archivos PDF. Será aplicada una marca "
"de agua."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Ver. descargar y editar."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Ver, descargar y mostrar a todas las personas invitadas."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Ver, descargar y subir."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "Ver, descargar, editar y mostrar a todas las perosnas invitadas."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Ver, descargar, subuir, editar, añadir y eliminar."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr "Ver, descargar, subir, editar, añadir, eliminar y gestionar miembros."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Ver, descargar, subir, editar, añadir, eliminar y mostrar a todas las "
"personas invitadas."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Ver, descargar, subir, editar, añadir y borrar incluyendo el historial."
@@ -0,0 +1,132 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jiri Grönroos <jiri.gronroos@iki.fi>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-08 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/qsfera-eu/teams/204053/fi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Voi muokata"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Voi muokata ilman versioita"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Voi hallita"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Voi lähettää"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Voi nähdä"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Voi nähdä (turvallinen)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Ei pääsyä"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Estä kaikki pääsy."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Täällä voit lisätä kuvauksen avaruudelle."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Näytä ja lataa."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr "Näytä vain asiakirjat, kuvat ja PDF:t. Vesileimat lisätään."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Näytä, lataa ja muokkaa."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Näytä, lataa ja näytä kaikki kutsutut henkilöt."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Näytä, lataa ja lähetä."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "Näytä, lataa, muokkaa ja näytä kaikki kaikki kutsutut henkilöt."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Näytä, lataa lähetä, muokkaa, lisää ja poista."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr "Näytä, lataa, lähetä, muokkaa, lisää, poista ja hallitse jäseniä."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Näytä, lataa, lähetä, muokkaa, lisää, poista ja näytä kaikki kutsutut "
"henkilöt."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr "Näytä, lataa, lähetä, muokkaa, lisää, poista mukaan lukien historia."
@@ -0,0 +1,141 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# eric_G <junk.eg@free.fr>, 2025
# Benoît Aguesse, 2026
# Jérôme HERBINET, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jérôme HERBINET, 2026\n"
"Language-Team: French (https://app.transifex.com/qsfera-eu/teams/204053/fr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fr\n"
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Peut éditer"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Peut éditer sans versions"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Peut gérer"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Peut téléverser"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Peut visualiser"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Peut visualiser (sécurisé)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Accès impossible"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Refuser tout accès."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Vous pouvez ajouter une description pour cet espace."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Consulter et télécharger."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
"Visualiser uniquement les documents, les images et les fichiers PDF. Des "
"filigranes seront appliqués."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Consulter, télécharger et modifier."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Consulter, télécharger et montrer toutes les personnes invitées."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Consulter, télécharger et téléverser."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr ""
"Consulter, télécharger, modifier et montrer toutes les personnes invitées."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Consulter, télécharger, téléverser, modifier, ajouter et supprimer."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Consulter, télécharger, téléverser, modifier, ajouter, supprimer et gérer "
"les membres."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Consulter, télécharger, téléverser, modifier, ajouter, supprimer et montrer "
"toutes les personnes invitées."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Consulter, télécharger, téléverser, modifier, ajouter, supprimer, y compris "
"l'historique."
@@ -0,0 +1,135 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Simone Broglia, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Simone Broglia, 2025\n"
"Language-Team: Italian (https://app.transifex.com/qsfera-eu/teams/204053/it/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: it\n"
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Può modificare"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Può modificare senza versioni"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Può gestire"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Può caricare"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Può visualizzare"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Può visualizzare (protetto)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Accesso non consentito"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Blocca tutti gli accessi"
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Qui puoi aggiungere una descrizione per questo Spazio."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Visualizza e scarica."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
"Visualizza solo documenti, immagini e PDF. Verranno applicate filigrane."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Visualizza, scarica e modifica."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Visualizza, scarica e mostra tutte le persone invitate."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Visualizza, scarica e carica."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "Visualizza, scarica, modifica e mostra tutte le persone invitate."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Visualizza, scarica, carica, modifica, aggiungi ed elimina."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Visualizza, scarica, carica, modifica, aggiungi, elimina e gestisci i "
"membri."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Visualizza, scarica, carica, modifica, aggiungi, elimina e mostra tutte le "
"persone invitate."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr "View, download, upload, edit, add, delete including the history."
@@ -0,0 +1,130 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# iikaka88, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-19 00:04+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: iikaka88, 2025\n"
"Language-Team: Japanese (https://app.transifex.com/qsfera-eu/teams/204053/ja/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ja\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "編集可"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "編集 (履歴なし)"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "管理可能"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "アップロード可能"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "閲覧可能"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "セキュア閲覧(閲覧のみ)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "アクセス不可"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "すべてのアクセスを拒否"
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "説明を追加"
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "閲覧とダウンロード"
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr "ドキュメント、画像、PDFの閲覧のみ。透かし(ウォーターマーク)が適用"
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "閲覧、ダウンロード、編集"
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "閲覧、ダウンロード、および全招待ユーザーの表示"
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "閲覧、ダウンロード、アップロード"
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "閲覧、ダウンロード、編集、および全招待ユーザーの表示"
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "閲覧、ダウンロード、アップロード、編集、追加、削除"
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr "閲覧、ダウンロード、アップロード、編集、追加、削除、およびメンバー管理"
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr "閲覧、ダウンロード、アップロード、編集、追加、削除、および全招待ユーザーの表示"
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr "閲覧、ダウンロード、アップロード、編集、追加、削除(履歴を含む)"
@@ -0,0 +1,130 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# gapho shin, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: gapho shin, 2025\n"
"Language-Team: Korean (https://app.transifex.com/qsfera-eu/teams/204053/ko/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ko\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "편집 가능"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "버전 없이 편집 가능"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "관리 가능"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "업로드 가능"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "보기 가능"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "보기 가능 (보안)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "접근 불가"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "모든 접근 거부"
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "여기에 이 공간에 대한 설명을 추가할 수 있습니다."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "보기 및 다운로드."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr "문서, 이미지 및 PDF만 보기. 워터마크가 적용됩니다."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "보기, 다운로드 및 편집."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "모든 초대받은 사람들을 보고, 다운로드하고, 보여줍니다."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "보기, 다운로드 및 업로드."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "모든 초대받은 사람들을 보고, 다운로드하고, 편집하고, 보여줍니다."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "보기, 다운로드, 업로드, 편집, 추가 및 삭제."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr "회원 보기, 다운로드, 업로드, 편집, 추가, 삭제 및 관리."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr "모든 초대받은 사람들을 보고, 다운로드하고, 업로드하고, 편집하고, 추가하고, 삭제하고, 보여줍니다."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr "기록을 포함하여 보기, 다운로드, 업로드, 편집, 추가, 삭제합니다."
@@ -0,0 +1,138 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Stephan Paternotte <stephan@paternottes.net>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-06 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/qsfera-eu/teams/204053/nl/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: nl\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Kan bewerken"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Kan bewerken zonder versies"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Kan beheren"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Kan uploaden"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Kan weergeven"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Kan weergeven (beveiligd)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Heeft geen toegang"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Alle toegang weigeren."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Hier kun je een beschrijving voor deze ruimte toevoegen."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Weergeven en downloaden."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
"Alleen documenten, afbeeldingen en PDF's weergeven. Met toepassing van "
"watermerken."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Weergeven, downloaden en bewerken."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Weergeven, downloaden en alle genodigde personen zien."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Weergeven, downloaden en uploaden."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "Weergeven, downloaden, bewerken en alle genodigde personen zien."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Weergeven, downloaden, uploaden, bewerken, toevoegen en verwijderen."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Weergeven, downloaden, uploaden, bewerken, toevoegen, verwijderen en leden "
"beheren."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Weergeven, downloaden, uploaden, bewerken, toevoegen, verwijderen en alle "
"genodigde personen zien."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Weergeven, downloaden, uploaden, bewerken, toevoegen, verwijderen, incl. "
"geschiedenis."
@@ -0,0 +1,139 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# skrzat <skrzacikus@gmail.com>, 2025
# Maksymilian Styżej, 2025
# Xiaomi Box, 2025
# Radoslaw Posim, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-06 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Radoslaw Posim, 2025\n"
"Language-Team: Polish (https://app.transifex.com/qsfera-eu/teams/204053/pl/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pl\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Może edytować"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr ""
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Może zarządzać"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Może przesyłać pliki"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Może oglądać"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Mogę zobaczyć ( źródło) "
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Nie ma dostępu"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Odmów dostępu wszystkim."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Tutaj możesz dodać opis dla tej Przestrzeni"
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Zobacz i pobierz"
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Wyświetl , Pobierz i edytuj "
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr ""
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Wyświetlanie, pobieranie i przesyłanie."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr ""
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Wyświetlanie, pobieranie, przesyłanie, edycja, dodawanie i usuwanie."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Wyświetlanie, pobieranie, przesyłanie, edycja, dodawanie, usuwanie i "
"zarządzanie członkami."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Wyświetlanie, pobieranie, przesyłanie, edycja, dodawanie, usuwanie i "
"wyświetlanie wszystkich zaproszonych osób."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Wyświetlanie, pobieranie, przesyłanie, edycja, dodawanie, usuwanie włącznie "
"z historią."
@@ -0,0 +1,139 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Mário Machado, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-06 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/qsfera-eu/teams/204053/pt/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt\n"
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Pode editar"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Pode editar sem versões"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Pode gerir"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Pode carregar"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Pode visualizar"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Pode visualizar (seguro)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Não pode aceder"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Negar todo o acesso."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Aqui pode adicionar uma descrição para este Espaço."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Visualizar e descarregar."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
"Visualizar apenas documentos, imagens e PDFs. Serão aplicadas marcas de "
"água."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Visualizar, descarregar e editar."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Visualizar, descarregar e mostrar todas as pessoas convidadas."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Visualizar, descarregar e carregar."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr ""
"Visualizar, descarregar, editar e mostrar todas as pessoas convidadas."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Visualizar, descarregar, carregar, editar, adicionar e eliminar."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Visualizar, descarregar, carregar, editar, adicionar, eliminar e gerir "
"membros."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Visualizar, descarregar, carregar, editar, adicionar, eliminar e mostrar "
"todas as pessoas convidadas."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Visualizar, descarregar, carregar, editar, adicionar, eliminar, incluindo o "
"histórico."
@@ -0,0 +1,140 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Savely Krasovsky, 2025
# Lulufox, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-06 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Lulufox, 2025\n"
"Language-Team: Russian (https://app.transifex.com/qsfera-eu/teams/204053/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ru\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Может редактировать"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Может редактировать без версионирования"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Может управлять"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Может загружать"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Можно смотреть"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Можно смотреть (защищено)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Не имеет доступа"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Отказать во всех доступах"
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Здесь вы можете добавить описание для Пространства"
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Просмотреть и скачать"
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
"Просмотреть только документы, изображения и файлы PDF. Будут присутствовать "
"вотермарки."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Просмотреть, скачать и редактировать."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Просмотреть, скачать и показать всех приглашенных людей."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Просмотреть, скачать и загрузить."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr ""
"Просмотреть, скачать, отредактировать и показать всех приглашенных людей."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Просмотреть, скачать, загрузить, редактировать, добавить и удалить."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Просмотреть, скачать, загрузить, редактировать, добавить, удалить и "
"управлять командой."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Просмотреть, скачать, загрузить, редактировать, добавить, удалить и показать"
" всех приглашенных людей."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Просмотреть, скачать, загрузить, редактировать, добавить, удалить включая "
"историю."
@@ -0,0 +1,137 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Daniel Nylander <po@danielnylander.se>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-20 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Daniel Nylander <po@danielnylander.se>, 2025\n"
"Language-Team: Swedish (https://app.transifex.com/qsfera-eu/teams/204053/sv/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: sv\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Kan redigera"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Kan redigera utan versioner"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Kan hantera"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Kan skicka upp"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Kan visa"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Kan visa (säkert)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Kan inte komma åt"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Neka all åtkomst."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Här kan du lägga till en beskrivning av denna arbetsyta."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Visa och hämta ner."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
"Visa endast dokument, bilder och PDF. Vattenstämplar kommer att tillämpas."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Visa, hämta ner och redigera."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Visa, hämta ner och visa alla inbjudna personer."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Visa. hämta ner och skicka upp."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "Visa, hämta ner, redigera och visa alla inbjudna personer."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Visa, hämta ner, skicka upp, redigera, lägga till och ta bort."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Visa, hämta ner, skicka upp, redigera, lägga till, ta bort och hantera "
"medlemmar."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Visa, hämta ner, skicka upp, redigera, lägga till, ta bort och visa alla "
"inbjudna personer."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Visa, hämta ner, skicka upp, redigera, lägga till, ta bort inklusive "
"historiken."
@@ -0,0 +1,142 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# LinkinWires <darkinsonic13@gmail.com>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-08 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: LinkinWires <darkinsonic13@gmail.com>, 2025\n"
"Language-Team: Ukrainian (https://app.transifex.com/qsfera-eu/teams/204053/uk/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: uk\n"
"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Може редагувати"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Може редагувати (без версій)"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Може керувати"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Може вивантажувати"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Може переглядати"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Може переглядати (обмежено)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Не має доступу"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Заборонити будь-який доступ."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "Тут можна додати опис до цього простору."
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Може переглядати та завантажувати файли."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr ""
"Може переглядати лише документи, зображення та PDF-файли. Будуть застосовані"
" водяні знаки."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Може переглядати, завантажувати та редагувати файли."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Може переглядати, завантажувати та показувати усіх запрошених людей."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Може переглядати, завантажувати та вивантажувати файли."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr ""
"Може переглядати, завантажувати, редагувати та показувати усіх запрошених "
"людей."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr ""
"Може переглядати, завантажувати, вивантажувати, редагувати, додавати та "
"видаляти файли."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr ""
"Може переглядати, завантажувати, вивантажувати, редагувати, додавати та "
"видаляти файли, а також може керувати учасниками."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Може переглядати, завантажувати, редагувати, додавати, видаляти та "
"показувати усіх запрошених людей."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr ""
"Може переглядати, завантажувати, вивантажувати, редагувати, додавати та "
"видаляти файли, у тому числі їх історію."
@@ -0,0 +1,132 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Quan Tran, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-19 00:04+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Quan Tran, 2025\n"
"Language-Team: Vietnamese (https://app.transifex.com/qsfera-eu/teams/204053/vi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: vi\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "Có quyền chỉnh sửa"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "Có quyền chỉnh sửa không tạo phiên bản"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "Có quyền quản lý"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "Có quyền đăng tải"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "Có quyền xem"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "Có quyền xem (bảo mật)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "Không có quyền truy cập"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "Cấm mọi quyền truy cập."
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr ""
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "Xem và tải về."
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr "Chỉ được xem tài liệu, hình ảnh và PDF. Sẽ hiển thị hình mờ."
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "Xem, tải về và chỉnh sửa."
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "Xem, tải về và hiển thị danh sách người được mời."
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "Xem, tải về và tải lên."
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "Xem, tải về, chỉnh sửa và hiển thị danh sách người được mời."
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "Xem, tải về, tải lên, chỉnh sửa, thêm và xóa."
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr "Xem, tải về, tải lên, chỉnh sửa, thêm, xóa và quản lý thành viên."
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr ""
"Xem, tải về, tải lên, chỉnh sửa, thêm, xóa và hiển thị danh sách người được "
"mời."
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr "Xem, tải về, tải lên, chỉnh sửa, thêm và xóa bao gồm lịch sử."
@@ -0,0 +1,130 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# YQS Yang, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: YQS Yang, 2025\n"
"Language-Team: Chinese (https://app.transifex.com/qsfera-eu/teams/204053/zh/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. UnifiedRole Editor, Role DisplayName (resolves directly)
#. UnifiedRole EditorListGrants, Role DisplayName (resolves directly)
#. UnifiedRole SpaseEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditor, Role DisplayName (resolves directly)
#. UnifiedRole FileEditorListGrants, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:116 pkg/unifiedrole/roles.go:122
#: pkg/unifiedrole/roles.go:128 pkg/unifiedrole/roles.go:140
#: pkg/unifiedrole/roles.go:146
msgid "Can edit"
msgstr "可编辑"
#. UnifiedRole SpaseEditorWithoutVersions, Role DisplayName (resolves
#. directly)
#: pkg/unifiedrole/roles.go:134
msgid "Can edit without versions"
msgstr "可编辑(无版本控制)"
#. UnifiedRole Manager, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:158
msgid "Can manage"
msgstr "可管理"
#. UnifiedRole EditorLite, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:152
msgid "Can upload"
msgstr "可上传"
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole Viewer, Role DisplayName (resolves directly)
#. UnifiedRole SpaseViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:98 pkg/unifiedrole/roles.go:104
#: pkg/unifiedrole/roles.go:110
msgid "Can view"
msgstr "可查看"
#. UnifiedRole SecureViewer, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:164
msgid "Can view (secure)"
msgstr "可查看(安全)"
#. UnifiedRole FullDenial, Role DisplayName (resolves directly)
#: pkg/unifiedrole/roles.go:170
msgid "Cannot access"
msgstr "无法访问"
#. UnifiedRole FullDenial, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:167
msgid "Deny all access."
msgstr "拒绝所有访问。"
#. default description for new spaces
#: pkg/service/v0/spacetemplates.go:32
msgid "Here you can add a description for this Space."
msgstr "此处可为该空间添加描述。"
#. UnifiedRole Viewer, Role Description (resolves directly)
#. UnifiedRole SpaceViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:95 pkg/unifiedrole/roles.go:107
msgid "View and download."
msgstr "查看和下载。"
#. UnifiedRole SecureViewer, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:161
msgid "View only documents, images and PDFs. Watermarks will be applied."
msgstr "仅可查看文档、图片和PDF,且会添加水印。"
#. UnifiedRole FileEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:137
msgid "View, download and edit."
msgstr "查看、下载和编辑。"
#. UnifiedRole ViewerListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:101
msgid "View, download and show all invited people."
msgstr "查看、下载并显示所有受邀人员。"
#. UnifiedRole EditorLite, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:149
msgid "View, download and upload."
msgstr "查看、下载和上传。"
#. UnifiedRole FileEditorListGrants, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:143
msgid "View, download, edit and show all invited people."
msgstr "查看、下载、编辑并显示所有受邀人员。"
#. UnifiedRole Editor, Role Description (resolves directly)
#. UnifiedRole SpaseEditorWithoutVersions, Role Description (resolves
#. directly)
#: pkg/unifiedrole/roles.go:113 pkg/unifiedrole/roles.go:131
msgid "View, download, upload, edit, add and delete."
msgstr "查看、下载、上传、编辑、添加和删除。"
#. UnifiedRole Manager, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:155
msgid "View, download, upload, edit, add, delete and manage members."
msgstr "查看、下载、上传、编辑、添加、删除和管理成员。"
#. UnifiedRoleListGrants Editor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:119
msgid "View, download, upload, edit, add, delete and show all invited people."
msgstr "查看、下载、上传、编辑、添加、删除并显示所有受邀人员。"
#. UnifiedRole SpaseEditor, Role Description (resolves directly)
#: pkg/unifiedrole/roles.go:125
msgid "View, download, upload, edit, add, delete including the history."
msgstr "查看、下载、上传、编辑、添加和删除(含历史版本)。"
@@ -0,0 +1,32 @@
package l10n
import (
"embed"
"github.com/qsfera/server/pkg/l10n"
)
var (
//go:embed locale
_localeFS embed.FS
)
const (
// subfolder where the translation files are stored
_localeSubPath = "locale"
// domain of the graph service (transifex)
_domain = "graph"
)
// Translate translates a string based on the locale and default locale
func Translate(content, locale, defaultLocale, translationPath string) string {
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, translationPath, _localeFS, _localeSubPath)
return t.Translate(content, locale)
}
// TranslateEntity returns a function that translates a struct or slice based on the locale
func TranslateEntity(locale, defaultLocale string, entity any, opts ...l10n.TranslateOption) error {
t := l10n.NewTranslatorFromCommonConfig(defaultLocale, _domain, "", _localeFS, _localeSubPath)
return t.TranslateEntity(locale, entity, opts...)
}
@@ -0,0 +1,182 @@
package linktype
import (
"errors"
linkv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/unifiedrole"
)
// NoPermissionMatchError is the message returned by a failed conversion
const NoPermissionMatchError = "no matching permission set found"
// LinkType contains cs3 permissions and a libregraph
// linktype reference
type LinkType struct {
Permissions *provider.ResourcePermissions
linkType libregraph.SharingLinkType
}
// GetPermissions returns the cs3 permissions type
func (l *LinkType) GetPermissions() *provider.ResourcePermissions {
if l != nil {
return l.Permissions
}
return nil
}
// SharingLinkTypeFromCS3Permissions creates a libregraph link type
// It returns a list of libregraph actions when the conversion is not possible
func SharingLinkTypeFromCS3Permissions(permissions *linkv1beta1.PublicSharePermissions) (*libregraph.SharingLinkType, []string) {
if permissions == nil {
return nil, nil
}
linkTypes := GetAvailableLinkTypes()
for _, linkType := range linkTypes {
if grants.PermissionsEqual(linkType.GetPermissions(), permissions.GetPermissions()) {
return &linkType.linkType, nil
}
}
return nil, unifiedrole.CS3ResourcePermissionsToLibregraphActions(permissions.GetPermissions())
}
// CS3ResourcePermissionsFromSharingLink creates a cs3 resource permissions type
// it returns an error when the link type is not allowed or empty
func CS3ResourcePermissionsFromSharingLink(createLink libregraph.DriveItemCreateLink, info provider.ResourceType) (*provider.ResourcePermissions, error) {
switch createLink.GetType() {
case "":
return nil, errors.New("link type is empty")
case libregraph.VIEW:
return NewViewLinkPermissionSet().GetPermissions(), nil
case libregraph.EDIT:
if info == provider.ResourceType_RESOURCE_TYPE_FILE {
return NewFileEditLinkPermissionSet().GetPermissions(), nil
}
return NewFolderEditLinkPermissionSet().GetPermissions(), nil
case libregraph.CREATE_ONLY:
if info == provider.ResourceType_RESOURCE_TYPE_FILE {
return nil, errors.New(NoPermissionMatchError)
}
return NewFolderDropLinkPermissionSet().GetPermissions(), nil
case libregraph.UPLOAD:
if info == provider.ResourceType_RESOURCE_TYPE_FILE {
return nil, errors.New(NoPermissionMatchError)
}
return NewFolderUploadLinkPermissionSet().GetPermissions(), nil
case libregraph.INTERNAL:
return NewInternalLinkPermissionSet().GetPermissions(), nil
default:
return nil, errors.New(NoPermissionMatchError)
}
}
// NewInternalLinkPermissionSet creates cs3 permissions for the internal link type
func NewInternalLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{},
linkType: libregraph.INTERNAL,
}
}
// NewViewLinkPermissionSet creates cs3 permissions for the view link type
func NewViewLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
ListContainer: true,
// why is this needed?
ListRecycle: true,
Stat: true,
},
linkType: libregraph.VIEW,
}
}
// NewFileEditLinkPermissionSet creates cs3 permissions for the file edit link type
func NewFileEditLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
InitiateFileUpload: true,
ListContainer: true,
// why is this needed?
ListRecycle: true,
// why is this needed?
RestoreRecycleItem: true,
Stat: true,
},
linkType: libregraph.EDIT,
}
}
// NewFolderEditLinkPermissionSet creates cs3 permissions for the folder edit link type
func NewFolderEditLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{
CreateContainer: true,
Delete: true,
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
InitiateFileUpload: true,
ListContainer: true,
// why is this needed?
ListRecycle: true,
Move: true,
// why is this needed?
RestoreRecycleItem: true,
Stat: true,
},
linkType: libregraph.EDIT,
}
}
// NewFolderDropLinkPermissionSet creates cs3 permissions for the folder createOnly link type
func NewFolderDropLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{
Stat: true,
GetPath: true,
CreateContainer: true,
InitiateFileUpload: true,
},
linkType: libregraph.CREATE_ONLY,
}
}
// NewFolderUploadLinkPermissionSet creates cs3 permissions for the folder upload link type
func NewFolderUploadLinkPermissionSet() *LinkType {
return &LinkType{
Permissions: &provider.ResourcePermissions{
CreateContainer: true,
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
InitiateFileUpload: true,
ListContainer: true,
ListRecycle: true,
Stat: true,
},
linkType: libregraph.UPLOAD,
}
}
// GetAvailableLinkTypes returns a slice of all available link types
func GetAvailableLinkTypes() []*LinkType {
return []*LinkType{
NewInternalLinkPermissionSet(),
NewViewLinkPermissionSet(),
NewFolderUploadLinkPermissionSet(),
NewFileEditLinkPermissionSet(),
NewFolderEditLinkPermissionSet(),
NewFolderDropLinkPermissionSet(),
}
}
@@ -0,0 +1,13 @@
package linktype_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestLinktype(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Linktype Suite")
}
@@ -0,0 +1,142 @@
package linktype_test
import (
linkv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/qsfera/server/services/graph/pkg/linktype"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
var _ = Describe("LinktypeFromPermission", func() {
var (
internalLinkType, _ = libregraph.NewSharingLinkTypeFromValue("internal")
createOnlyLinkType, _ = libregraph.NewSharingLinkTypeFromValue("createOnly")
viewLinkType, _ = libregraph.NewSharingLinkTypeFromValue("view")
uploadLinkType, _ = libregraph.NewSharingLinkTypeFromValue("upload")
editLinkType, _ = libregraph.NewSharingLinkTypeFromValue("edit")
folderEditPermsHaveChanged = linktype.NewFolderEditLinkPermissionSet().GetPermissions()
)
BeforeEach(func() {
// simulate that permissions have changed after link creation
folderEditPermsHaveChanged.CreateContainer = false
})
DescribeTable("SharingLinkTypeFromCS3Permissions",
func(permissions *linkv1beta1.PublicSharePermissions,
expectedSharingLinkType *libregraph.SharingLinkType,
expectedActions []string) {
sharingLinkType, actions := linktype.SharingLinkTypeFromCS3Permissions(permissions)
Expect(sharingLinkType).To(Equal(expectedSharingLinkType))
Expect(expectedActions).To(Equal(actions))
},
Entry("Internal",
&linkv1beta1.PublicSharePermissions{Permissions: linktype.NewInternalLinkPermissionSet().GetPermissions()},
internalLinkType,
nil,
),
Entry("CreateOnly",
&linkv1beta1.PublicSharePermissions{Permissions: linktype.NewFolderDropLinkPermissionSet().GetPermissions()},
createOnlyLinkType,
nil,
),
Entry("View File",
&linkv1beta1.PublicSharePermissions{Permissions: linktype.NewViewLinkPermissionSet().GetPermissions()},
viewLinkType,
nil,
),
Entry("Upload in Folder",
&linkv1beta1.PublicSharePermissions{Permissions: linktype.NewFolderUploadLinkPermissionSet().GetPermissions()},
uploadLinkType,
nil,
),
Entry("File Edit",
&linkv1beta1.PublicSharePermissions{Permissions: linktype.NewFileEditLinkPermissionSet().GetPermissions()},
editLinkType,
nil,
),
Entry("Folder Edit",
&linkv1beta1.PublicSharePermissions{Permissions: linktype.NewFolderEditLinkPermissionSet().GetPermissions()},
editLinkType,
nil,
),
Entry("Folder Edit- Permissions have changed after creation",
&linkv1beta1.PublicSharePermissions{Permissions: folderEditPermsHaveChanged},
nil,
[]string{
"libre.graph/driveItem/standard/delete",
"libre.graph/driveItem/path/read",
"libre.graph/driveItem/quota/read",
"libre.graph/driveItem/content/read",
"libre.graph/driveItem/upload/create",
"libre.graph/driveItem/children/read",
"libre.graph/driveItem/deleted/read",
"libre.graph/driveItem/path/update",
"libre.graph/driveItem/deleted/update",
"libre.graph/driveItem/basic/read",
},
),
)
DescribeTable("CS3ResourcePermissionsFromSharingLink",
func(createLink libregraph.DriveItemCreateLink,
info provider.ResourceType,
expectedPermissions *provider.ResourcePermissions,
hasError bool) {
permissions, err := linktype.CS3ResourcePermissionsFromSharingLink(createLink, info)
if hasError == true {
Expect(err).To(HaveOccurred())
} else {
Expect(err).ToNot(HaveOccurred())
Expect(grants.PermissionsEqual(permissions, expectedPermissions)).To(BeTrue())
}
},
Entry("Internal",
libregraph.DriveItemCreateLink{Type: internalLinkType},
provider.ResourceType_RESOURCE_TYPE_FILE, linktype.NewInternalLinkPermissionSet().GetPermissions(),
false,
),
Entry("Create Only",
libregraph.DriveItemCreateLink{Type: createOnlyLinkType},
provider.ResourceType_RESOURCE_TYPE_CONTAINER, linktype.NewFolderDropLinkPermissionSet().GetPermissions(),
false,
),
Entry("Create Only",
libregraph.DriveItemCreateLink{Type: createOnlyLinkType},
provider.ResourceType_RESOURCE_TYPE_FILE, linktype.NewFolderDropLinkPermissionSet().GetPermissions(),
true,
),
Entry("View File",
libregraph.DriveItemCreateLink{Type: viewLinkType},
provider.ResourceType_RESOURCE_TYPE_FILE, linktype.NewViewLinkPermissionSet().GetPermissions(),
false,
),
Entry("View Folder",
libregraph.DriveItemCreateLink{Type: viewLinkType},
provider.ResourceType_RESOURCE_TYPE_CONTAINER, linktype.NewViewLinkPermissionSet().GetPermissions(),
false,
),
Entry("Upload in Folder",
libregraph.DriveItemCreateLink{Type: uploadLinkType},
provider.ResourceType_RESOURCE_TYPE_CONTAINER, linktype.NewFolderUploadLinkPermissionSet().GetPermissions(),
false,
),
Entry("File Edit",
libregraph.DriveItemCreateLink{Type: editLinkType},
provider.ResourceType_RESOURCE_TYPE_FILE, linktype.NewFileEditLinkPermissionSet().GetPermissions(),
false,
),
Entry("Folder Edit",
libregraph.DriveItemCreateLink{Type: editLinkType},
provider.ResourceType_RESOURCE_TYPE_CONTAINER, linktype.NewFolderEditLinkPermissionSet().GetPermissions(),
false,
),
)
})
@@ -0,0 +1,33 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
// Namespace defines the namespace for the defines metrics.
Namespace = "qsfera"
// Subsystem defines the subsystem for the defines metrics.
Subsystem = "graph"
)
// 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
}
@@ -0,0 +1,95 @@
package middleware
import (
"net/http"
gmmetadata "go-micro.dev/v4/metadata"
"google.golang.org/grpc/metadata"
"github.com/qsfera/server/pkg/account"
"github.com/qsfera/server/pkg/log"
opkgm "github.com/qsfera/server/pkg/middleware"
"github.com/qsfera/server/services/graph/pkg/errorcode"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/token/manager/jwt"
)
// authOptions initializes the available default options.
func authOptions(opts ...account.Option) account.Options {
opt := account.Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Auth provides a middleware to authenticate requests using the x-access-token header value
// and write it to the context. If there is no x-access-token the middleware prevents access and renders a json document.
func Auth(opts ...account.Option) func(http.Handler) http.Handler {
// Note: This largely duplicates what pkg/middleware/account.go already does (apart from a slightly different error
// handling). Ideally we should merge both middlewares.
opt := authOptions(opts...)
tokenManager, err := jwt.New(map[string]any{
"secret": opt.JWTSecret,
"expires": int64(24 * 60 * 60),
})
if err != nil {
opt.Logger.Fatal().Err(err).Msgf("Could not initialize token-manager")
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
t := r.Header.Get(revactx.TokenHeader)
if t == "" {
errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, "Access token is empty.")
/* msgraph error for GET https://graph.microsoft.com/v1.0/me
{
"error":
{
"code":"InvalidAuthenticationToken",
"message":"Access token is empty.",
"innerError":{
"date":"2021-07-09T14:40:51",
"request-id":"bb12f7db-b4c4-43a9-ba4b-31676aeed019",
"client-request-id":"bb12f7db-b4c4-43a9-ba4b-31676aeed019"
}
}
}
*/
return
}
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), t)
if err != nil {
errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, "invalid token")
return
}
if ok, err := scope.VerifyScope(ctx, tokenScope, r); err != nil || !ok {
opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Err(err).Msg("verifying scope failed")
errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, "verifying scope failed")
return
}
ctx = revactx.ContextSetToken(ctx, t)
ctx = revactx.ContextSetUser(ctx, u)
ctx = gmmetadata.Set(ctx, opkgm.AccountID, u.GetId().GetOpaqueId())
if m := u.GetOpaque().GetMap(); m != nil {
if roles, ok := m["roles"]; ok {
ctx = gmmetadata.Set(ctx, opkgm.RoleIDs, string(roles.GetValue()))
}
}
ctx = metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, t)
initiatorID := r.Header.Get(revactx.InitiatorHeader)
if initiatorID != "" {
ctx = revactx.ContextSetInitiator(ctx, initiatorID)
ctx = metadata.AppendToOutgoingContext(ctx, revactx.InitiatorHeader, initiatorID)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
@@ -0,0 +1,54 @@
package middleware
import (
"net/http"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/roles"
"github.com/qsfera/server/services/graph/pkg/errorcode"
settings "github.com/qsfera/server/services/settings/pkg/service/v0"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
)
// RequireAdmin middleware is used to require the user in context to be an admin / have account management permissions
func RequireAdmin(rm *roles.Manager, logger log.Logger) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
l := logger.With().Str("middleware", "requireAdmin").Logger()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, ok := revactx.ContextGetUser(r.Context())
if !ok {
errorcode.AccessDenied.Render(w, r, http.StatusUnauthorized, "Unauthorized")
return
}
if u.Id == nil || u.Id.OpaqueId == "" {
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "user is missing an id")
return
}
// get roles from context
roleIDs, ok := roles.ReadRoleIDsFromContext(r.Context())
if !ok {
l.Debug().Str("userid", u.Id.OpaqueId).Msg("No roles in context, contacting settings service")
var err error
roleIDs, err = rm.FindRoleIDsForUser(r.Context(), u.Id.OpaqueId)
if err != nil {
l.Error().Err(err).Str("userid", u.Id.OpaqueId).Msg("Failed to get roles for user")
errorcode.AccessDenied.Render(w, r, http.StatusUnauthorized, "Unauthorized")
return
}
if len(roleIDs) == 0 {
l.Error().Err(err).Str("userid", u.Id.OpaqueId).Msg("No roles assigned to user")
errorcode.AccessDenied.Render(w, r, http.StatusUnauthorized, "Unauthorized")
return
}
}
// check if permission is present in roles of the authenticated account
if rm.FindPermissionByID(r.Context(), roleIDs, settings.AccountManagementPermissionID) != nil {
next.ServeHTTP(w, r)
return
}
errorcode.AccessDenied.Render(w, r, http.StatusForbidden, "Forbidden")
})
}
}
@@ -0,0 +1,45 @@
package middleware
import (
"crypto/sha256"
"crypto/subtle"
"net/http"
"github.com/qsfera/server/services/graph/pkg/errorcode"
)
var (
// ErrInvalidToken is returned when the request token is invalid.
ErrInvalidToken = "invalid or missing token"
)
// Token provides a middleware to check access secured by a static token.
func Token(token string) func(http.Handler) http.Handler {
h := sha256.New()
requiredTokenHash := h.Sum(([]byte("Bearer " + token)))
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if token == "" {
next.ServeHTTP(w, r)
return
}
header := r.Header.Get("Authorization")
if header == "" {
errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, ErrInvalidToken)
return
}
h = sha256.New()
providedTokenHash := h.Sum([]byte(header))
if subtle.ConstantTimeCompare(requiredTokenHash, providedTokenHash) == 0 {
errorcode.InvalidAuthenticationToken.Render(w, r, http.StatusUnauthorized, ErrInvalidToken)
return
}
next.ServeHTTP(w, r)
})
}
}
@@ -0,0 +1,49 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
)
type dummyHandler struct{}
func (h dummyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func TestToken(t *testing.T) {
dh := dummyHandler{}
handler := Token("test-api-key")(dh)
req, err := http.NewRequest("GET", "/token-protected", nil)
req.Header.Set("Authorization", "Bearer wrong")
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusUnauthorized {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusUnauthorized)
}
req, err = http.NewRequest("GET", "/token-protected", nil)
req.Header.Set("Authorization", "Bearer test-api-key")
if err != nil {
t.Fatal(err)
}
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
}
+104
View File
@@ -0,0 +1,104 @@
package odata
import (
"strings"
"github.com/CiscoM31/godata"
)
// GetExpandValues extracts the values of the $expand query parameter and
// returns them in a []string, rejecting any $expand value that consists of more
// than just a single path segment.
func GetExpandValues(req *godata.GoDataQuery) ([]string, error) {
if req == nil || req.Expand == nil {
return []string{}, nil
}
var expand []string
for _, item := range req.Expand.ExpandItems {
paths, err := collectExpandPaths(item, "")
if err != nil {
return nil, err
}
expand = append(expand, paths...)
}
return expand, nil
}
// collectExpandPaths recursively collects all valid expand paths from the given item.
func collectExpandPaths(item *godata.ExpandItem, prefix string) ([]string, error) {
if err := validateExpandItem(item); err != nil {
return nil, err
}
// Build the current path
currentPath := prefix
if len(item.Path) > 1 {
return nil, godata.NotImplementedError("multiple segments in $expand not supported")
}
if len(item.Path) == 1 {
if currentPath == "" {
currentPath = item.Path[0].Value
} else {
currentPath += "." + item.Path[0].Value
}
}
// Collect all paths, including nested ones
paths := []string{currentPath}
if item.Expand != nil {
for _, subItem := range item.Expand.ExpandItems {
subPaths, err := collectExpandPaths(subItem, currentPath)
if err != nil {
return nil, err
}
paths = append(paths, subPaths...)
}
}
return paths, nil
}
// validateExpandItem checks if an expand item contains unsupported options.
func validateExpandItem(item *godata.ExpandItem) error {
if item.Filter != nil || item.At != nil || item.Search != nil ||
item.OrderBy != nil || item.Skip != nil || item.Top != nil ||
item.Select != nil || item.Compute != nil || item.Levels != 0 {
return godata.NotImplementedError("options for $expand not supported")
}
return nil
}
// GetSelectValues extracts the values of the $select query parameter and
// returns them in a []string, rejects any $select value that consists of more
// than just a single path segment
func GetSelectValues(req *godata.GoDataQuery) ([]string, error) {
if req == nil || req.Select == nil {
return []string{}, nil
}
sel := make([]string, 0, len(req.Select.SelectItems))
for _, item := range req.Select.SelectItems {
if len(item.Segments) > 1 {
return []string{}, godata.NotImplementedError("multiple segments in $select not supported")
}
sel = append(sel, item.Segments[0].Value)
}
return sel, nil
}
// GetSearchValues extracts the value of the $search query parameter and returns
// it as a string. Rejects any search query that is more than just a simple string
func GetSearchValues(req *godata.GoDataQuery) (string, error) {
if req == nil || req.Search == nil {
return "", nil
}
// Only allow simple search queries for now
if len(req.Search.Tree.Children) != 0 {
return "", godata.NotImplementedError("complex search queries are not supported")
}
searchValue := strings.Trim(req.Search.Tree.Token.Value, "\"")
return searchValue, nil
}
@@ -0,0 +1,160 @@
package odata
import (
"testing"
"github.com/CiscoM31/godata"
"github.com/stretchr/testify/assert"
)
func TestGetExpandValues(t *testing.T) {
t.Run("NilRequest", func(t *testing.T) {
result, err := GetExpandValues(nil)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("EmptyExpand", func(t *testing.T) {
req := &godata.GoDataQuery{}
result, err := GetExpandValues(req)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("SinglePathSegment", func(t *testing.T) {
req := &godata.GoDataQuery{
Expand: &godata.GoDataExpandQuery{
ExpandItems: []*godata.ExpandItem{
{Path: []*godata.Token{{Value: "orders"}}},
},
},
}
result, err := GetExpandValues(req)
assert.NoError(t, err)
assert.Equal(t, []string{"orders"}, result)
})
t.Run("MultiplePathSegments", func(t *testing.T) {
req := &godata.GoDataQuery{
Expand: &godata.GoDataExpandQuery{
ExpandItems: []*godata.ExpandItem{
{Path: []*godata.Token{{Value: "orders"}, {Value: "details"}}},
},
},
}
result, err := GetExpandValues(req)
assert.Error(t, err)
assert.Empty(t, result)
})
t.Run("NestedExpand", func(t *testing.T) {
req := &godata.GoDataQuery{
Expand: &godata.GoDataExpandQuery{
ExpandItems: []*godata.ExpandItem{
{
Path: []*godata.Token{{Value: "items"}},
Expand: &godata.GoDataExpandQuery{
ExpandItems: []*godata.ExpandItem{
{
Path: []*godata.Token{{Value: "subitem"}},
Expand: &godata.GoDataExpandQuery{
ExpandItems: []*godata.ExpandItem{
{Path: []*godata.Token{{Value: "subsubitems"}}},
},
},
},
},
},
},
},
},
}
result, err := GetExpandValues(req)
assert.NoError(t, err)
assert.Subset(t, result, []string{"items", "items.subitem", "items.subitem.subsubitems"}, "must contain all levels of expansion")
})
}
func TestGetSelectValues(t *testing.T) {
t.Run("NilRequest", func(t *testing.T) {
result, err := GetSelectValues(nil)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("EmptySelect", func(t *testing.T) {
req := &godata.GoDataQuery{}
result, err := GetSelectValues(req)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("SinglePathSegment", func(t *testing.T) {
req := &godata.GoDataQuery{
Select: &godata.GoDataSelectQuery{
SelectItems: []*godata.SelectItem{
{Segments: []*godata.Token{{Value: "name"}}},
},
},
}
result, err := GetSelectValues(req)
assert.NoError(t, err)
assert.Equal(t, []string{"name"}, result)
})
t.Run("MultiplePathSegments", func(t *testing.T) {
req := &godata.GoDataQuery{
Select: &godata.GoDataSelectQuery{
SelectItems: []*godata.SelectItem{
{Segments: []*godata.Token{{Value: "name"}, {Value: "first"}}},
},
},
}
result, err := GetSelectValues(req)
assert.Error(t, err)
assert.Empty(t, result)
})
}
func TestGetSearchValues(t *testing.T) {
t.Run("NilRequest", func(t *testing.T) {
result, err := GetSearchValues(nil)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("EmptySearch", func(t *testing.T) {
req := &godata.GoDataQuery{}
result, err := GetSearchValues(req)
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("SimpleSearch", func(t *testing.T) {
req := &godata.GoDataQuery{
Search: &godata.GoDataSearchQuery{
Tree: &godata.ParseNode{
Token: &godata.Token{Value: "test"},
},
},
}
result, err := GetSearchValues(req)
assert.NoError(t, err)
assert.Equal(t, "test", result)
})
t.Run("ComplexSearch", func(t *testing.T) {
req := &godata.GoDataQuery{
Search: &godata.GoDataSearchQuery{
Tree: &godata.ParseNode{
Children: []*godata.ParseNode{
{Token: &godata.Token{Value: "test"}},
},
},
},
}
result, err := GetSearchValues(req)
assert.Error(t, err)
assert.Empty(t, result)
})
}
@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/graph/pkg/config"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Context context.Context
Config *config.Config
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
@@ -0,0 +1,50 @@
package debug
import (
"net/http"
"net/url"
"github.com/qsfera/server/pkg/checks"
"github.com/qsfera/server/pkg/handlers"
"github.com/qsfera/server/pkg/service/debug"
"github.com/qsfera/server/pkg/version"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
healthHandlerConfiguration := handlers.NewCheckHandlerConfiguration().
WithLogger(options.Logger).
WithCheck("web reachability", checks.NewHTTPCheck(options.Config.HTTP.Addr))
readyHandlerConfiguration := healthHandlerConfiguration
// Check for LDAP reachability, when we're using the LDAP backend
if options.Config.Identity.Backend == "ldap" {
u, err := url.Parse(options.Config.Identity.LDAP.URI)
if err != nil {
return nil, err
}
readyHandlerConfiguration = readyHandlerConfiguration.
WithCheck("ldap reachability", checks.NewTCPCheck(u.Host))
}
// only check nats if really needed
if options.Config.Events.Endpoint != "" {
readyHandlerConfiguration = readyHandlerConfiguration.
WithCheck("nats reachability", checks.NewNatsCheck(options.Config.Events.Endpoint))
}
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.GetString()),
debug.Address(options.Config.Debug.Addr),
debug.Token(options.Config.Debug.Token),
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
debug.Health(handlers.NewCheckHandler(healthHandlerConfiguration)),
debug.Ready(handlers.NewCheckHandler(readyHandlerConfiguration)),
), nil
}
@@ -0,0 +1,95 @@
package http
import (
"context"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/metrics"
"github.com/nats-io/nats.go/jetstream"
"github.com/spf13/pflag"
"go.opentelemetry.io/otel/trace"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Context context.Context
Config *config.Config
Metrics *metrics.Metrics
Flags []pflag.Flag
Namespace string
TraceProvider trace.TracerProvider
NatsKeyValue jetstream.KeyValue
}
// 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
}
}
// Metrics provides a function to set the metrics option.
func Metrics(val *metrics.Metrics) Option {
return func(o *Options) {
o.Metrics = val
}
}
// Flags provides a function to set the flags option.
func Flags(flags ...pflag.Flag) Option {
return func(o *Options) {
o.Flags = append(o.Flags, flags...)
}
}
// Namespace provides a function to set the Namespace option.
func Namespace(val string) Option {
return func(o *Options) {
o.Namespace = val
}
}
// TraceProvider provides a function to set the TraceProvider option.
func TraceProvider(val trace.TracerProvider) Option {
return func(o *Options) {
o.TraceProvider = val
}
}
// NatsKeyValue provides a function to set the NatsKeyValue option.
func NatsKeyValue(val jetstream.KeyValue) Option {
return func(o *Options) {
o.NatsKeyValue = val
}
}
@@ -0,0 +1,193 @@
package http
import (
"context"
"errors"
"fmt"
stdhttp "net/http"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
revaMetadata "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"go-micro.dev/v4"
"go-micro.dev/v4/events"
"github.com/qsfera/server/pkg/account"
"github.com/qsfera/server/pkg/cors"
"github.com/qsfera/server/pkg/generators"
"github.com/qsfera/server/pkg/keycloak"
"github.com/qsfera/server/pkg/middleware"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/service/grpc"
"github.com/qsfera/server/pkg/service/http"
"github.com/qsfera/server/pkg/storage/metadata"
"github.com/qsfera/server/pkg/version"
ehsvc "github.com/qsfera/server/protogen/gen/qsfera/services/eventhistory/v0"
searchsvc "github.com/qsfera/server/protogen/gen/qsfera/services/search/v0"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
graphMiddleware "github.com/qsfera/server/services/graph/pkg/middleware"
svc "github.com/qsfera/server/services/graph/pkg/service/v0"
)
// Server initializes the http service and server.
func Server(opts ...Option) (http.Service, error) {
options := newOptions(opts...)
service, err := http.NewService(
http.TLSConfig(options.Config.HTTP.TLS),
http.Logger(options.Logger),
http.Namespace(options.Config.HTTP.Namespace),
http.Name(options.Config.Service.Name),
http.Version(version.GetString()),
http.Address(options.Config.HTTP.Addr),
http.Context(options.Context),
http.Flags(options.Flags...),
http.TraceProvider(options.TraceProvider),
)
if err != nil {
options.Logger.Error().
Err(err).
Msg("Error initializing http service")
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
}
var eventsStream events.Stream
if options.Config.Events.Endpoint != "" {
var err error
connName := generators.GenerateConnectionName(options.Config.Service.Name, generators.NTypeBus)
eventsStream, err = stream.NatsFromConfig(connName, false, stream.NatsConfig(options.Config.Events))
if err != nil {
options.Logger.Error().
Err(err).
Msg("Error initializing events publisher")
return http.Service{}, fmt.Errorf("could not initialize events publisher: %w", err)
}
}
middlewares := []func(stdhttp.Handler) stdhttp.Handler{
middleware.TraceContext,
chimiddleware.RequestID,
middleware.Version(
options.Config.Service.Name,
version.GetString(),
),
middleware.Logger(
options.Logger,
),
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),
),
}
// how do we secure the api?
var requireAdminMiddleware func(stdhttp.Handler) stdhttp.Handler
var roleService svc.RoleService
var valueService settingssvc.ValueService
var gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
grpcClient, err := grpc.NewClient(append(grpc.GetClientOptions(options.Config.GRPCClientTLS), grpc.WithTraceProvider(options.TraceProvider))...)
if err != nil {
return http.Service{}, err
}
if options.Config.HTTP.APIToken == "" {
middlewares = append(middlewares,
graphMiddleware.Auth(
account.Logger(options.Logger),
account.JWTSecret(options.Config.TokenManager.JWTSecret),
))
roleService = settingssvc.NewRoleService("qsfera.api.settings", grpcClient)
valueService = settingssvc.NewValueService("qsfera.api.settings", grpcClient)
gatewaySelector, err = pool.GatewaySelector(
options.Config.Reva.Address,
append(
options.Config.Reva.GetRevaOptions(),
pool.WithRegistry(registry.GetRegistry()),
pool.WithTracerProvider(options.TraceProvider),
)...)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize gateway selector: %w", err)
}
} else {
middlewares = append(middlewares, graphMiddleware.Token(options.Config.HTTP.APIToken))
// use a dummy admin middleware for the chi router
requireAdminMiddleware = func(next stdhttp.Handler) stdhttp.Handler {
return next
}
// no gateway client needed
}
// Keycloak client is optional, so if it stays nil, it's fine.
var keyCloakClient keycloak.Client
if options.Config.Keycloak.BasePath != "" {
kcc := options.Config.Keycloak
if kcc.ClientID == "" || kcc.ClientSecret == "" || kcc.ClientRealm == "" || kcc.UserRealm == "" {
return http.Service{}, errors.New("keycloak client id, secret, client realm and user realm must be set")
}
keyCloakClient = keycloak.New(kcc.BasePath, kcc.ClientID, kcc.ClientSecret, kcc.ClientRealm, kcc.InsecureSkipVerify)
}
hClient := ehsvc.NewEventHistoryService("qsfera.api.eventhistory", grpcClient)
var userProfilePhotoService svc.UsersUserProfilePhotoProvider
{
photoStorage, err := revaMetadata.NewCS3Storage(
options.Config.Metadata.GatewayAddress,
options.Config.Metadata.StorageAddress,
options.Config.Metadata.SystemUserID,
options.Config.Metadata.SystemUserIDP,
options.Config.Metadata.SystemUserAPIKey,
)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize reva metadata storage: %w", err)
}
photoStorage, err = metadata.NewLazyStorage(photoStorage)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize lazy metadata storage: %w", err)
}
if err := photoStorage.Init(context.Background(), "f2bdd61a-da7c-49fc-8203-0558109d1b4f"); err != nil {
return http.Service{}, fmt.Errorf("could not initialize metadata storage: %w", err)
}
userProfilePhotoService, err = svc.NewUsersUserProfilePhotoService(photoStorage)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize user profile photo service: %w", err)
}
}
var handle svc.Service
handle, err = svc.NewService(
svc.Context(options.Context),
svc.UserProfilePhotoService(userProfilePhotoService),
svc.Logger(options.Logger),
svc.Config(options.Config),
svc.Middleware(middlewares...),
svc.EventsPublisher(eventsStream),
svc.EventsConsumer(eventsStream),
svc.WithRoleService(roleService),
svc.WithValueService(valueService),
svc.WithRequireAdminMiddleware(requireAdminMiddleware),
svc.WithGatewaySelector(gatewaySelector),
svc.WithSearchService(searchsvc.NewSearchProviderService("qsfera.api.search", grpcClient)),
svc.KeycloakClient(keyCloakClient),
svc.EventHistoryClient(hClient),
svc.TraceProvider(options.TraceProvider),
svc.WithNatsKeyValue(options.NatsKeyValue),
)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize graph service: %w", err)
}
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
return http.Service{}, fmt.Errorf("could not register graph service handler: %w", err)
}
return service, nil
}
@@ -0,0 +1,991 @@
package svc
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"github.com/CiscoM31/godata"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/share"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/qsfera/server/pkg/l10n"
l10n_pkg "github.com/qsfera/server/services/graph/pkg/l10n"
"github.com/qsfera/server/services/graph/pkg/odata"
"github.com/qsfera/server/pkg/conversions"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/errorcode"
"github.com/qsfera/server/services/graph/pkg/identity"
"github.com/qsfera/server/services/graph/pkg/identity/cache"
"github.com/qsfera/server/services/graph/pkg/unifiedrole"
"github.com/qsfera/server/services/graph/pkg/validate"
)
const (
invalidIdMsg = "invalid driveID or itemID"
parseDriveIDErrMsg = "could not parse driveID"
federatedRolesODataFilter = "@libre.graph.permissions.roles.allowedValues/rolePermissions/any(p:contains(p/condition, '@Subject.UserType==\"Federated\"'))"
noLinksODataFilter = "grantedToV2 ne ''"
)
// DriveItemPermissionsProvider contains the methods related to handling permissions on drive items
type DriveItemPermissionsProvider interface {
Invite(ctx context.Context, resourceId *storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error)
SpaceRootInvite(ctx context.Context, driveID *storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error)
ListPermissions(ctx context.Context, itemID *storageprovider.ResourceId, queryOptions ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)
ListSpaceRootPermissions(ctx context.Context, driveID *storageprovider.ResourceId, queryOptions ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error)
DeletePermission(ctx context.Context, itemID *storageprovider.ResourceId, permissionID string) error
DeleteSpaceRootPermission(ctx context.Context, driveID *storageprovider.ResourceId, permissionID string) error
UpdatePermission(ctx context.Context, itemID *storageprovider.ResourceId, permissionID string, newPermission libregraph.Permission) (libregraph.Permission, error)
UpdateSpaceRootPermission(ctx context.Context, driveID *storageprovider.ResourceId, permissionID string, newPermission libregraph.Permission) (libregraph.Permission, error)
CreateLink(ctx context.Context, driveItemID *storageprovider.ResourceId, createLink libregraph.DriveItemCreateLink) (libregraph.Permission, error)
CreateSpaceRootLink(ctx context.Context, driveID *storageprovider.ResourceId, createLink libregraph.DriveItemCreateLink) (libregraph.Permission, error)
SetPublicLinkPassword(ctx context.Context, driveItemID *storageprovider.ResourceId, permissionID string, password string) (libregraph.Permission, error)
SetPublicLinkPasswordOnSpaceRoot(ctx context.Context, driveID *storageprovider.ResourceId, permissionID string, password string) (libregraph.Permission, error)
}
// DriveItemPermissionsService contains the production business logic for everything that relates to permissions on drive items.
type DriveItemPermissionsService struct {
BaseGraphService
}
type permissionType int
const (
Unknown permissionType = iota
Public
User
Space
OCM
)
type ListPermissionsQueryOptions struct {
Count bool
NoValues bool
NoLinkPermissions bool
FilterFederatedRoles bool
SelectedAttrs []string
}
// NewDriveItemPermissionsService creates a new DriveItemPermissionsService
func NewDriveItemPermissionsService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], identityCache cache.IdentityCache, config *config.Config) (DriveItemPermissionsService, error) {
publicBaseURL, err := url.Parse(config.Spaces.WebDavBase)
if err != nil {
return DriveItemPermissionsService{}, fmt.Errorf("could not parse graph.spaces.webdav_base: %w", err)
}
return DriveItemPermissionsService{
BaseGraphService: BaseGraphService{
logger: &log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemService").Logger()},
gatewaySelector: gatewaySelector,
identityCache: identityCache,
config: config,
availableRoles: unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(config.UnifiedRoles.AvailableRoles...)),
publicBaseURL: publicBaseURL,
},
}, nil
}
// Invite invites a user to a drive item.
func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) {
tenantId := revactx.ContextMustGetUser(ctx).GetId().GetTenantId()
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return libregraph.Permission{}, err
}
statResponse, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: resourceId}})
if err := errorcode.FromStat(statResponse, err); err != nil {
s.logger.Warn().Err(err).Interface("stat.res", statResponse).Msg("stat failed")
return libregraph.Permission{}, err
}
var condition string
if condition, err = roleConditionForResourceType(statResponse.GetInfo()); err != nil {
return libregraph.Permission{}, err
}
unifiedRolePermissions := []*libregraph.UnifiedRolePermission{{AllowedResourceActions: invite.LibreGraphPermissionsActions}}
for _, roleID := range invite.GetRoles() {
// only allow roles that are enabled in the config
if !slices.Contains(s.config.UnifiedRoles.AvailableRoles, roleID) {
return libregraph.Permission{}, unifiedrole.ErrUnknownRole
}
role, err := unifiedrole.GetRole(unifiedrole.RoleFilterIDs(roleID))
if err != nil {
s.logger.Debug().Err(err).Interface("role", invite.GetRoles()[0]).Msg("unable to convert requested role")
return libregraph.Permission{}, err
}
allowedResourceActions := unifiedrole.GetAllowedResourceActions(role, condition)
if len(allowedResourceActions) == 0 && role.GetId() != unifiedrole.UnifiedRoleDeniedID {
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "role not applicable to this resource")
}
unifiedRolePermissions = append(unifiedRolePermissions, conversions.ToPointerSlice(role.GetRolePermissions())...)
}
driveRecipient := invite.GetRecipients()[0]
objectID := driveRecipient.GetObjectId()
cs3ResourcePermissions := unifiedrole.PermissionsToCS3ResourcePermissions(unifiedRolePermissions)
permission := &libregraph.Permission{}
availableRoles := unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(s.config.UnifiedRoles.AvailableRoles...))
if role := unifiedrole.CS3ResourcePermissionsToRole(availableRoles, cs3ResourcePermissions, condition, false); role != nil {
permission.Roles = []string{role.GetId()}
}
if len(permission.GetRoles()) == 0 {
permission.LibreGraphPermissionsActions = unifiedrole.CS3ResourcePermissionsToLibregraphActions(cs3ResourcePermissions)
}
var shareid string
var expiration *types.Timestamp
var cTime *types.Timestamp
switch driveRecipient.GetLibreGraphRecipientType() {
case "group":
group, err := s.identityCache.GetGroup(ctx, objectID)
if err != nil {
s.logger.Debug().Err(err).Interface("groupId", objectID).Msg("failed group lookup")
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error())
}
permission.GrantedToV2 = &libregraph.SharePointIdentitySet{
Group: &libregraph.Identity{
DisplayName: group.GetDisplayName(),
Id: conversions.ToPointer(group.GetId()),
},
}
createShareRequest := createShareRequestToGroup(group, statResponse.GetInfo(), cs3ResourcePermissions)
if invite.ExpirationDateTime != nil {
createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime)
}
createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest)
if err := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {
s.logger.Debug().Err(err).Msg("share creation failed")
return libregraph.Permission{}, err
}
shareid = createShareResponse.GetShare().GetId().GetOpaqueId()
cTime = createShareResponse.GetShare().GetCtime()
expiration = createShareResponse.GetShare().GetExpiration()
default:
user, err := s.identityCache.GetCS3User(ctx, tenantId, objectID)
if errors.Is(err, identity.ErrNotFound) && s.config.IncludeOCMSharees {
user, err = s.identityCache.GetAcceptedCS3User(ctx, objectID)
if err == nil && IsSpaceRoot(statResponse.GetInfo().GetId()) {
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "federated user can not become a space member")
}
}
if err != nil {
s.logger.Debug().Err(err).Interface("userId", objectID).Msg("failed user lookup")
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error())
}
permission.GrantedToV2 = &libregraph.SharePointIdentitySet{
User: &libregraph.Identity{
DisplayName: user.GetDisplayName(),
Id: conversions.ToPointer(user.GetId().GetOpaqueId()),
LibreGraphUserType: conversions.ToPointer(identity.CS3UserTypeToGraph(user.GetId().GetType())),
},
}
if user.GetId().GetType() == userpb.UserType_USER_TYPE_FEDERATED {
providerInfoResp, err := gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: user.GetId().GetIdp(),
})
if err = errorcode.FromCS3Status(providerInfoResp.GetStatus(), err); err != nil {
s.logger.Error().Err(err).Msg("getting provider info failed")
return libregraph.Permission{}, err
}
createShareRequest := createShareRequestToFederatedUser(user, statResponse.GetInfo().GetId(), providerInfoResp.ProviderInfo, cs3ResourcePermissions)
if invite.ExpirationDateTime != nil {
createShareRequest.Expiration = utils.TimeToTS(*invite.ExpirationDateTime)
}
createShareResponse, err := gatewayClient.CreateOCMShare(ctx, createShareRequest)
if err = errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {
s.logger.Error().Err(err).Msg("share creation failed")
return libregraph.Permission{}, err
}
shareid = createShareResponse.GetShare().GetId().GetOpaqueId()
cTime = createShareResponse.GetShare().GetCtime()
expiration = createShareResponse.GetShare().GetExpiration()
} else {
createShareRequest := createShareRequestToUser(user, statResponse.GetInfo(), cs3ResourcePermissions)
if invite.ExpirationDateTime != nil {
createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime)
}
createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest)
if err = errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {
s.logger.Error().Err(err).Msg("share creation failed")
return libregraph.Permission{}, err
}
shareid = createShareResponse.GetShare().GetId().GetOpaqueId()
cTime = createShareResponse.GetShare().GetCtime()
expiration = createShareResponse.GetShare().GetExpiration()
}
}
if shareid != "" {
permission.Id = conversions.ToPointer(shareid)
}
if expiration != nil {
permission.SetExpirationDateTime(utils.TSToTime(expiration))
}
// set cTime
if cTime != nil {
permission.SetCreatedDateTime(cs3TimestampToTime(cTime))
}
if user, ok := revactx.ContextGetUser(ctx); ok {
userIdentity, err := userIdToIdentity(ctx, s.identityCache, tenantId, user.GetId().GetOpaqueId())
if err != nil {
s.logger.Error().Err(err).Msg("identity lookup failed")
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, err.Error())
}
permission.SetInvitation(libregraph.SharingInvitation{
InvitedBy: &libregraph.IdentitySet{
User: &userIdentity,
},
})
}
return *permission, nil
}
func createShareRequestToGroup(group libregraph.Group, info *storageprovider.ResourceInfo, cs3ResourcePermissions *storageprovider.ResourcePermissions) *collaboration.CreateShareRequest {
return &collaboration.CreateShareRequest{
ResourceInfo: info,
Grant: &collaboration.ShareGrant{
Grantee: &storageprovider.Grantee{
Type: storageprovider.GranteeType_GRANTEE_TYPE_GROUP,
Id: &storageprovider.Grantee_GroupId{GroupId: &grouppb.GroupId{
OpaqueId: group.GetId(),
}},
},
Permissions: &collaboration.SharePermissions{
Permissions: cs3ResourcePermissions,
},
},
}
}
func createShareRequestToUser(user *userpb.User, info *storageprovider.ResourceInfo, cs3ResourcePermissions *storageprovider.ResourcePermissions) *collaboration.CreateShareRequest {
return &collaboration.CreateShareRequest{
ResourceInfo: info,
Grant: &collaboration.ShareGrant{
Grantee: &storageprovider.Grantee{
Type: storageprovider.GranteeType_GRANTEE_TYPE_USER,
Id: &storageprovider.Grantee_UserId{
UserId: user.GetId(),
},
},
Permissions: &collaboration.SharePermissions{
Permissions: cs3ResourcePermissions,
},
},
}
}
func createShareRequestToFederatedUser(user *userpb.User, resourceId *storageprovider.ResourceId, providerInfo *ocmprovider.ProviderInfo, cs3ResourcePermissions *storageprovider.ResourcePermissions) *ocm.CreateOCMShareRequest {
return &ocm.CreateOCMShareRequest{
ResourceId: resourceId,
Grantee: &storageprovider.Grantee{
Type: storageprovider.GranteeType_GRANTEE_TYPE_USER,
Id: &storageprovider.Grantee_UserId{
UserId: user.GetId(),
},
},
RecipientMeshProvider: providerInfo,
AccessMethods: []*ocm.AccessMethod{
{
Term: &ocm.AccessMethod_WebdavOptions{
WebdavOptions: &ocm.WebDAVAccessMethod{
Permissions: cs3ResourcePermissions,
},
},
},
},
}
}
// SpaceRootInvite handles invitation request on project spaces
func (s DriveItemPermissionsService) SpaceRootInvite(ctx context.Context, driveID *storageprovider.ResourceId, invite libregraph.DriveItemInvite) (libregraph.Permission, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return libregraph.Permission{}, err
}
space, err := utils.GetSpace(ctx, storagespace.FormatResourceID(driveID), gatewayClient)
if err != nil {
return libregraph.Permission{}, errorcode.FromUtilsStatusCodeError(err)
}
if space.SpaceType != _spaceTypeProject {
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "unsupported space type")
}
rootResourceID := space.GetRoot()
return s.Invite(ctx, rootResourceID, invite)
}
// ListPermissions lists the permissions of a driveItem
func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID *storageprovider.ResourceId, queryOptions ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
collectionOfPermissions := libregraph.CollectionOfPermissionsWithAllowedValues{}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return collectionOfPermissions, err
}
statResponse, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: itemID}})
if err := errorcode.FromStat(statResponse, err); err != nil {
s.logger.Warn().Err(err).Interface("stat.res", statResponse).Msg("stat failed")
return collectionOfPermissions, err
}
var condition string
if condition, err = roleConditionForResourceType(statResponse.GetInfo()); err != nil {
return collectionOfPermissions, err
}
permissionSet := statResponse.GetInfo().GetPermissionSet()
allowedActions := unifiedrole.CS3ResourcePermissionsToLibregraphActions(permissionSet)
collectionOfPermissions = libregraph.CollectionOfPermissionsWithAllowedValues{}
if len(queryOptions.SelectedAttrs) == 0 || slices.Contains(queryOptions.SelectedAttrs, "@libre.graph.permissions.actions.allowedValues") {
collectionOfPermissions.LibreGraphPermissionsActionsAllowedValues = allowedActions
}
if len(queryOptions.SelectedAttrs) == 0 || slices.Contains(queryOptions.SelectedAttrs, "@libre.graph.permissions.roles.allowedValues") {
collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues = conversions.ToValueSlice(
unifiedrole.GetRolesByPermissions(
unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(s.config.UnifiedRoles.AvailableRoles...)),
allowedActions,
condition,
queryOptions.FilterFederatedRoles,
false,
),
)
}
for i, definition := range collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues {
// the openapi spec defines that the rolePermissions should not be part of the response
definition.RolePermissions = nil
collectionOfPermissions.LibreGraphPermissionsRolesAllowedValues[i] = definition
}
if len(queryOptions.SelectedAttrs) > 0 {
// no need to fetch shares, we are only interested allowedActions and/or allowedRoles
return collectionOfPermissions, nil
}
driveItems := make(driveItemsByResourceID, 1)
// we can use the statResponse to build the drive item before fetching the shares
item, err := cs3ResourceToDriveItem(s.logger, s.publicBaseURL, statResponse.GetInfo())
if err != nil {
return collectionOfPermissions, err
}
driveItems[storagespace.FormatResourceID(statResponse.GetInfo().GetId())] = *item
var permissionsCount int
if IsSpaceRoot(statResponse.GetInfo().GetId()) {
driveItems, err = s.listSpaceRootUserShares(ctx, []*collaboration.Filter{
share.ResourceIDFilter(itemID),
}, driveItems)
if err != nil {
return collectionOfPermissions, err
}
} else {
// "normal" driveItem, populate user permissions via share providers
driveItems, err = s.listUserShares(ctx, []*collaboration.Filter{
share.ResourceIDFilter(itemID),
}, driveItems)
if err != nil {
return collectionOfPermissions, err
}
if s.config.IncludeOCMSharees {
driveItems, err = s.listOCMShares(ctx, []*ocm.ListOCMSharesRequest_Filter{
{
Type: ocm.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID,
Term: &ocm.ListOCMSharesRequest_Filter_ResourceId{ResourceId: itemID},
},
}, driveItems)
if err != nil {
return collectionOfPermissions, err
}
}
}
if !queryOptions.NoLinkPermissions {
// finally get public shares, which are possible for spaceroots and "normal" resources
driveItems, err = s.listPublicShares(ctx, []*link.ListPublicSharesRequest_Filter{
publicshare.ResourceIDFilter(itemID),
}, driveItems)
if err != nil {
return collectionOfPermissions, err
}
}
for _, driveItem := range driveItems {
permissionsCount += len(driveItem.Permissions)
if !queryOptions.NoValues {
collectionOfPermissions.Value = append(collectionOfPermissions.Value, driveItem.Permissions...)
}
}
if queryOptions.Count {
collectionOfPermissions.SetOdataCount(int32(permissionsCount))
}
return collectionOfPermissions, nil
}
// ListSpaceRootPermissions handles ListPermissions request on project spaces
func (s DriveItemPermissionsService) ListSpaceRootPermissions(ctx context.Context, driveID *storageprovider.ResourceId, queryOptions ListPermissionsQueryOptions) (libregraph.CollectionOfPermissionsWithAllowedValues, error) {
collectionOfPermissions := libregraph.CollectionOfPermissionsWithAllowedValues{}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return collectionOfPermissions, err
}
space, err := utils.GetSpace(ctx, storagespace.FormatResourceID(driveID), gatewayClient)
if err != nil {
return collectionOfPermissions, errorcode.FromUtilsStatusCodeError(err)
}
isSupportedSpaceType := slices.Contains([]string{_spaceTypeProject, _spaceTypePersonal, _spaceTypeVirtual}, space.GetSpaceType())
if !isSupportedSpaceType {
return collectionOfPermissions, errorcode.New(errorcode.InvalidRequest, "unsupported space type")
}
rootResourceID := space.GetRoot()
return s.ListPermissions(ctx, rootResourceID, queryOptions) // federated roles are not supported for spaces
}
// DeletePermission deletes a permission from a drive item
func (s DriveItemPermissionsService) DeletePermission(ctx context.Context, itemID *storageprovider.ResourceId, permissionID string) error {
var permissionType permissionType
sharedResourceID, err := s.getLinkPermissionResourceID(ctx, permissionID)
switch {
// Check if the ID is referring to a public share
case err == nil:
permissionType = Public
// If the item id is referring to a space root and this is not a public share
// we have to deal with space permissions
case IsSpaceRoot(itemID):
permissionType = Space
sharedResourceID = itemID
err = nil
// If this is neither a public share nor a space permission, check if this is a
// user share
default:
sharedResourceID, err = s.getUserPermissionResourceID(ctx, permissionID)
if err == nil {
permissionType = User
}
}
if sharedResourceID == nil && s.config.IncludeOCMSharees {
sharedResourceID, err = s.getOCMPermissionResourceID(ctx, permissionID)
if err == nil {
permissionType = OCM
}
}
switch {
case err != nil:
return err
case permissionType == Unknown:
return errorcode.New(errorcode.ItemNotFound, "permission not found")
case sharedResourceID == nil:
return errorcode.New(errorcode.ItemNotFound, "failed to resolve resource id for shared resource")
}
// The resourceID of the shared resource need to match the item ID from the Request Path
// otherwise this is an invalid Request.
if !utils.ResourceIDEqual(sharedResourceID, itemID) {
s.logger.Debug().Msg("resourceID of shared does not match itemID")
return errorcode.New(errorcode.InvalidRequest, "permissionID and itemID do not match")
}
switch permissionType {
case User, Space:
return s.removeUserShare(ctx, permissionID)
case Public:
return s.removePublicShare(ctx, permissionID)
case OCM:
return s.removeOCMPermission(ctx, permissionID)
default:
// This should never be reached
return errorcode.New(errorcode.GeneralException, "failed to delete permission")
}
}
// DeleteSpaceRootPermission deletes a permission on the root item of a project space
func (s DriveItemPermissionsService) DeleteSpaceRootPermission(ctx context.Context, driveID *storageprovider.ResourceId, permissionID string) error {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return err
}
space, err := utils.GetSpace(ctx, storagespace.FormatResourceID(driveID), gatewayClient)
if err != nil {
return errorcode.FromUtilsStatusCodeError(err)
}
if space.SpaceType != _spaceTypeProject {
return errorcode.New(errorcode.InvalidRequest, "unsupported space type")
}
rootResourceID := space.GetRoot()
return s.DeletePermission(ctx, rootResourceID, permissionID)
}
// UpdatePermission updates a permission on a drive item
func (s DriveItemPermissionsService) UpdatePermission(ctx context.Context, itemID *storageprovider.ResourceId, permissionID string, newPermission libregraph.Permission) (libregraph.Permission, error) {
oldPermission, sharedResourceID, err := s.getPermissionByID(ctx, permissionID, itemID)
// try to get the permission from ocm if the permission was not found first place
if err != nil && s.config.IncludeOCMSharees {
oldPermission, sharedResourceID, err = s.getOCMPermissionByID(ctx, permissionID, itemID)
}
// if we still can't find the permission, return an error
if err != nil {
return libregraph.Permission{}, err
}
// The resourceID of the shared resource need to match the item ID from the Request Path
// otherwise this is an invalid Request.
if !utils.ResourceIDEqual(sharedResourceID, itemID) {
s.logger.Debug().Msg("resourceID of shared does not match itemID")
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "permissionID and itemID do not match")
}
// This is a public link
if _, ok := oldPermission.GetLinkOk(); ok {
updatedPermission, err := s.updatePublicLinkPermission(ctx, permissionID, itemID, &newPermission)
if err != nil {
return libregraph.Permission{}, err
}
return *updatedPermission, nil
}
// This is a user share
updatedPermission, err := s.updateUserShare(ctx, permissionID, sharedResourceID, &newPermission)
if err == nil && updatedPermission != nil {
return *updatedPermission, nil
}
// This is an ocm share
if s.config.IncludeOCMSharees {
updatePermission, err := s.updateOCMPermission(ctx, permissionID, itemID, &newPermission)
if err == nil {
return *updatePermission, nil
}
}
return libregraph.Permission{}, err
}
// UpdateSpaceRootPermission updates a permission on the root item of a project space
func (s DriveItemPermissionsService) UpdateSpaceRootPermission(ctx context.Context, driveID *storageprovider.ResourceId, permissionID string, newPermission libregraph.Permission) (libregraph.Permission, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return libregraph.Permission{}, err
}
space, err := utils.GetSpace(ctx, storagespace.FormatResourceID(driveID), gatewayClient)
if err != nil {
return libregraph.Permission{}, errorcode.FromUtilsStatusCodeError(err)
}
if space.SpaceType != _spaceTypeProject {
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "unsupported space type")
}
rootResourceID := space.GetRoot()
return s.UpdatePermission(ctx, rootResourceID, permissionID, newPermission)
}
// DriveItemPermissionsApi is the api that registers the http endpoints which expose needed operation to the graph api.
// the business logic is delegated to the permissions service and further down to the cs3 client.
type DriveItemPermissionsApi struct {
logger log.Logger
driveItemPermissionsService DriveItemPermissionsProvider
config *config.Config
}
// NewDriveItemPermissionsApi creates a new DriveItemPermissionsApi
func NewDriveItemPermissionsApi(driveItemPermissionService DriveItemPermissionsProvider, logger log.Logger, c *config.Config) (DriveItemPermissionsApi, error) {
return DriveItemPermissionsApi{
logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemApi").Logger()},
driveItemPermissionsService: driveItemPermissionService,
config: c,
}, nil
}
// Invite handles DriveItemInvite requests
func (api DriveItemPermissionsApi) Invite(w http.ResponseWriter, r *http.Request) {
_, itemID, err := GetDriveAndItemIDParam(r, &api.logger)
if err != nil {
api.logger.Debug().Err(err).Msg(invalidIdMsg)
errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, invalidIdMsg)
return
}
driveItemInvite := &libregraph.DriveItemInvite{}
if err = StrictJSONUnmarshal(r.Body, driveItemInvite); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("failed unmarshalling request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid request body")
return
}
ctx := validate.ContextWithAllowedRoleIDs(r.Context(), api.config.UnifiedRoles.AvailableRoles)
if err = validate.StructCtx(ctx, driveItemInvite); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
permission, err := api.driveItemPermissionsService.Invite(ctx, itemID, *driveItemInvite)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: []any{permission}})
}
// SpaceRootInvite handles DriveItemInvite requests on a space root
func (api DriveItemPermissionsApi) SpaceRootInvite(w http.ResponseWriter, r *http.Request) {
driveID, err := parseIDParam(r, "driveID")
if err != nil {
api.logger.Debug().Err(err).Msg(parseDriveIDErrMsg)
errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, parseDriveIDErrMsg)
return
}
driveItemInvite := &libregraph.DriveItemInvite{}
if err = StrictJSONUnmarshal(r.Body, driveItemInvite); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("failed unmarshalling request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid request body")
return
}
ctx := validate.ContextWithAllowedRoleIDs(r.Context(), api.config.UnifiedRoles.AvailableRoles)
if err = validate.StructCtx(ctx, driveItemInvite); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
permission, err := api.driveItemPermissionsService.SpaceRootInvite(ctx, &driveID, *driveItemInvite)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: []any{permission}})
}
// ListPermissions handles ListPermissions requests
func (api DriveItemPermissionsApi) ListPermissions(w http.ResponseWriter, r *http.Request) {
_, itemID, err := GetDriveAndItemIDParam(r, &api.logger)
if err != nil {
api.logger.Debug().Err(err).Msg(invalidIdMsg)
errorcode.RenderError(w, r, err)
return
}
sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
if err != nil {
api.logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("Error parsing ListPermissionRequest: query error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
var queryOptions ListPermissionsQueryOptions
queryOptions, err = api.getListPermissionsQueryOptions(odataReq)
if err != nil {
api.logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("Error parsing ListPermissionRequest query options")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
ctx := r.Context()
permissions, err := api.driveItemPermissionsService.ListPermissions(ctx, itemID, queryOptions)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
loc := r.Header.Get(l10n.HeaderAcceptLanguage)
w.Header().Add("Content-Language", loc)
if loc != "" && loc != "en" {
err := l10n_pkg.TranslateEntity(loc, "en", permissions,
l10n.TranslateEach("LibreGraphPermissionsRolesAllowedValues",
l10n.TranslateField("Description"),
l10n.TranslateField("DisplayName"),
),
)
if err != nil {
api.logger.Error().Err(err).Msg("tranlation error")
}
}
render.Status(r, http.StatusOK)
render.JSON(w, r, permissions)
}
// ListSpaceRootPermissions handles ListPermissions requests on a space root
func (api DriveItemPermissionsApi) ListSpaceRootPermissions(w http.ResponseWriter, r *http.Request) {
driveID, err := parseIDParam(r, "driveID")
if err != nil {
api.logger.Debug().Err(err).Msg(parseDriveIDErrMsg)
errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, parseDriveIDErrMsg)
return
}
sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
if err != nil {
api.logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("Error parsing ListPermissionRequest: query error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
var queryOptions ListPermissionsQueryOptions
queryOptions, err = api.getListPermissionsQueryOptions(odataReq)
if err != nil {
api.logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("Error parsing ListPermissionRequest query options")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
ctx := r.Context()
permissions, err := api.driveItemPermissionsService.ListSpaceRootPermissions(ctx, &driveID, queryOptions)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
loc := r.Header.Get(l10n.HeaderAcceptLanguage)
w.Header().Add("Content-Language", loc)
if loc != "" && loc != "en" {
err := l10n_pkg.TranslateEntity(loc, "en", permissions,
l10n.TranslateEach("LibreGraphPermissionsRolesAllowedValues",
l10n.TranslateField("Description"),
l10n.TranslateField("DisplayName"),
),
)
if err != nil {
api.logger.Error().Err(err).Msg("tranlation error")
}
}
render.Status(r, http.StatusOK)
render.JSON(w, r, permissions)
}
func (api DriveItemPermissionsApi) getListPermissionsQueryOptions(odataReq *godata.GoDataRequest) (ListPermissionsQueryOptions, error) {
queryOptions := ListPermissionsQueryOptions{}
if odataReq.Query.Filter != nil {
switch odataReq.Query.Filter.RawValue {
case federatedRolesODataFilter:
queryOptions.FilterFederatedRoles = true
case noLinksODataFilter:
queryOptions.NoLinkPermissions = true
default:
return ListPermissionsQueryOptions{}, errorcode.New(errorcode.InvalidRequest, "invalid filter value")
}
}
selectAttrs, err := odata.GetSelectValues(odataReq.Query)
if err != nil {
return ListPermissionsQueryOptions{}, err
}
queryOptions.SelectedAttrs = selectAttrs
if odataReq.Query.Count != nil {
queryOptions.Count = bool(*odataReq.Query.Count)
}
if odataReq.Query.Top != nil {
top := int(*odataReq.Query.Top)
switch {
case top != 0:
return ListPermissionsQueryOptions{}, err
case top == 0 && !queryOptions.Count:
return ListPermissionsQueryOptions{}, err
default:
queryOptions.NoValues = true
}
}
return queryOptions, nil
}
// DeletePermission handles DeletePermission requests
func (api DriveItemPermissionsApi) DeletePermission(w http.ResponseWriter, r *http.Request) {
_, itemID, err := GetDriveAndItemIDParam(r, &api.logger)
if err != nil {
api.logger.Debug().Err(err).Msg(invalidIdMsg)
errorcode.RenderError(w, r, err)
return
}
permissionID, err := url.PathUnescape(chi.URLParam(r, "permissionID"))
if err != nil {
api.logger.Debug().Err(err).Msg("could not parse permissionID")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid permissionID")
return
}
ctx := r.Context()
err = api.driveItemPermissionsService.DeletePermission(ctx, itemID, permissionID)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// DeleteSpaceRootPermission handles DeletePermission requests on a space root
func (api DriveItemPermissionsApi) DeleteSpaceRootPermission(w http.ResponseWriter, r *http.Request) {
driveID, err := parseIDParam(r, "driveID")
if err != nil {
api.logger.Debug().Err(err).Msg(parseDriveIDErrMsg)
errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, parseDriveIDErrMsg)
return
}
permissionID, err := url.PathUnescape(chi.URLParam(r, "permissionID"))
if err != nil {
api.logger.Debug().Err(err).Msg("could not parse permissionID")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid permissionID")
return
}
ctx := r.Context()
err = api.driveItemPermissionsService.DeleteSpaceRootPermission(ctx, &driveID, permissionID)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// UpdatePermission handles UpdatePermission requests
func (api DriveItemPermissionsApi) UpdatePermission(w http.ResponseWriter, r *http.Request) {
_, itemID, err := GetDriveAndItemIDParam(r, &api.logger)
if err != nil {
api.logger.Debug().Err(err).Msg(invalidIdMsg)
errorcode.RenderError(w, r, err)
return
}
permissionID, err := url.PathUnescape(chi.URLParam(r, "permissionID"))
if err != nil {
api.logger.Debug().Err(err).Msg("could not parse permissionID")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid permissionID")
return
}
permission := libregraph.Permission{}
if err = StrictJSONUnmarshal(r.Body, &permission); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("failed unmarshalling request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid request body")
return
}
ctx := r.Context()
if err = validate.StructCtx(ctx, permission); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
updatedPermission, err := api.driveItemPermissionsService.UpdatePermission(ctx, itemID, permissionID, permission)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &updatedPermission)
}
// UpdateSpaceRootPermission handles UpdatePermission requests on a space root
func (api DriveItemPermissionsApi) UpdateSpaceRootPermission(w http.ResponseWriter, r *http.Request) {
driveID, err := parseIDParam(r, "driveID")
if err != nil {
api.logger.Debug().Err(err).Msg(parseDriveIDErrMsg)
errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, parseDriveIDErrMsg)
return
}
permissionID, err := url.PathUnescape(chi.URLParam(r, "permissionID"))
if err != nil {
api.logger.Debug().Err(err).Msg("could not parse permissionID")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid permissionID")
return
}
permission := libregraph.Permission{}
if err = StrictJSONUnmarshal(r.Body, &permission); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("failed unmarshalling request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid request body")
return
}
ctx := r.Context()
if err = validate.StructCtx(ctx, permission); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
updatedPermission, err := api.driveItemPermissionsService.UpdateSpaceRootPermission(ctx, &driveID, permissionID, permission)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &updatedPermission)
}
@@ -0,0 +1,426 @@
package svc
import (
"context"
"net/http"
"net/url"
"strconv"
"time"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/qsfera/server/services/graph/pkg/errorcode"
"github.com/qsfera/server/services/graph/pkg/linktype"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
func (s DriveItemPermissionsService) CreateLink(ctx context.Context, driveItemID *storageprovider.ResourceId, createLink libregraph.DriveItemCreateLink) (libregraph.Permission, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
s.logger.Error().Err(err).Msg("could not select next gateway client")
return libregraph.Permission{}, errorcode.New(errorcode.GeneralException, err.Error())
}
statResp, err := gatewayClient.Stat(
ctx,
&storageprovider.StatRequest{
Ref: &storageprovider.Reference{
ResourceId: driveItemID,
Path: ".",
},
})
if err != nil {
s.logger.Error().Err(err).Msg("transport error, could not stat resource")
return libregraph.Permission{}, errorcode.New(errorcode.GeneralException, err.Error())
}
if code := statResp.GetStatus().GetCode(); code != rpc.Code_CODE_OK {
s.logger.Debug().Interface("itemID", driveItemID).Msg(statResp.GetStatus().GetMessage())
return libregraph.Permission{}, errorcode.New(cs3StatusToErrCode(code), statResp.GetStatus().GetMessage())
}
permissions, err := linktype.CS3ResourcePermissionsFromSharingLink(createLink, statResp.GetInfo().GetType())
if err != nil {
s.logger.Debug().Interface("createLink", createLink).Msg(err.Error())
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "invalid link type")
}
if createLink.GetType() == libregraph.INTERNAL && len(createLink.GetPassword()) > 0 {
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "password is redundant for the internal link")
}
req := link.CreatePublicShareRequest{
ResourceInfo: statResp.GetInfo(),
Grant: &link.Grant{
Permissions: &link.PublicSharePermissions{
Permissions: permissions,
},
Password: createLink.GetPassword(),
},
}
expirationDate, isSet := createLink.GetExpirationDateTimeOk()
if isSet {
expireTime := parseAndFillUpTime(expirationDate)
if expireTime == nil {
s.logger.Debug().Interface("createLink", createLink).Send()
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "invalid expiration date")
}
req.GetGrant().Expiration = expireTime
}
// set displayname and password protected as arbitrary metadata
req.ResourceInfo.ArbitraryMetadata = &storageprovider.ArbitraryMetadata{
Metadata: map[string]string{
"name": createLink.GetDisplayName(),
"quicklink": strconv.FormatBool(createLink.GetLibreGraphQuickLink()),
},
}
createResp, err := gatewayClient.CreatePublicShare(ctx, &req)
if err != nil {
s.logger.Error().Err(err).Msg("transport error, could not create link")
return libregraph.Permission{}, errorcode.New(errorcode.GeneralException, err.Error())
}
if statusCode := createResp.GetStatus().GetCode(); statusCode != rpc.Code_CODE_OK {
return libregraph.Permission{}, errorcode.New(cs3StatusToErrCode(statusCode), createResp.Status.Message)
}
link := createResp.GetShare()
perm, err := s.libreGraphPermissionFromCS3PublicShare(link)
if err != nil {
return libregraph.Permission{}, errorcode.New(errorcode.GeneralException, err.Error())
}
return *perm, nil
}
func (s DriveItemPermissionsService) CreateSpaceRootLink(ctx context.Context, driveID *storageprovider.ResourceId, createLink libregraph.DriveItemCreateLink) (libregraph.Permission, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return libregraph.Permission{}, err
}
space, err := utils.GetSpace(ctx, storagespace.FormatResourceID(driveID), gatewayClient)
if err != nil {
return libregraph.Permission{}, errorcode.FromUtilsStatusCodeError(err)
}
if space.SpaceType != _spaceTypeProject {
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "unsupported space type")
}
rootResourceID := space.GetRoot()
return s.CreateLink(ctx, rootResourceID, createLink)
}
func (s DriveItemPermissionsService) SetPublicLinkPassword(ctx context.Context, driveItemId *storageprovider.ResourceId, permissionID string, password string) (libregraph.Permission, error) {
publicShare, err := s.getCS3PublicShareByID(ctx, permissionID)
if err != nil {
return libregraph.Permission{}, err
}
// The resourceID of the shared resource need to match the item ID from the Request Path
// otherwise this is an invalid Request.
if !utils.ResourceIDEqual(publicShare.GetResourceId(), driveItemId) {
s.logger.Debug().Msg("resourceID of shared does not match itemID")
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "permissionID and itemID do not match")
}
permission, err := s.updatePublicLinkPassword(ctx, permissionID, password)
if err != nil {
return libregraph.Permission{}, err
}
return *permission, nil
}
func (s DriveItemPermissionsService) SetPublicLinkPasswordOnSpaceRoot(ctx context.Context, driveID *storageprovider.ResourceId, permissionID string, password string) (libregraph.Permission, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return libregraph.Permission{}, err
}
space, err := utils.GetSpace(ctx, storagespace.FormatResourceID(driveID), gatewayClient)
if err != nil {
return libregraph.Permission{}, errorcode.FromUtilsStatusCodeError(err)
}
if space.SpaceType != _spaceTypeProject {
return libregraph.Permission{}, errorcode.New(errorcode.InvalidRequest, "unsupported space type")
}
rootResourceID := space.GetRoot()
return s.SetPublicLinkPassword(ctx, rootResourceID, permissionID, password)
}
// CreateLink creates a public link on the cs3 api
func (api DriveItemPermissionsApi) CreateLink(w http.ResponseWriter, r *http.Request) {
logger := api.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling create link")
_, driveItemID, err := GetDriveAndItemIDParam(r, &logger)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
var createLink libregraph.DriveItemCreateLink
if err = StrictJSONUnmarshal(r.Body, &createLink); err != nil {
logger.Error().Err(err).Interface("body", r.Body).Msg("could not create link: invalid body schema definition")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid body schema definition")
return
}
perm, err := api.driveItemPermissionsService.CreateLink(r.Context(), driveItemID, createLink)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, perm)
}
func (api DriveItemPermissionsApi) CreateSpaceRootLink(w http.ResponseWriter, r *http.Request) {
logger := api.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling create link")
driveID, err := parseIDParam(r, "driveID")
if err != nil {
msg := "could not parse driveID"
api.logger.Debug().Err(err).Msg(msg)
errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, msg)
return
}
var createLink libregraph.DriveItemCreateLink
if err = StrictJSONUnmarshal(r.Body, &createLink); err != nil {
logger.Error().Err(err).Interface("body", r.Body).Msg("could not create link: invalid body schema definition")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid body schema definition")
return
}
perm, err := api.driveItemPermissionsService.CreateSpaceRootLink(r.Context(), &driveID, createLink)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, perm)
}
// SetLinkPassword sets public link password on the cs3 api
func (api DriveItemPermissionsApi) SetLinkPassword(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_, itemID, err := GetDriveAndItemIDParam(r, &api.logger)
if err != nil {
errorcode.RenderError(w, r, err)
return
}
permissionID, err := url.PathUnescape(chi.URLParam(r, "permissionID"))
if err != nil {
api.logger.Debug().Err(err).Msg("could not parse permissionID")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid permissionID")
return
}
password := &libregraph.SharingLinkPassword{}
if err = StrictJSONUnmarshal(r.Body, password); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("failed unmarshalling request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid request body")
return
}
newPermission, err := api.driveItemPermissionsService.SetPublicLinkPassword(ctx, itemID, permissionID, password.GetPassword())
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, newPermission)
}
func (api DriveItemPermissionsApi) SetSpaceRootLinkPassword(w http.ResponseWriter, r *http.Request) {
driveID, err := parseIDParam(r, "driveID")
if err != nil {
msg := "could not parse driveID"
api.logger.Debug().Err(err).Msg(msg)
errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, msg)
return
}
permissionID, err := url.PathUnescape(chi.URLParam(r, "permissionID"))
if err != nil {
api.logger.Debug().Err(err).Msg("could not parse permissionID")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid permissionID")
return
}
password := &libregraph.SharingLinkPassword{}
if err = StrictJSONUnmarshal(r.Body, password); err != nil {
api.logger.Debug().Err(err).Interface("Body", r.Body).Msg("failed unmarshalling request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid request body")
return
}
ctx := r.Context()
newPermission, err := api.driveItemPermissionsService.SetPublicLinkPasswordOnSpaceRoot(ctx, &driveID, permissionID, password.GetPassword())
if err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, newPermission)
}
func (s DriveItemPermissionsService) updatePublicLinkPermission(ctx context.Context, permissionID string, itemID *storageprovider.ResourceId, newPermission *libregraph.Permission) (perm *libregraph.Permission, err error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
s.logger.Error().Err(err).Msg("could not select next gateway client")
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
statResp, err := gatewayClient.Stat(
ctx,
&storageprovider.StatRequest{
Ref: &storageprovider.Reference{
ResourceId: itemID,
Path: ".",
},
})
if err := errorcode.FromCS3Status(statResp.GetStatus(), err); err != nil {
return nil, err
}
if newPermission.HasExpirationDateTime() {
expirationDate := newPermission.GetExpirationDateTime()
update := &link.UpdatePublicShareRequest_Update{
Type: link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION,
Grant: &link.Grant{Expiration: parseAndFillUpTime(&expirationDate)},
}
perm, err = s.updatePublicLink(ctx, permissionID, update)
if err != nil {
return nil, err
}
}
if newPermission.HasLink() && newPermission.Link.HasLibreGraphDisplayName() {
changedLink := newPermission.GetLink()
update := &link.UpdatePublicShareRequest_Update{
Type: link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME,
DisplayName: changedLink.GetLibreGraphDisplayName(),
}
perm, err = s.updatePublicLink(ctx, permissionID, update)
if err != nil {
return nil, err
}
}
if newPermission.HasLink() && newPermission.Link.HasType() {
changedLink := newPermission.Link.GetType()
permissions, err := linktype.CS3ResourcePermissionsFromSharingLink(
libregraph.DriveItemCreateLink{
Type: &changedLink,
},
statResp.GetInfo().GetType(),
)
if err != nil {
return nil, err
}
update := &link.UpdatePublicShareRequest_Update{
Type: link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS,
Grant: &link.Grant{
Permissions: &link.PublicSharePermissions{Permissions: permissions},
},
}
perm, err = s.updatePublicLink(ctx, permissionID, update)
if err != nil {
return nil, err
}
// reset the password for the internal link
if changedLink == libregraph.INTERNAL {
perm, err = s.updatePublicLinkPassword(ctx, permissionID, "")
if err != nil {
return nil, err
}
}
}
return perm, err
}
func (s DriveItemPermissionsService) updatePublicLinkPassword(ctx context.Context, permissionID string, password string) (*libregraph.Permission, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
changeLinkRes, err := gatewayClient.UpdatePublicShare(ctx, &link.UpdatePublicShareRequest{
Update: &link.UpdatePublicShareRequest_Update{
Type: link.UpdatePublicShareRequest_Update_TYPE_PASSWORD,
Grant: &link.Grant{
Password: password,
},
},
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Id{
Id: &link.PublicShareId{
OpaqueId: permissionID,
},
},
},
})
if err := errorcode.FromCS3Status(changeLinkRes.GetStatus(), err); err != nil {
return nil, err
}
permission, err := s.libreGraphPermissionFromCS3PublicShare(changeLinkRes.GetShare())
if err != nil {
return nil, err
}
return permission, nil
}
func (s DriveItemPermissionsService) updatePublicLink(ctx context.Context, permissionID string, update *link.UpdatePublicShareRequest_Update) (*libregraph.Permission, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
changeLinkRes, err := gatewayClient.UpdatePublicShare(ctx, &link.UpdatePublicShareRequest{
Update: update,
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Id{
Id: &link.PublicShareId{
OpaqueId: permissionID,
},
},
},
})
if err := errorcode.FromCS3Status(changeLinkRes.GetStatus(), err); err != nil {
return nil, err
}
permission, err := s.libreGraphPermissionFromCS3PublicShare(changeLinkRes.GetShare())
if err != nil {
return nil, err
}
return permission, nil
}
func parseAndFillUpTime(t *time.Time) *types.Timestamp {
if t == nil || t.IsZero() {
return nil
}
// the link needs to be valid for the whole day
tLink := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
tLink = tLink.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
final := tLink.UnixNano()
return &types.Timestamp{
Seconds: uint64(final / 1000000000),
Nanos: uint32(final % 1000000000),
}
}
@@ -0,0 +1,258 @@
package svc_test
import (
"context"
"errors"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/graph/mocks"
"github.com/qsfera/server/services/graph/pkg/config/defaults"
"github.com/qsfera/server/services/graph/pkg/errorcode"
"github.com/qsfera/server/services/graph/pkg/identity/cache"
"github.com/qsfera/server/services/graph/pkg/linktype"
service "github.com/qsfera/server/services/graph/pkg/service/v0"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/utils"
cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks"
"github.com/stretchr/testify/mock"
)
var _ = Describe("createLinkTests", func() {
var (
svc service.DriveItemPermissionsService
driveItemId *provider.ResourceId
ctx context.Context
gatewayClient *cs3mocks.GatewayAPIClient
gatewaySelector *mocks.Selectable[gateway.GatewayAPIClient]
currentUser = &userpb.User{
Id: &userpb.UserId{
OpaqueId: "user",
},
}
)
const (
ViewerLinkString = "Viewer Link"
)
BeforeEach(func() {
var err error
logger := log.NewLogger()
gatewayClient = cs3mocks.NewGatewayAPIClient(GinkgoT())
gatewaySelector = mocks.NewSelectable[gateway.GatewayAPIClient](GinkgoT())
gatewaySelector.On("Next").Return(gatewayClient, nil)
cache := cache.NewIdentityCache(cache.IdentityCacheWithGatewaySelector(gatewaySelector))
cfg := defaults.FullDefaultConfig()
svc, err = service.NewDriveItemPermissionsService(logger, gatewaySelector, cache, cfg)
Expect(err).ToNot(HaveOccurred())
driveItemId = &provider.ResourceId{
StorageId: "1",
SpaceId: "2",
OpaqueId: "3",
}
ctx = revactx.ContextSetUser(context.Background(), currentUser)
})
Describe("CreateLink", func() {
var (
driveItemCreateLink libregraph.DriveItemCreateLink
statResponse *provider.StatResponse
createLinkResponse *link.CreatePublicShareResponse
)
BeforeEach(func() {
driveItemCreateLink = libregraph.DriveItemCreateLink{
Type: nil,
ExpirationDateTime: nil,
Password: nil,
DisplayName: nil,
LibreGraphQuickLink: nil,
}
statResponse = &provider.StatResponse{
Status: status.NewOK(ctx),
Info: &provider.ResourceInfo{Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER},
}
createLinkResponse = &link.CreatePublicShareResponse{
Status: status.NewOK(ctx),
}
linkType, err := libregraph.NewSharingLinkTypeFromValue("view")
Expect(err).ToNot(HaveOccurred())
driveItemCreateLink.Type = linkType
driveItemCreateLink.ExpirationDateTime = libregraph.PtrTime(time.Now().Add(time.Hour))
permissions, err := linktype.CS3ResourcePermissionsFromSharingLink(driveItemCreateLink, provider.ResourceType_RESOURCE_TYPE_CONTAINER)
Expect(err).ToNot(HaveOccurred())
createLinkResponse.Share = &link.PublicShare{
Id: &link.PublicShareId{OpaqueId: "123"},
Expiration: utils.TimeToTS(*driveItemCreateLink.ExpirationDateTime),
PasswordProtected: false,
DisplayName: ViewerLinkString,
Token: "SomeGOODCoffee",
Permissions: &link.PublicSharePermissions{Permissions: permissions},
}
})
// Public Shares / "links" in graph terms
It("creates a public link as expected (happy path)", func() {
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
gatewayClient.On("CreatePublicShare", mock.Anything, mock.Anything).Return(createLinkResponse, nil)
perm, err := svc.CreateLink(context.Background(), driveItemId, driveItemCreateLink)
Expect(err).ToNot(HaveOccurred())
Expect(perm.GetId()).To(Equal("123"))
Expect(perm.GetExpirationDateTime().Unix()).To(Equal(driveItemCreateLink.ExpirationDateTime.Unix()))
Expect(perm.GetHasPassword()).To(Equal(false))
Expect(perm.GetLink().LibreGraphDisplayName).To(Equal(libregraph.PtrString(ViewerLinkString)))
link := perm.GetLink()
respLinkType := link.GetType()
expected, err := libregraph.NewSharingLinkTypeFromValue("view")
Expect(err).ToNot(HaveOccurred())
Expect(&respLinkType).To(Equal(expected))
})
It("handles a failing CreateLink", func() {
statResponse.Info = &provider.ResourceInfo{Type: provider.ResourceType_RESOURCE_TYPE_FILE}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
gatewayClient.On("CreatePublicShare", mock.Anything, mock.Anything).Return(createLinkResponse, nil)
linkType, err := libregraph.NewSharingLinkTypeFromValue("edit")
Expect(err).ToNot(HaveOccurred())
driveItemCreateLink.Type = linkType
permissions, err := linktype.CS3ResourcePermissionsFromSharingLink(driveItemCreateLink, provider.ResourceType_RESOURCE_TYPE_CONTAINER)
Expect(err).ToNot(HaveOccurred())
createLinkResponse.Status = status.NewInternal(ctx, "transport error")
createLinkResponse.Share = &link.PublicShare{
Id: &link.PublicShareId{OpaqueId: "123"},
Permissions: &link.PublicSharePermissions{Permissions: permissions},
}
perm, err := svc.CreateLink(context.Background(), driveItemId, driveItemCreateLink)
Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, "transport error")))
Expect(perm).To(BeZero())
})
It("fails when the stat returns access denied", func() {
err := errors.New("no permission to stat the file")
statResponse.Status = status.NewPermissionDenied(ctx, err, err.Error())
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
perm, err := svc.CreateLink(context.Background(), driveItemId, driveItemCreateLink)
Expect(err).To(MatchError(errorcode.New(errorcode.AccessDenied, "no permission to stat the file")))
Expect(perm).To(BeZero())
})
It("fails when the stat returns resource is locked", func() {
err := errors.New("the resource is locked")
statResponse.Status = status.NewLocked(ctx, err.Error())
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
perm, err := svc.CreateLink(context.Background(), driveItemId, driveItemCreateLink)
Expect(err).To(MatchError(errorcode.New(errorcode.ItemIsLocked, "the resource is locked")))
Expect(perm).To(BeZero())
})
It("succeeds when the link type mapping is not successful", func() {
// we need to send a valid link type
linkType, err := libregraph.NewSharingLinkTypeFromValue("edit")
Expect(err).ToNot(HaveOccurred())
driveItemCreateLink.Type = linkType
permissions := &provider.ResourcePermissions{
CreateContainer: true,
InitiateFileUpload: true,
Move: true,
}
// return different permissions which do not match a link type
createLinkResponse.Share = &link.PublicShare{
Id: &link.PublicShareId{OpaqueId: "123"},
Expiration: utils.TimeToTS(*driveItemCreateLink.ExpirationDateTime),
PasswordProtected: false,
DisplayName: ViewerLinkString,
Token: "SomeGOODCoffee",
Permissions: &link.PublicSharePermissions{Permissions: permissions},
}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
gatewayClient.On("CreatePublicShare", mock.Anything, mock.Anything).Return(createLinkResponse, nil)
perm, err := svc.CreateLink(context.Background(), driveItemId, driveItemCreateLink)
Expect(err).ToNot(HaveOccurred())
Expect(perm.GetId()).To(Equal("123"))
Expect(perm.GetExpirationDateTime().Unix()).To(Equal(driveItemCreateLink.ExpirationDateTime.Unix()))
Expect(perm.GetHasPassword()).To(Equal(false))
Expect(perm.GetLink().LibreGraphDisplayName).To(Equal(libregraph.PtrString(ViewerLinkString)))
respLink := perm.GetLink()
// some conversion gymnastics
respLinkType := respLink.GetType()
Expect(err).ToNot(HaveOccurred())
mockLink := libregraph.SharingLink{}
lt, _ := linktype.SharingLinkTypeFromCS3Permissions(&link.PublicSharePermissions{Permissions: permissions})
mockLink.Type = lt
expectedType := mockLink.GetType()
Expect(&respLinkType).To(Equal(&expectedType))
libreGraphActions := perm.LibreGraphPermissionsActions
Expect(libreGraphActions[0]).To(Equal("libre.graph/driveItem/children/create"))
Expect(libreGraphActions[1]).To(Equal("libre.graph/driveItem/upload/create"))
Expect(libreGraphActions[2]).To(Equal("libre.graph/driveItem/path/update"))
})
})
Describe("SetLinPassword", func() {
var (
updatePublicShareMockResponse link.UpdatePublicShareResponse
getPublicShareResponse link.GetPublicShareResponse
)
const TestLinkName = "Test Link"
BeforeEach(func() {
updatePublicShareMockResponse = link.UpdatePublicShareResponse{
Status: status.NewOK(ctx),
Share: &link.PublicShare{DisplayName: TestLinkName},
}
getPublicShareResponse = link.GetPublicShareResponse{
Status: status.NewOK(ctx),
Share: &link.PublicShare{
Id: &link.PublicShareId{
OpaqueId: "permissionid",
},
ResourceId: driveItemId,
Permissions: &link.PublicSharePermissions{
Permissions: linktype.NewViewLinkPermissionSet().GetPermissions(),
},
Token: "token",
},
}
})
It("updates the password on a public share", func() {
gatewayClient.On("GetPublicShare", mock.Anything, mock.Anything).Return(&getPublicShareResponse, nil)
updatePublicShareMockResponse.Share.Permissions = &link.PublicSharePermissions{
Permissions: linktype.NewViewLinkPermissionSet().Permissions,
}
updatePublicShareMockResponse.Share.PasswordProtected = true
gatewayClient.On("UpdatePublicShare",
mock.Anything,
mock.MatchedBy(func(req *link.UpdatePublicShareRequest) bool {
return req.GetRef().GetId().GetOpaqueId() == "permissionid"
}),
).Return(&updatePublicShareMockResponse, nil)
perm, err := svc.SetPublicLinkPassword(context.Background(), driveItemId, "permissionid", "OC123!")
Expect(err).ToNot(HaveOccurred())
linkType := perm.Link.GetType()
Expect(string(linkType)).To(Equal("view"))
Expect(perm.GetHasPassword()).To(BeTrue())
})
})
})
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,575 @@
package svc
import (
"context"
"errors"
"net/http"
"path/filepath"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/graph/pkg/errorcode"
)
const (
_fieldMaskPathState = "state"
_fieldMaskPathMountPoint = "mount_point"
_fieldMaskPathHidden = "hidden"
)
var (
// ErrNoUpdates is returned when no updates are provided
ErrNoUpdates = errors.New("no updates")
// ErrNoUpdater is returned when no updater is provided
ErrNoUpdater = errors.New("no updater")
// ErrAbsoluteNamePath is returned when the name is an absolute path
ErrAbsoluteNamePath = errors.New("name cannot be an absolute path")
// ErrCode errors
// ErrNotAShareJail is returned when the driveID does not belong to a share jail
ErrNotAShareJail = errorcode.New(errorcode.InvalidRequest, "id does not belong to a share jail")
// ErrInvalidDriveIDOrItemID is returned when the driveID or itemID is invalid
ErrInvalidDriveIDOrItemID = errorcode.New(errorcode.InvalidRequest, "invalid driveID or itemID")
// ErrInvalidRequestBody is returned when the request body is invalid
ErrInvalidRequestBody = errorcode.New(errorcode.InvalidRequest, "invalid request body")
// ErrUnmountShare is returned when unmounting a share fails
ErrUnmountShare = errorcode.New(errorcode.InvalidRequest, "unmounting share failed")
// ErrMountShare is returned when mounting a share fails
ErrMountShare = errorcode.New(errorcode.InvalidRequest, "mounting share failed")
// ErrUpdateShares is returned when updating shares fails
ErrUpdateShares = errorcode.New(errorcode.InvalidRequest, "failed to update share")
// ErrInvalidID is returned when the id is invalid
ErrInvalidID = errorcode.New(errorcode.InvalidRequest, "invalid id")
// ErrDriveItemConversion is returned when converting to drive items fails
ErrDriveItemConversion = errorcode.New(errorcode.InvalidRequest, "converting to drive items failed")
// ErrNoShares is returned when no shares are found
ErrNoShares = errorcode.New(errorcode.ItemNotFound, "no shares found")
// ErrAlreadyMounted is returned when all shares are already mounted
ErrAlreadyMounted = errorcode.New(errorcode.NameAlreadyExists, "shares already mounted")
// ErrAlreadyUnmounted is returned when all shares are already unmounted
ErrAlreadyUnmounted = errorcode.New(errorcode.NameAlreadyExists, "shares already unmounted")
)
type (
// UpdateShareClosure is a closure that injects required updates into the update request
UpdateShareClosure func(share *collaboration.ReceivedShare, request *collaboration.UpdateReceivedShareRequest)
// DrivesDriveItemProvider is the interface that needs to be implemented by the individual space service
DrivesDriveItemProvider interface {
// MountShare mounts a share
MountShare(ctx context.Context, resourceID *storageprovider.ResourceId, name string) ([]*collaboration.ReceivedShare, error)
// MountOCMShare mounts an OCM share
MountOCMShare(ctx context.Context, resourceID *storageprovider.ResourceId /*, name string*/) ([]*ocm.ReceivedShare, error)
// UnmountShare unmounts a share
UnmountShare(ctx context.Context, shareID *collaboration.ShareId) error
// UpdateShares updates multiple shares
UpdateShares(ctx context.Context, shares []*collaboration.ReceivedShare, updater UpdateShareClosure) ([]*collaboration.ReceivedShare, error)
// GetShare returns the share
GetShare(ctx context.Context, shareID *collaboration.ShareId) (*collaboration.ReceivedShare, error)
// GetSharesForResource returns all shares for a given resourceID
GetSharesForResource(ctx context.Context, resourceID *storageprovider.ResourceId, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error)
}
)
// DrivesDriveItemService contains the production business logic for everything that relates to drives
type DrivesDriveItemService struct {
logger log.Logger
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
}
// NewDrivesDriveItemService creates a new DrivesDriveItemService
func NewDrivesDriveItemService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) (DrivesDriveItemService, error) {
return DrivesDriveItemService{
logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemService").Logger()},
gatewaySelector: gatewaySelector,
}, nil
}
func (s DrivesDriveItemService) GetShare(ctx context.Context, shareID *collaboration.ShareId) (*collaboration.ReceivedShare, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
// Now, find out the resourceID of the shared resource
getReceivedShareResponse, err := gatewayClient.GetReceivedShare(ctx,
&collaboration.GetReceivedShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: shareID,
},
},
},
)
return getReceivedShareResponse.GetShare(), errorcode.FromCS3Status(getReceivedShareResponse.GetStatus(), err)
}
// GetSharesForResource returns all shares for a given resourceID
func (s DrivesDriveItemService) GetSharesForResource(ctx context.Context, resourceID *storageprovider.ResourceId, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) {
// Find all accepted shares for this resource
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
receivedSharesResponse, err := gatewayClient.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{
Filters: append([]*collaboration.Filter{
{
Type: collaboration.Filter_TYPE_RESOURCE_ID,
Term: &collaboration.Filter_ResourceId{
ResourceId: resourceID,
},
},
}, filters...),
})
switch {
case err != nil:
return nil, err
case len(receivedSharesResponse.GetShares()) == 0:
return nil, ErrNoShares
default:
return receivedSharesResponse.GetShares(), errorcode.FromCS3Status(receivedSharesResponse.GetStatus(), err)
}
}
// UpdateShares updates multiple shares;
// it could happen that some shares are updated and some are not,
// this will return a list of updated shares and a list of errors;
// there is no guarantee that all updates are successful
func (s DrivesDriveItemService) UpdateShares(ctx context.Context, shares []*collaboration.ReceivedShare, updater UpdateShareClosure) ([]*collaboration.ReceivedShare, error) {
errs := make([]error, 0, len(shares))
updatedShares := make([]*collaboration.ReceivedShare, 0, len(shares))
for _, share := range shares {
updatedShare, err := s.UpdateShare(
ctx,
share,
updater,
)
if err != nil {
errs = append(errs, err)
continue
}
updatedShares = append(updatedShares, updatedShare)
}
return updatedShares, errors.Join(errs...)
}
// UpdateShare updates a single share
func (s DrivesDriveItemService) UpdateShare(ctx context.Context, share *collaboration.ReceivedShare, updater UpdateShareClosure) (*collaboration.ReceivedShare, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
updateReceivedShareRequest := &collaboration.UpdateReceivedShareRequest{
Share: &collaboration.ReceivedShare{
Share: &collaboration.Share{
Id: share.GetShare().GetId(),
},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}},
}
switch updater {
case nil:
return nil, ErrNoUpdater
default:
updater(share, updateReceivedShareRequest)
}
if len(updateReceivedShareRequest.GetUpdateMask().GetPaths()) == 0 {
return nil, ErrNoUpdates
}
updateReceivedShareResponse, err := gatewayClient.UpdateReceivedShare(ctx, updateReceivedShareRequest)
return updateReceivedShareResponse.GetShare(), errorcode.FromCS3Status(updateReceivedShareResponse.GetStatus(), err)
}
// UnmountShare unmounts a share
func (s DrivesDriveItemService) UnmountShare(ctx context.Context, shareID *collaboration.ShareId) error {
share, err := s.GetShare(ctx, shareID)
if err != nil {
return err
}
shares, err := s.GetSharesForResource(ctx, share.GetShare().GetResourceId(), []*collaboration.Filter{
{
Type: collaboration.Filter_TYPE_STATE,
Term: &collaboration.Filter_State{
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
},
},
{
Type: collaboration.Filter_TYPE_STATE,
Term: &collaboration.Filter_State{
State: collaboration.ShareState_SHARE_STATE_REJECTED,
},
},
})
if err != nil {
return err
}
availableShares := make([]*collaboration.ReceivedShare, 0, 1)
rejectedShares := make([]*collaboration.ReceivedShare, 0, 1)
for _, v := range shares {
switch v.GetState() {
case collaboration.ShareState_SHARE_STATE_ACCEPTED:
availableShares = append(availableShares, v)
case collaboration.ShareState_SHARE_STATE_REJECTED:
rejectedShares = append(rejectedShares, v)
}
}
if len(availableShares) == 0 {
if len(rejectedShares) > 0 {
return ErrAlreadyUnmounted
}
return ErrNoShares
}
_, err = s.UpdateShares(ctx, availableShares, func(_ *collaboration.ReceivedShare, request *collaboration.UpdateReceivedShareRequest) {
request.Share.State = collaboration.ShareState_SHARE_STATE_REJECTED
request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathState)
})
return err
}
// MountShare mounts a share, there is no guarantee that all siblings will be mounted
// in some rare cases it could happen that none of the siblings could be mounted,
// then the error will be returned
func (s DrivesDriveItemService) MountShare(ctx context.Context, resourceID *storageprovider.ResourceId, name string) ([]*collaboration.ReceivedShare, error) {
if filepath.IsAbs(name) {
return nil, ErrAbsoluteNamePath
}
if name != "" {
name = filepath.Clean(name)
}
shares, err := s.GetSharesForResource(ctx, resourceID, nil)
if err != nil {
return nil, err
}
availableShares := make([]*collaboration.ReceivedShare, 0, len(shares))
mountedShares := make([]*collaboration.ReceivedShare, 0, 1)
for _, v := range shares {
switch v.GetState() {
case collaboration.ShareState_SHARE_STATE_ACCEPTED:
mountedShares = append(mountedShares, v)
case collaboration.ShareState_SHARE_STATE_PENDING, collaboration.ShareState_SHARE_STATE_REJECTED:
availableShares = append(availableShares, v)
}
}
if len(availableShares) == 0 {
if len(mountedShares) > 0 {
return nil, ErrAlreadyMounted
}
return nil, ErrNoShares
}
updatedShares, err := s.UpdateShares(ctx, availableShares, func(share *collaboration.ReceivedShare, request *collaboration.UpdateReceivedShareRequest) {
request.Share.State = collaboration.ShareState_SHARE_STATE_ACCEPTED
request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathState)
// only update if mountPoint name is not empty and the path has changed
if name != "" {
mountPoint := share.GetMountPoint()
if mountPoint == nil {
mountPoint = &storageprovider.Reference{}
}
if filepath.Clean(mountPoint.GetPath()) != name {
mountPoint.Path = name
request.Share.MountPoint = mountPoint
request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathMountPoint)
}
}
})
errs, ok := err.(interface{ Unwrap() []error })
if ok && len(errs.Unwrap()) == len(availableShares) {
// none of the received shares could be accepted.
// this is an error, return it.
return nil, err
}
return updatedShares, nil
}
// DrivesDriveItemApi is the api that registers the http endpoints which expose needed operation to the graph api.
// the business logic is delegated to the space service and further down to the cs3 client.
type DrivesDriveItemApi struct {
logger log.Logger
drivesDriveItemService DrivesDriveItemProvider
baseGraphService BaseGraphProvider
}
// NewDrivesDriveItemApi creates a new DrivesDriveItemApi
func NewDrivesDriveItemApi(drivesDriveItemService DrivesDriveItemProvider, baseGraphService BaseGraphProvider, logger log.Logger) (DrivesDriveItemApi, error) {
return DrivesDriveItemApi{
logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemApi").Logger()},
drivesDriveItemService: drivesDriveItemService,
baseGraphService: baseGraphService,
}, nil
}
// DeleteDriveItem deletes a drive item
func (api DrivesDriveItemApi) DeleteDriveItem(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
driveID, itemID, err := GetDriveAndItemIDParam(r, &api.logger)
if err != nil {
api.logger.Debug().Err(err).Msg(ErrInvalidDriveIDOrItemID.Error())
ErrInvalidDriveIDOrItemID.Render(w, r)
return
}
if !IsShareJail(driveID) {
api.logger.Debug().Interface("driveID", driveID).Msg(ErrNotAShareJail.Error())
ErrNotAShareJail.Render(w, r)
return
}
shareID := ExtractShareIdFromResourceId(itemID)
if err := api.drivesDriveItemService.UnmountShare(ctx, shareID); err != nil {
api.logger.Debug().Err(err).Msg(ErrUnmountShare.Error())
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// GetDriveItem get a drive item
func (api DrivesDriveItemApi) GetDriveItem(w http.ResponseWriter, r *http.Request) {
driveID, itemID, err := GetDriveAndItemIDParam(r, &api.logger)
if err != nil {
api.logger.Debug().Err(err).Msg(ErrInvalidDriveIDOrItemID.Error())
ErrInvalidDriveIDOrItemID.Render(w, r)
return
}
if !IsShareJail(driveID) {
api.logger.Debug().Interface("driveID", driveID).Msg(ErrNotAShareJail.Error())
ErrNotAShareJail.Render(w, r)
return
}
shareID := ExtractShareIdFromResourceId(itemID)
share, err := api.drivesDriveItemService.GetShare(r.Context(), shareID)
if err != nil {
api.logger.Debug().Err(err).Msg(ErrNoShares.Error())
ErrNoShares.Render(w, r)
return
}
availableShares, err := api.drivesDriveItemService.GetSharesForResource(r.Context(), share.GetShare().GetResourceId(), nil)
if err != nil {
api.logger.Debug().Err(err).Msg(ErrNoShares.Error())
ErrNoShares.Render(w, r)
return
}
driveItems, err := api.baseGraphService.CS3ReceivedSharesToDriveItems(r.Context(), availableShares)
switch {
case err != nil:
break
case len(driveItems) != 1:
err = ErrDriveItemConversion
}
if err != nil {
api.logger.Debug().Err(err).Msg(ErrDriveItemConversion.Error())
ErrDriveItemConversion.Render(w, r)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, driveItems[0])
}
// UpdateDriveItem updates a drive item, currently only the visibility of the share is updated
func (api DrivesDriveItemApi) UpdateDriveItem(w http.ResponseWriter, r *http.Request) {
driveID, itemID, err := GetDriveAndItemIDParam(r, &api.logger)
if err != nil {
api.logger.Debug().Err(err).Msg(ErrInvalidDriveIDOrItemID.Error())
ErrInvalidDriveIDOrItemID.Render(w, r)
return
}
if !IsShareJail(driveID) {
api.logger.Debug().Interface("driveID", driveID).Msg(ErrNotAShareJail.Error())
ErrNotAShareJail.Render(w, r)
return
}
shareID := ExtractShareIdFromResourceId(itemID)
requestDriveItem := libregraph.DriveItem{}
if err := StrictJSONUnmarshal(r.Body, &requestDriveItem); err != nil {
api.logger.Debug().Err(err).Msg(ErrInvalidRequestBody.Error())
ErrInvalidRequestBody.Render(w, r)
return
}
share, err := api.drivesDriveItemService.GetShare(r.Context(), shareID)
if err != nil {
api.logger.Debug().Err(err).Msg(ErrNoShares.Error())
ErrNoShares.Render(w, r)
return
}
availableShares, err := api.drivesDriveItemService.GetSharesForResource(r.Context(), share.GetShare().GetResourceId(), nil)
if err != nil {
api.logger.Debug().Err(err).Msg(ErrNoShares.Error())
ErrNoShares.Render(w, r)
return
}
updatedShares, err := api.drivesDriveItemService.UpdateShares(
r.Context(),
availableShares,
func(_ *collaboration.ReceivedShare, request *collaboration.UpdateReceivedShareRequest) {
request.GetShare().Hidden = requestDriveItem.GetUIHidden()
request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathHidden)
},
)
if err != nil {
api.logger.Debug().Err(err).Msg(ErrUpdateShares.Error())
ErrUpdateShares.Render(w, r)
return
}
driveItems, err := api.baseGraphService.CS3ReceivedSharesToDriveItems(r.Context(), updatedShares)
switch {
case err != nil:
break
case len(driveItems) != 1:
err = ErrDriveItemConversion
}
if err != nil {
api.logger.Debug().Err(err).Msg(ErrDriveItemConversion.Error())
ErrDriveItemConversion.Render(w, r)
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, driveItems[0])
}
// CreateDriveItem creates a drive item
func (api DrivesDriveItemApi) CreateDriveItem(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
driveID, err := parseIDParam(r, "driveID")
if err != nil {
api.logger.Debug().Err(err).Msg(ErrInvalidDriveIDOrItemID.Error())
ErrInvalidDriveIDOrItemID.Render(w, r)
return
}
if !IsShareJail(&driveID) {
api.logger.Debug().Interface("driveID", driveID).Msg(ErrNotAShareJail.Error())
ErrNotAShareJail.Render(w, r)
return
}
requestDriveItem := libregraph.DriveItem{}
if err := StrictJSONUnmarshal(r.Body, &requestDriveItem); err != nil {
api.logger.Debug().Err(err).Msg(ErrInvalidRequestBody.Error())
ErrInvalidRequestBody.Render(w, r)
return
}
remoteItem := requestDriveItem.GetRemoteItem()
resourceId, err := storagespace.ParseID(remoteItem.GetId())
if err != nil {
api.logger.Debug().Err(err).Msg(ErrInvalidID.Error())
ErrInvalidID.Render(w, r)
return
}
var driveItems []libregraph.DriveItem
switch {
case resourceId.GetStorageId() == utils.OCMStorageProviderID:
var mountedOcmShares []*ocm.ReceivedShare
mountedOcmShares, err = api.drivesDriveItemService.MountOCMShare(ctx, &resourceId /*, requestDriveItem.GetName()*/)
if err != nil {
api.logger.Debug().Err(err).Msg(ErrMountShare.Error())
switch e, ok := errorcode.ToError(err); {
case ok && e.GetOrigin() == errorcode.ErrorOriginCS3 && e.GetCode() == errorcode.ItemNotFound:
ErrDriveItemConversion.Render(w, r)
default:
errorcode.RenderError(w, r, err)
}
return
}
driveItems, err = api.baseGraphService.CS3ReceivedOCMSharesToDriveItems(ctx, mountedOcmShares)
default:
var mountedShares []*collaboration.ReceivedShare
// Get all shares that the user has received for this resource. There might be multiple
mountedShares, err = api.drivesDriveItemService.MountShare(ctx, &resourceId, requestDriveItem.GetName())
if err != nil {
api.logger.Debug().Err(err).Msg(ErrMountShare.Error())
switch e, ok := errorcode.ToError(err); {
case ok && e.GetOrigin() == errorcode.ErrorOriginCS3 && e.GetCode() == errorcode.ItemNotFound:
ErrDriveItemConversion.Render(w, r)
default:
errorcode.RenderError(w, r, err)
}
return
}
driveItems, err = api.baseGraphService.CS3ReceivedSharesToDriveItems(ctx, mountedShares)
}
switch {
case err != nil:
break
case len(driveItems) != 1:
err = ErrDriveItemConversion
}
if err != nil {
api.logger.Debug().Err(err).Msg(ErrDriveItemConversion.Error())
ErrDriveItemConversion.Render(w, r)
return
}
render.Status(r, http.StatusCreated)
render.JSON(w, r, driveItems[0])
}
@@ -0,0 +1,173 @@
package svc
import (
"context"
"errors"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/qsfera/server/services/graph/pkg/errorcode"
)
var (
// ErrUnmountOCMShare is returned when unmounting a share fails
ErrUnmountOCMShare = errorcode.New(errorcode.InvalidRequest, "unmounting ocm share failed")
// ErrMountOCMShare is returned when mounting a share fails
ErrMountOCMShare = errorcode.New(errorcode.InvalidRequest, "mounting ocm share failed")
)
type (
// UpdateOCMShareClosure is a closure that injects required updates into the update request
UpdateOCMShareClosure func(share *ocm.ReceivedShare, request *ocm.UpdateReceivedOCMShareRequest)
)
// GetOCMSharesForResource returns all federated shares for a given resourceID
func (s DrivesDriveItemService) GetOCMSharesForResource(ctx context.Context, resourceID *storageprovider.ResourceId) ([]*ocm.ReceivedShare, error) {
// Find all accepted shares for this resource
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
receivedOCMSharesResponse, err := gatewayClient.ListReceivedOCMShares(ctx, &ocm.ListReceivedOCMSharesRequest{
/* ocm has no filters, yet
Filters: append([]*collaboration.Filter{
{
Type: collaboration.Filter_TYPE_RESOURCE_ID,
Term: &collaboration.Filter_ResourceId{
ResourceId: resourceID,
},
},
}, filters...),
*/
})
switch {
case err != nil:
return nil, err
case len(receivedOCMSharesResponse.GetShares()) == 0:
return nil, ErrNoShares
default:
return receivedOCMSharesResponse.GetShares(), errorcode.FromCS3Status(receivedOCMSharesResponse.GetStatus(), err)
}
}
// UpdateShares updates multiple shares;
// it could happen that some shares are updated and some are not,
// this will return a list of updated shares and a list of errors;
// there is no guarantee that all updates are successful
func (s DrivesDriveItemService) UpdateOCMShares(ctx context.Context, shares []*ocm.ReceivedShare, updater UpdateOCMShareClosure) ([]*ocm.ReceivedShare, error) {
errs := make([]error, 0, len(shares))
updatedShares := make([]*ocm.ReceivedShare, 0, len(shares))
for _, share := range shares {
err := s.UpdateOCMShare(
ctx,
share,
updater,
)
if err != nil {
errs = append(errs, err)
continue
}
updatedShares = append(updatedShares, share)
}
return updatedShares, errors.Join(errs...)
}
// UpdateOCMShare updates a single share
func (s DrivesDriveItemService) UpdateOCMShare(ctx context.Context, share *ocm.ReceivedShare, updater UpdateOCMShareClosure) error {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return err
}
updateReceivedOCMShareRequest := &ocm.UpdateReceivedOCMShareRequest{
Share: &ocm.ReceivedShare{
Id: share.GetId(),
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}},
}
switch updater {
case nil:
return ErrNoUpdater
default:
updater(share, updateReceivedOCMShareRequest)
}
if len(updateReceivedOCMShareRequest.GetUpdateMask().GetPaths()) == 0 {
return ErrNoUpdates
}
updateReceivedOCMShareResponse, err := gatewayClient.UpdateReceivedOCMShare(ctx, updateReceivedOCMShareRequest)
return errorcode.FromCS3Status(updateReceivedOCMShareResponse.GetStatus(), err)
}
func (s DrivesDriveItemService) MountOCMShare(ctx context.Context, resourceID *storageprovider.ResourceId /*, name string*/) ([]*ocm.ReceivedShare, error) {
/*
if filepath.IsAbs(name) {
return nil, ErrAbsoluteNamePath
}
if name != "" {
name = filepath.Clean(name)
}
*/
shares, err := s.GetOCMSharesForResource(ctx, resourceID)
if err != nil {
return nil, err
}
availableShares := make([]*ocm.ReceivedShare, 0, len(shares))
mountedShares := make([]*ocm.ReceivedShare, 0, 1)
for _, v := range shares {
switch v.GetState() {
case ocm.ShareState_SHARE_STATE_ACCEPTED:
mountedShares = append(mountedShares, v)
case ocm.ShareState_SHARE_STATE_PENDING, ocm.ShareState_SHARE_STATE_REJECTED:
availableShares = append(availableShares, v)
}
}
if len(availableShares) == 0 {
if len(mountedShares) > 0 {
return nil, ErrAlreadyMounted
}
return nil, ErrNoShares
}
updatedShares, err := s.UpdateOCMShares(ctx, availableShares, func(share *ocm.ReceivedShare, request *ocm.UpdateReceivedOCMShareRequest) {
request.Share.State = ocm.ShareState_SHARE_STATE_ACCEPTED
request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathState)
// only update if mountPoint name is not empty and the path has changed
/* ocm shares have no mount point???
if name != "" {
mountPoint := share.GetMountPoint()
if mountPoint == nil {
mountPoint = &storageprovider.Reference{}
}
if filepath.Clean(mountPoint.GetPath()) != name {
mountPoint.Path = name
request.Share.MountPoint = mountPoint
request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathMountPoint)
}
}
*/
})
errs, ok := err.(interface{ Unwrap() []error })
if ok && len(errs.Unwrap()) == len(availableShares) {
// none of the received ocm shares could be accepted.
// this is an error, return it.
return nil, err
}
return updatedShares, nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,159 @@
package svc
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/go-chi/render"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/graph/pkg/errorcode"
)
type (
// UsersUserProfilePhotoProvider is the interface that defines the methods for the user profile photo service
UsersUserProfilePhotoProvider interface {
// GetPhoto retrieves the requested photo
GetPhoto(ctx context.Context, id string) ([]byte, error)
// UpdatePhoto retrieves the requested photo
UpdatePhoto(ctx context.Context, id string, r io.Reader) error
// DeletePhoto deletes the requested photo
DeletePhoto(ctx context.Context, id string) error
}
)
var (
// ErrNoBytes is returned when no bytes are found
ErrNoBytes = errors.New("no bytes")
// ErrInvalidContentType is returned when the content type is invalid
ErrInvalidContentType = errors.New("invalid content type")
// ErrMissingArgument is returned when a required argument is missing
ErrMissingArgument = errors.New("required argument is missing")
)
// UsersUserProfilePhotoService is the implementation of the UsersUserProfilePhotoProvider interface
type UsersUserProfilePhotoService struct {
storage metadata.Storage
}
// NewUsersUserProfilePhotoService creates a new UsersUserProfilePhotoService
func NewUsersUserProfilePhotoService(storage metadata.Storage) (UsersUserProfilePhotoService, error) {
return UsersUserProfilePhotoService{
storage: storage,
}, nil
}
// GetPhoto retrieves the requested photo
func (s UsersUserProfilePhotoService) GetPhoto(ctx context.Context, id string) ([]byte, error) {
return s.storage.SimpleDownload(ctx, id)
}
// DeletePhoto deletes the requested photo
func (s UsersUserProfilePhotoService) DeletePhoto(ctx context.Context, id string) error {
return s.storage.Delete(ctx, id)
}
// UpdatePhoto updates the requested photo
func (s UsersUserProfilePhotoService) UpdatePhoto(ctx context.Context, id string, r io.Reader) error {
if id == "" {
return fmt.Errorf("%w: %s", ErrMissingArgument, "id")
}
photo, err := io.ReadAll(r)
if err != nil {
return err
}
if len(photo) == 0 {
return ErrNoBytes
}
contentType := http.DetectContentType(photo)
if !strings.HasPrefix(contentType, "image/") {
return fmt.Errorf("%w: %s", ErrInvalidContentType, contentType)
}
return s.storage.SimpleUpload(ctx, id, photo)
}
// UsersUserProfilePhotoApi contains all photo related api endpoints
type UsersUserProfilePhotoApi struct {
logger log.Logger
usersUserProfilePhotoService UsersUserProfilePhotoProvider
}
// NewUsersUserProfilePhotoApi creates a new UsersUserProfilePhotoApi
func NewUsersUserProfilePhotoApi(usersUserProfilePhotoService UsersUserProfilePhotoProvider, logger log.Logger) (UsersUserProfilePhotoApi, error) {
return UsersUserProfilePhotoApi{
logger: log.Logger{Logger: logger.With().Str("graph api", "UsersUserProfilePhotoApi").Logger()},
usersUserProfilePhotoService: usersUserProfilePhotoService,
}, nil
}
// GetProfilePhoto creates a handler which renders the corresponding photo
func (api UsersUserProfilePhotoApi) GetProfilePhoto(h HTTPDataHandler[string]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
v, ok := h(w, r)
if !ok {
return
}
photo, err := api.usersUserProfilePhotoService.GetPhoto(r.Context(), v)
if err != nil {
api.logger.Debug().Err(err)
errorcode.GeneralException.Render(w, r, http.StatusNotFound, "failed to get photo")
return
}
render.Status(r, http.StatusOK)
_, _ = w.Write(photo)
}
}
// UpsertProfilePhoto creates a handler which updates or creates the corresponding photo
func (api UsersUserProfilePhotoApi) UpsertProfilePhoto(h HTTPDataHandler[string]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
v, ok := h(w, r)
if !ok {
return
}
if err := api.usersUserProfilePhotoService.UpdatePhoto(r.Context(), v, r.Body); err != nil {
api.logger.Debug().Err(err)
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "failed to update photo")
return
}
defer func() {
_ = r.Body.Close()
}()
render.Status(r, http.StatusOK)
}
}
// DeleteProfilePhoto creates a handler which deletes the corresponding photo
func (api UsersUserProfilePhotoApi) DeleteProfilePhoto(h HTTPDataHandler[string]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
v, ok := h(w, r)
if !ok {
return
}
if err := api.usersUserProfilePhotoService.DeletePhoto(r.Context(), v); err != nil {
api.logger.Debug().Err(err)
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "failed to delete photo")
return
}
render.Status(r, http.StatusOK)
}
}
@@ -0,0 +1,141 @@
package svc_test
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/graph/mocks"
svc "github.com/qsfera/server/services/graph/pkg/service/v0"
)
func TestNewUsersUserProfilePhotoService(t *testing.T) {
service, err := svc.NewUsersUserProfilePhotoService(mocks.NewStorage(t))
assert.NoError(t, err)
t.Run("UpdatePhoto", func(t *testing.T) {
t.Run("reports an error if id is empty", func(t *testing.T) {
err := service.UpdatePhoto(context.Background(), "", bytes.NewReader([]byte{}))
assert.ErrorIs(t, err, svc.ErrMissingArgument)
})
t.Run("reports an error if the reader does not contain any bytes", func(t *testing.T) {
err := service.UpdatePhoto(context.Background(), "123", bytes.NewReader([]byte{}))
assert.ErrorIs(t, err, svc.ErrNoBytes)
})
t.Run("reports an error if data is not an image", func(t *testing.T) {
err := service.UpdatePhoto(context.Background(), "234", bytes.NewReader([]byte("not an image")))
assert.ErrorIs(t, err, svc.ErrInvalidContentType)
})
})
}
func TestUsersUserProfilePhotoApi(t *testing.T) {
var (
serviceProvider = mocks.NewUsersUserProfilePhotoProvider(t)
dataProvider = func(w http.ResponseWriter, r *http.Request) (string, bool) {
return "123", true
}
)
api, err := svc.NewUsersUserProfilePhotoApi(serviceProvider, log.NopLogger())
assert.NoError(t, err)
t.Run("GetProfilePhoto", func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/", nil)
ep := api.GetProfilePhoto(dataProvider)
t.Run("fails if photo provider errors", func(t *testing.T) {
w := httptest.NewRecorder()
serviceProvider.EXPECT().GetPhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) ([]byte, error) {
return nil, errors.New("any")
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusNotFound, w.Code)
})
t.Run("successfully returns the requested photo", func(t *testing.T) {
w := httptest.NewRecorder()
serviceProvider.EXPECT().GetPhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) ([]byte, error) {
return []byte("photo"), nil
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "photo", w.Body.String())
})
})
t.Run("DeleteProfilePhoto", func(t *testing.T) {
r := httptest.NewRequest(http.MethodDelete, "/", nil)
ep := api.DeleteProfilePhoto(dataProvider)
t.Run("fails if photo provider errors", func(t *testing.T) {
w := httptest.NewRecorder()
serviceProvider.EXPECT().DeletePhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) error {
return errors.New("any")
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
t.Run("successfully deletes the requested photo", func(t *testing.T) {
w := httptest.NewRecorder()
serviceProvider.EXPECT().DeletePhoto(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string) error {
return nil
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
})
})
t.Run("UpsertProfilePhoto", func(t *testing.T) {
r := httptest.NewRequest(http.MethodPut, "/", strings.NewReader("body"))
ep := api.UpsertProfilePhoto(dataProvider)
t.Run("fails if photo provider errors", func(t *testing.T) {
w := httptest.NewRecorder()
serviceProvider.EXPECT().UpdatePhoto(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string, r io.Reader) error {
return errors.New("any")
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
t.Run("successfully upserts the photo", func(t *testing.T) {
w := httptest.NewRecorder()
serviceProvider.EXPECT().UpdatePhoto(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, s string, r io.Reader) error {
return nil
}).Once()
ep.ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
})
})
}
@@ -0,0 +1,77 @@
package svc
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/graph/pkg/errorcode"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
// ListApplications implements the Service interface.
func (g Graph) ListApplications(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Interface("query", r.URL.Query()).Msg("calling list applications")
lbr, err := g.roleService.ListRoles(r.Context(), &settingssvc.ListBundlesRequest{})
if err != nil {
logger.Error().Err(err).Msg("could not list roles: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
roles := make([]libregraph.AppRole, 0, len(lbr.Bundles))
for _, bundle := range lbr.GetBundles() {
role := libregraph.NewAppRole(bundle.GetId())
role.SetDisplayName(bundle.GetDisplayName())
roles = append(roles, *role)
}
application := libregraph.NewApplication(g.config.Application.ID)
application.SetDisplayName(g.config.Application.DisplayName)
application.SetAppRoles(roles)
applications := []*libregraph.Application{
application,
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: applications})
}
// GetApplication implements the Service interface.
func (g Graph) GetApplication(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Interface("query", r.URL.Query()).Msg("calling get application")
applicationID := chi.URLParam(r, "applicationID")
if applicationID != g.config.Application.ID {
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, fmt.Sprintf("requested id %s does not match expected application id %v", applicationID, g.config.Application.ID))
return
}
lbr, err := g.roleService.ListRoles(r.Context(), &settingssvc.ListBundlesRequest{})
if err != nil {
logger.Error().Err(err).Msg("could not list roles: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
roles := make([]libregraph.AppRole, 0, len(lbr.Bundles))
for _, bundle := range lbr.GetBundles() {
role := libregraph.NewAppRole(bundle.GetId())
role.SetDisplayName(bundle.GetDisplayName())
roles = append(roles, *role)
}
application := libregraph.NewApplication(applicationID)
application.SetDisplayName(g.config.Application.DisplayName)
application.SetAppRoles(roles)
render.Status(r, http.StatusOK)
render.JSON(w, r, application)
}
@@ -0,0 +1,149 @@
package svc_test
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/go-chi/chi/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
"github.com/qsfera/server/pkg/shared"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
settings "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/graph/mocks"
"github.com/qsfera/server/services/graph/pkg/config"
"github.com/qsfera/server/services/graph/pkg/config/defaults"
identitymocks "github.com/qsfera/server/services/graph/pkg/identity/mocks"
service "github.com/qsfera/server/services/graph/pkg/service/v0"
)
type applicationList struct {
Value []*libregraph.Application
}
var _ = Describe("Applications", func() {
var (
svc service.Service
ctx context.Context
cfg *config.Config
gatewayClient *cs3mocks.GatewayAPIClient
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
eventsPublisher mocks.Publisher
roleService *mocks.RoleService
identityBackend *identitymocks.Backend
rr *httptest.ResponseRecorder
)
BeforeEach(func() {
eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
identityBackend = &identitymocks.Backend{}
roleService = &mocks.RoleService{}
pool.RemoveSelector("GatewaySelector" + "qsfera.api.gateway")
gatewayClient = &cs3mocks.GatewayAPIClient{}
gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient](
"GatewaySelector",
"qsfera.api.gateway",
func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient {
return gatewayClient
},
)
rr = httptest.NewRecorder()
ctx = context.Background()
cfg = defaults.FullDefaultConfig()
cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests
cfg.TokenManager.JWTSecret = "loremipsum"
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
cfg.Application.ID = "some-application-ID"
var err error
svc, err = service.NewService(
service.Config(cfg),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
service.WithRoleService(roleService),
)
Expect(err).ToNot(HaveOccurred())
})
Describe("ListApplications", func() {
It("lists the configured application with appRoles", func() {
roleService.On("ListRoles", mock.Anything, mock.Anything, mock.Anything).Return(&settings.ListBundlesResponse{
Bundles: []*settingsmsg.Bundle{
{
Id: "some-appRole-ID",
Type: settingsmsg.Bundle_TYPE_ROLE,
DisplayName: "A human readable name for a role",
},
},
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/applications", nil)
svc.ListApplications(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
responseList := applicationList{}
err = json.Unmarshal(data, &responseList)
Expect(err).ToNot(HaveOccurred())
Expect(len(responseList.Value)).To(Equal(1))
Expect(responseList.Value[0].Id).To(Equal(cfg.Application.ID))
Expect(len(responseList.Value[0].GetAppRoles())).To(Equal(1))
Expect(responseList.Value[0].GetAppRoles()[0].GetId()).To(Equal("some-appRole-ID"))
Expect(responseList.Value[0].GetAppRoles()[0].GetDisplayName()).To(Equal("A human readable name for a role"))
})
})
Describe("GetApplication", func() {
It("gets the application with appRoles", func() {
roleService.On("ListRoles", mock.Anything, mock.Anything, mock.Anything).Return(&settings.ListBundlesResponse{
Bundles: []*settingsmsg.Bundle{
{
Id: "some-appRole-ID",
Type: settingsmsg.Bundle_TYPE_ROLE,
DisplayName: "A human readable name for a role",
},
},
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/applications/some-application-ID", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("applicationID", cfg.Application.ID)
r = r.WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx))
svc.GetApplication(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
application := libregraph.Application{}
err = json.Unmarshal(data, &application)
Expect(err).ToNot(HaveOccurred())
Expect(application.Id).To(Equal(cfg.Application.ID))
Expect(len(application.GetAppRoles())).To(Equal(1))
Expect(application.GetAppRoles()[0].GetId()).To(Equal("some-appRole-ID"))
Expect(application.GetAppRoles()[0].GetDisplayName()).To(Equal("A human readable name for a role"))
})
})
})

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