Merge branch 'use-scim-server-php' into 'main'
Use scim-server-php lib See merge request libre.sh/scim/scimserviceprovider!2
This commit is contained in:
commit
50266601a7
28 changed files with 8038 additions and 2362 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
vendor
|
vendor
|
||||||
|
/lib/Vendor
|
||||||
|
|
116
README.md
116
README.md
|
@ -1,22 +1,101 @@
|
||||||
# SCIM Service Provider
|
# SCIM Service Provider
|
||||||
|
|
||||||
This app allows to provision users and groups in Nextcloud from a scim client.
|
This app allows to provision users and groups in Nextcloud from a scim client. It is based on [audriga/scim-server-php](https://github.com/audriga/scim-server-php) SCIM library.
|
||||||
|
|
||||||
You can see the [video](https://hot-objects.liiib.re/meet-liiib-re-recordings/pair_2022-05-02-15-40-37.mp4) that shows how it works.
|
You can see the [video](https://hot-objects.liiib.re/meet-liiib-re-recordings/pair_2022-05-02-15-40-37.mp4) that shows how it works.
|
||||||
|
|
||||||
## Limitations
|
---
|
||||||
|
|
||||||
- doesn't accept `application/scim+json` content-type, but only `application/json`
|
## Table of content
|
||||||
- doesn't implement `meta:createdAt` nor `meta:lastModified` due to this [bug](https://github.com/nextcloud/server/issues/22640) (return unix epoch instead).
|
|
||||||
|
1. [How to use](#how-to-use)
|
||||||
|
1. [Installation](#installation)
|
||||||
|
2. [Authentication](#authentication)
|
||||||
|
1. [Basic authentication](#basic-authentication)
|
||||||
|
2. [Bearer token authentication](#bearer-token-authentication)
|
||||||
|
1. [JWT generation (for admins only!)](#jwt-generation-for-admins-only)
|
||||||
|
2. [Usage of the JWT](#usage-of-the-jwt)
|
||||||
|
2. [Use with Keycloak](#use-with-keycloak)
|
||||||
|
3. [Use with AzureAD](#use-with-azuread)
|
||||||
|
4. [Running tests](#running-tests)
|
||||||
|
5. [Todo](#todo)
|
||||||
|
6. [Disclaimer](#disclaimer)
|
||||||
|
7. [NextGov Hackathon](#nextgov-hackathon)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
We plan to publish on the Nextcloud app store, but in the mean time, you can use instructions at the bottom.
|
### Installation
|
||||||
|
We plan to publish on the Nextcloud app store, but in the mean time you can use instructions bellow.
|
||||||
|
|
||||||
|
```
|
||||||
|
cd apps
|
||||||
|
wget https://lab.libreho.st/libre.sh/scim/nextcloud-scim/-/archive/main/nextcloud-scim-main.zip
|
||||||
|
unzip nextcloud-scim-main.zip
|
||||||
|
rm nextcloud-scim-main.zip
|
||||||
|
rm -rf scimserviceprovider
|
||||||
|
mv nextcloud-scim-main scimserviceprovider
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
Currently, this app supports both Basic authentication, as well as Bearer token authentication via JWTs. One can change between these two authentication modes by setting the `auth_type` config parameter in the config file under `/lib/Config/config.php` to either `basic` or `bearer`.
|
||||||
|
|
||||||
|
#### Basic authentication
|
||||||
|
In order to authenticate via Basic auth, send SCIM requests to the SCIM endpoints of the following form:
|
||||||
|
|
||||||
|
> `http://<path-to-nextcloud>/index.php/apps/scimserviceprovider/<Resource>`
|
||||||
|
|
||||||
|
where `<Resource>` designates a SCIM resource, such as `Users` or `Groups`.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl http://<path-to-nextcloud>/index.php/apps/scimserviceprovider/<Resource> -u someusername:pass123 -H 'Content-Type: application/scim+json'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Bearer token authentication
|
||||||
|
In order to authenticate via a Bearer token, send SCIM requests to the SCIM endpoints of the following form:
|
||||||
|
|
||||||
|
> `http://<path-to-nextcloud>/index.php/apps/scimserviceprovider/bearer/<Resource>`
|
||||||
|
|
||||||
|
where `<Resource>` designates a SCIM resource, such as `Users` or `Groups`. Also, make sure to provide the Bearer token in the `Authorization` header of the SCIM HTTP request.
|
||||||
|
|
||||||
|
##### JWT generation (for admins only!)
|
||||||
|
Before providing the token, though, you'd need to obtain one. This is done with the help of a script which can generate JWTs and which is part of `scim-server-php`, the SCIM library by audriga, used as a dependency in this app.
|
||||||
|
|
||||||
|
A JWT can be generated as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vendor/audriga/scim-opf/bin/generate_jwt.php --username someusername --secret topsecret123
|
||||||
|
```
|
||||||
|
|
||||||
|
where
|
||||||
|
|
||||||
|
- `--username` is the username of the user that you want to generate a JWT
|
||||||
|
- `--secret` is the secret key set in the `jwt` config parameter in the config file under `/lib/Config/config.php`, used for signing the JWT
|
||||||
|
|
||||||
|
**Note:** the generated JWT has a claim, called `user` which contains the username that was passed to the JWT generation script and which is later also used for performing the actual authentication check in Nextcloud. For example, it could look like something like this: `{"user":"someusername"}.`
|
||||||
|
|
||||||
|
##### Usage of the JWT
|
||||||
|
A sample usage of JWT authentication as an example:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl http://<path-to-nextcloud>/index.php/apps/scimserviceprovider/<Resource> -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.Oetm7xvhkYbiItRiqNx-z7LZ6ZkmDe1z_95igbPUSjA' -H 'Content-Type: application/scim+json'
|
||||||
|
```
|
||||||
|
|
||||||
## Use with Keycloak
|
## Use with Keycloak
|
||||||
|
|
||||||
You can use with the [SCIM plugin we developped for keycloak](https://lab.libreho.st/libre.sh/scim/keycloak-scim).
|
You can use with the [SCIM plugin we developped for keycloak](https://lab.libreho.st/libre.sh/scim/keycloak-scim).
|
||||||
|
|
||||||
|
## Use with AzureAD
|
||||||
|
|
||||||
|
You can provision users from AzureAD to Nextcloud with this app. For this, you need to do the following:
|
||||||
|
|
||||||
|
- Enable Bearer token authentication via JWTs (see [Authentication](#authentication))
|
||||||
|
- Generate a JWT (see [JWT Generation](#jwt-generation-for-admins-only)) and provide it to AzureAD
|
||||||
|
- Finally, point AzureAD to `https://<path-to-nextcloud>/index.php/apps/scimserviceprovider/bearer`
|
||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
To run the test, you can use [insomnia UI](https://docs.insomnia.rest).
|
To run the test, you can use [insomnia UI](https://docs.insomnia.rest).
|
||||||
|
@ -27,10 +106,11 @@ For CI, there is still [a bug](https://github.com/Kong/insomnia/issues/4747) we
|
||||||
|
|
||||||
## Todo
|
## Todo
|
||||||
|
|
||||||
- [ ] Meta (Create our own table)
|
- [ ] Meta -> ([can't implement yet](https://github.com/nextcloud/server/issues/22640))
|
||||||
- createdAt
|
- createdAt
|
||||||
- lastModified
|
- lastModified
|
||||||
- [ ] ExternalID for Groups (Create our onw table)
|
- [ ] ExternalID
|
||||||
|
- [ ] Groups - [waiting for feedback](https://help.nextcloud.com/t/add-metadata-to-groups/139271)
|
||||||
- [ ] json exceptions
|
- [ ] json exceptions
|
||||||
- [ ] group member removal
|
- [ ] group member removal
|
||||||
- [ ] pagination
|
- [ ] pagination
|
||||||
|
@ -39,19 +119,19 @@ For CI, there is still [a bug](https://github.com/Kong/insomnia/issues/4747) we
|
||||||
- [ ] test psalm
|
- [ ] test psalm
|
||||||
- [ ] test insomnia
|
- [ ] test insomnia
|
||||||
- [ ] publish app on app store
|
- [ ] publish app on app store
|
||||||
- [ ] lib user scim php
|
- [ ] Allow for simultaneous usage of basic auth and bearer token auth (see **Authentication TODOs / Open issues**)
|
||||||
- [ ] accept first email, even if not primary
|
|
||||||
|
|
||||||
## Quick "Deploy" to test
|
### Authentication TODOs / Open issues
|
||||||
|
#### Support for simultaneously using basic auth and bearer token auth in parallel
|
||||||
|
Solution idea:
|
||||||
|
|
||||||
```
|
- Instead of having two different sets of endpoints which are disjunct from each other for supporting both auth types, one could add an authentication middleware which intercepts requests and checks the `Authorization` header's contents
|
||||||
cd apps
|
- Depending on whether the header has as first part of its value the string `Basic` or `Bearer`, the middleware can decide which authentication logic to call for performing the authentication with the provided authentication credentials
|
||||||
wget https://lab.libreho.st/libre.sh/scim/nextcloud-scim/-/archive/main/nextcloud-scim-main.zip
|
- In case of `Bearer`, the current implementation of bearer token authentication via JWTs can be used
|
||||||
unzip nextcloud-scim-main.zip
|
- In case of `Basic`, one could take a closer look at how Nextcloud performs basic authentication for API endpoints and possibly make use of methods like [checkPassword](https://github.com/nextcloud/server/blob/master/lib/private/User/Manager.php#L237) from the [Manager](https://github.com/nextcloud/server/blob/master/lib/private/User/Manager.php) class for Nextcloud users
|
||||||
rm nextcloud-scim-main.zip
|
|
||||||
rm -rf scimserviceprovider
|
## Disclaimer
|
||||||
mv nextcloud-scim-main scimserviceprovider
|
This app relies on the fixes, being introduced to Nextcloud in [PR #34172](https://github.com/nextcloud/server/pull/34172), since Nextcloud can't properly handle the `Content-Type` header value for SCIM (`application/scim+json`) otherwise. In the meantime until this PR is merged, SCIM clients interacting with this app might need to resort to using the standard value of `application/json` instead.
|
||||||
```
|
|
||||||
|
|
||||||
## NextGov Hackathon
|
## NextGov Hackathon
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,46 @@
|
||||||
<?php
|
<?php
|
||||||
return [
|
|
||||||
'resources' => [
|
$routes = [
|
||||||
'user' => ['url' => '/Users'],
|
'routes' => [
|
||||||
'group' => ['url' => '/Groups']
|
['name' => 'service_provider_configuration#resource_types', 'url' => '/ResourceTypes', 'verb' => 'GET'],
|
||||||
|
['name' => 'service_provider_configuration#schemas', 'url' => '/Schemas', 'verb' => 'GET'],
|
||||||
|
['name' => 'service_provider_configuration#service_provider_config', 'url' => '/ServiceProviderConfig', 'verb' => 'GET'],
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$config = require dirname(__DIR__) . '/lib/Config/config.php';
|
||||||
|
$userAndGroupRoutes = [];
|
||||||
|
|
||||||
|
if (isset($config['auth_type']) && !empty($config['auth_type']) && (strcmp($config['auth_type'], 'bearer') === 0)) {
|
||||||
|
$userAndGroupRoutes = [
|
||||||
|
['name' => 'user_bearer#index', 'url' => '/bearer/Users', 'verb' => 'GET'],
|
||||||
|
['name' => 'user_bearer#show', 'url' => '/bearer/Users/{id}', 'verb' => 'GET'],
|
||||||
|
['name' => 'user_bearer#create', 'url' => '/bearer/Users', 'verb' => 'POST'],
|
||||||
|
['name' => 'user_bearer#update', 'url' => '/bearer/Users/{id}', 'verb' => 'PUT'],
|
||||||
|
['name' => 'user_bearer#destroy', 'url' => '/bearer/Users/{id}', 'verb' => 'DELETE'],
|
||||||
|
|
||||||
|
['name' => 'group_bearer#index', 'url' => '/bearer/Groups', 'verb' => 'GET'],
|
||||||
|
['name' => 'group_bearer#show', 'url' => '/bearer/Groups/{id}', 'verb' => 'GET'],
|
||||||
|
['name' => 'group_bearer#create', 'url' => '/bearer/Groups', 'verb' => 'POST'],
|
||||||
|
['name' => 'group_bearer#update', 'url' => '/bearer/Groups/{id}', 'verb' => 'PUT'],
|
||||||
|
['name' => 'group_bearer#destroy', 'url' => '/bearer/Groups/{id}', 'verb' => 'DELETE'],
|
||||||
|
];
|
||||||
|
} else if (!isset($config['auth_type']) || empty($config['auth_type']) || (strcmp($config['auth_type'], 'basic') === 0)) {
|
||||||
|
$userAndGroupRoutes = [
|
||||||
|
['name' => 'user#index', 'url' => '/Users', 'verb' => 'GET'],
|
||||||
|
['name' => 'user#show', 'url' => '/Users/{id}', 'verb' => 'GET'],
|
||||||
|
['name' => 'user#create', 'url' => '/Users', 'verb' => 'POST'],
|
||||||
|
['name' => 'user#update', 'url' => '/Users/{id}', 'verb' => 'PUT'],
|
||||||
|
['name' => 'user#destroy', 'url' => '/Users/{id}', 'verb' => 'DELETE'],
|
||||||
|
|
||||||
|
['name' => 'group#index', 'url' => '/Groups', 'verb' => 'GET'],
|
||||||
|
['name' => 'group#show', 'url' => '/Groups/{id}', 'verb' => 'GET'],
|
||||||
|
['name' => 'group#create', 'url' => '/Groups', 'verb' => 'POST'],
|
||||||
|
['name' => 'group#update', 'url' => '/Groups/{id}', 'verb' => 'PUT'],
|
||||||
|
['name' => 'group#destroy', 'url' => '/Groups/{id}', 'verb' => 'DELETE'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$routes['routes'] = array_merge($routes['routes'], $userAndGroupRoutes);
|
||||||
|
|
||||||
|
return $routes;
|
||||||
|
|
|
@ -11,6 +11,25 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cs:check": "php-cs-fixer fix --dry-run --diff",
|
"cs:check": "php-cs-fixer fix --dry-run --diff",
|
||||||
"cs:fix": "php-cs-fixer fix"
|
"cs:fix": "php-cs-fixer fix",
|
||||||
|
"post-install-cmd": [
|
||||||
|
"rm -rf vendor/firebase",
|
||||||
|
"composer dump-autoload"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"rm -rf vendor/firebase",
|
||||||
|
"composer dump-autoload"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"repositories": {
|
||||||
|
"scim": {
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "git@github.com:audriga/scim-server-php.git"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"audriga/scim-server-php": "dev-main",
|
||||||
|
"doctrine/lexer": "^1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6519
composer.lock
generated
6519
composer.lock
generated
File diff suppressed because it is too large
Load diff
110
lib/Adapter/Groups/NextcloudGroupAdapter.php
Normal file
110
lib/Adapter/Groups/NextcloudGroupAdapter.php
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Adapter\Groups;
|
||||||
|
|
||||||
|
use OCP\IGroup;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use Opf\Adapters\AbstractAdapter;
|
||||||
|
use Opf\Models\SCIM\Standard\Groups\CoreGroup;
|
||||||
|
use Opf\Models\SCIM\Standard\MultiValuedAttribute;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class NextcloudGroupAdapter extends AbstractAdapter
|
||||||
|
{
|
||||||
|
/** @var Psr\Log\LoggerInterface */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
/** @var IUserManager */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
|
/** @var IRequest */
|
||||||
|
private $request;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$this->logger = $container->get(LoggerInterface::class);
|
||||||
|
$this->userManager = $container->get(IUserManager::class);
|
||||||
|
$this->request = $container->get(IRequest::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform an NC group into a SCIM group
|
||||||
|
*/
|
||||||
|
public function getCoreGroup(?IGroup $ncGroup): ?CoreGroup
|
||||||
|
{
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupAdapter::class . "] entering getCoreGroup() method"
|
||||||
|
);
|
||||||
|
|
||||||
|
$baseUrl = $this->request->getServerProtocol() . "://" . $this->request->getServerHost() . "/index.php/apps/scimserviceprovider";
|
||||||
|
|
||||||
|
if (!isset($ncGroup)) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudGroupAdapter::class . "] passed NC group in getCoreGroup() method is null"
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$coreGroup = new CoreGroup();
|
||||||
|
$coreGroup->setId($ncGroup->getGID());
|
||||||
|
$coreGroup->setDisplayName($ncGroup->getDisplayName());
|
||||||
|
|
||||||
|
$ncGroupMembers = $ncGroup->getUsers();
|
||||||
|
|
||||||
|
if (isset($ncGroupMembers) && !empty($ncGroupMembers)) {
|
||||||
|
$coreGroupMembers = [];
|
||||||
|
|
||||||
|
foreach ($ncGroupMembers as $ncGroupMember) {
|
||||||
|
$coreGroupMember = new MultiValuedAttribute();
|
||||||
|
$coreGroupMember->setValue($ncGroupMember->getUID());
|
||||||
|
$coreGroupMember->setRef($baseUrl . "/Users/" . $ncGroupMember->getUID());
|
||||||
|
$coreGroupMember->setDisplay($ncGroupMember->getDisplayName());
|
||||||
|
|
||||||
|
$coreGroupMembers[] = $coreGroupMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
$coreGroup->setMembers($coreGroupMembers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $coreGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a SCIM group into an NC group
|
||||||
|
*
|
||||||
|
* Note: the second parameter is needed, since we can't instantiate an NC group
|
||||||
|
* ourselves and need to receive an instance, passed from somewhere
|
||||||
|
*/
|
||||||
|
public function getNCGroup(?CoreGroup $coreGroup, IGroup $ncGroup): ?IGroup
|
||||||
|
{
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupAdapter::class . "] entering getNCGroup() method"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isset($coreGroup) || !isset($ncGroup)) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudGroupAdapter::class . "] passed Core Group in getNCGroup() method is null"
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ncGroup->setDisplayName($coreGroup->getDisplayName());
|
||||||
|
|
||||||
|
if ($coreGroup->getMembers() !== null && !empty($coreGroup->getMembers())) {
|
||||||
|
foreach ($coreGroup->getMembers() as $coreGroupMember) {
|
||||||
|
// If user with this uid exists, then add it as a member of the group
|
||||||
|
if ($coreGroupMember->getValue() !== null && !empty($coreGroupMember->getValue())) {
|
||||||
|
if ($this->userManager->userExists($coreGroupMember->getValue())) {
|
||||||
|
$ncGroup->addUser($this->userManager->get($coreGroupMember->getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ncGroup;
|
||||||
|
}
|
||||||
|
}
|
124
lib/Adapter/Users/NextcloudUserAdapter.php
Normal file
124
lib/Adapter/Users/NextcloudUserAdapter.php
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Adapter\Users;
|
||||||
|
|
||||||
|
use OCP\IConfig;
|
||||||
|
use OCP\IUser;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use OCP\Security\ISecureRandom;
|
||||||
|
use Opf\Adapters\AbstractAdapter;
|
||||||
|
use Opf\Models\SCIM\Standard\MultiValuedAttribute;
|
||||||
|
use Opf\Models\SCIM\Standard\Users\CoreUser;
|
||||||
|
use Opf\Models\SCIM\Standard\Users\Name;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class NextcloudUserAdapter extends AbstractAdapter
|
||||||
|
{
|
||||||
|
/** @var Psr\Log\LoggerInterface */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
/** @var IConfig */
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
/** @var IUserManager */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
|
/** @var ISecureRandom */
|
||||||
|
private $secureRandom;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$this->logger = $container->get(LoggerInterface::class);
|
||||||
|
$this->config = $container->get(IConfig::class);
|
||||||
|
$this->userManager = $container->get(IUserManager::class);
|
||||||
|
$this->secureRandom = $container->get(ISecureRandom::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform an NC User into a SCIM user
|
||||||
|
*/
|
||||||
|
public function getCoreUser(?IUser $ncUser): ?CoreUser
|
||||||
|
{
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserAdapter::class . "] entering getCoreUser() method"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isset($ncUser)) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudUserAdapter::class . "] passed NC user in getCoreUser() method is null"
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$coreUser = new CoreUser();
|
||||||
|
$coreUser->setId($ncUser->getUID());
|
||||||
|
|
||||||
|
$coreUserName = new Name();
|
||||||
|
$coreUserName->setFormatted($ncUser->getDisplayName());
|
||||||
|
$coreUser->setName($coreUserName);
|
||||||
|
|
||||||
|
$coreUser->setUserName($ncUser->getUID());
|
||||||
|
$coreUser->setDisplayName($ncUser->getDisplayName());
|
||||||
|
$coreUser->setActive($ncUser->isEnabled());
|
||||||
|
|
||||||
|
$ncUserExternalId = $this->config->getUserValue($ncUser->getUID(), 'SCIMServiceProvider', 'ExternalId', '');
|
||||||
|
$coreUser->setExternalId($ncUserExternalId);
|
||||||
|
|
||||||
|
if ($ncUser->getEMailAddress() !== null && !empty($ncUser->getEMailAddress())) {
|
||||||
|
$coreUserEmail = new MultiValuedAttribute();
|
||||||
|
$coreUserEmail->setValue($ncUser->getEMailAddress());
|
||||||
|
$coreUserEmail->setPrimary(true);
|
||||||
|
|
||||||
|
$coreUser->setEmails(array($coreUserEmail));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $coreUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a SCIM user into an NC User
|
||||||
|
*
|
||||||
|
* Note: we need the second parameter, since we can't instantiate an NC user in PHP
|
||||||
|
* ourselves and need to receive an instance that we can populate with data from the SCIM user
|
||||||
|
*/
|
||||||
|
public function getNCUser(?CoreUser $coreUser, IUser $ncUser): ?IUser
|
||||||
|
{
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserAdapter::class . "] entering getNCUser() method"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isset($coreUser) || !isset($ncUser)) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudUserAdapter::class . "] passed Core User in getNCUser() method is null"
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($coreUser->getDisplayName() !== null && !empty($coreUser->getDisplayName())) {
|
||||||
|
$ncUser->setDisplayName($coreUser->getDisplayName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($coreUser->getActive() !== null) {
|
||||||
|
$ncUser->setEnabled($coreUser->getActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($coreUser->getExternalId() !== null && !empty($coreUser->getExternalId())) {
|
||||||
|
$this->config->setUserValue($ncUser->getUID(), 'SCIMServiceProvider', 'ExternalId', $coreUser->getExternalId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($coreUser->getEmails() !== null && !empty($coreUser->getEmails())) {
|
||||||
|
// Here, we use the first email of the SCIM user to set as the NC user's email
|
||||||
|
// TODO: is this ok or should we rather first iterate and search for a primary email of the SCIM user
|
||||||
|
if ($coreUser->getEmails()[0] !== null && !empty($coreUser->getEmails()[0])) {
|
||||||
|
if ($coreUser->getEmails()[0]->getValue() !== null && !empty($coreUser->getEmails()[0]->getValue())) {
|
||||||
|
$ncUser->setEMailAddress($coreUser->getEmails()[0]->getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ncUser;
|
||||||
|
}
|
||||||
|
}
|
161
lib/AppInfo/Application.php
Normal file
161
lib/AppInfo/Application.php
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\AppInfo;
|
||||||
|
|
||||||
|
use Error;
|
||||||
|
use OCA\SCIMServiceProvider\Adapter\Groups\NextcloudGroupAdapter;
|
||||||
|
use OCA\SCIMServiceProvider\Adapter\Users\NextcloudUserAdapter;
|
||||||
|
use OCA\SCIMServiceProvider\Controller\GroupBearerController;
|
||||||
|
use OCA\SCIMServiceProvider\Controller\GroupController;
|
||||||
|
use OCA\SCIMServiceProvider\Controller\UserBearerController;
|
||||||
|
use OCA\SCIMServiceProvider\Controller\UserController;
|
||||||
|
use OCA\SCIMServiceProvider\DataAccess\Groups\NextcloudGroupDataAccess;
|
||||||
|
use OCA\SCIMServiceProvider\DataAccess\Users\NextcloudUserDataAccess;
|
||||||
|
use OCA\SCIMServiceProvider\Middleware\BearerAuthMiddleware;
|
||||||
|
use OCA\SCIMServiceProvider\Repositories\Groups\NextcloudGroupRepository;
|
||||||
|
use OCA\SCIMServiceProvider\Repositories\Users\NextcloudUserRepository;
|
||||||
|
use OCA\SCIMServiceProvider\Service\GroupService;
|
||||||
|
use OCA\SCIMServiceProvider\Service\SCIMGroup;
|
||||||
|
use OCA\SCIMServiceProvider\Service\SCIMUser;
|
||||||
|
use OCA\SCIMServiceProvider\Service\UserService;
|
||||||
|
use OCA\SCIMServiceProvider\Util\Authentication\BearerAuthenticator;
|
||||||
|
use OCP\AppFramework\App;
|
||||||
|
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||||
|
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||||
|
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||||
|
use OCP\IConfig;
|
||||||
|
use OCP\IGroupManager;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use OCP\Security\ISecureRandom;
|
||||||
|
use Opf\Util\Util;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main entry point of the entire application
|
||||||
|
*/
|
||||||
|
class Application extends App implements IBootstrap
|
||||||
|
{
|
||||||
|
public const APP_ID = 'SCIMServiceProvider';
|
||||||
|
|
||||||
|
public function __construct(array $urlParams = [])
|
||||||
|
{
|
||||||
|
parent::__construct(self::APP_ID, $urlParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is used for registering services, needed as dependencies via dependency injection (DI)
|
||||||
|
*
|
||||||
|
* Note: "service" here means simply a class that is needed as a dependency somewhere
|
||||||
|
* and needs to be injected as such via a DI container (as per PSR-11)
|
||||||
|
*/
|
||||||
|
public function register(IRegistrationContext $context): void
|
||||||
|
{
|
||||||
|
require realpath(dirname(__DIR__) . '/../vendor/autoload.php');
|
||||||
|
$config = require dirname(__DIR__) . '/Config/config.php';
|
||||||
|
$context->registerService('SCIMUser', function(ContainerInterface $c) {
|
||||||
|
return new SCIMUser(
|
||||||
|
$c->get(IUserManager::class),
|
||||||
|
$c->get(IConfig::class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService(UserService::class, function(ContainerInterface $c) {
|
||||||
|
return new UserService($c);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService(GroupService::class, function(ContainerInterface $c) {
|
||||||
|
return new GroupService($c);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService('UserRepository', function(ContainerInterface $c) {
|
||||||
|
return new NextcloudUserRepository($c);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService('UserAdapter', function(ContainerInterface $c) {
|
||||||
|
return new NextcloudUserAdapter($c);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService('UserDataAccess', function(ContainerInterface $c) {
|
||||||
|
return new NextcloudUserDataAccess($c);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$context->registerService('SCIMGroup', function(ContainerInterface $c) {
|
||||||
|
return new SCIMGroup(
|
||||||
|
$c->get(IGroupManager::class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService('GroupRepository', function(ContainerInterface $c) {
|
||||||
|
return new NextcloudGroupRepository($c);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService('GroupAdapter', function(ContainerInterface $c) {
|
||||||
|
return new NextcloudGroupAdapter($c);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService('GroupDataAccess', function(ContainerInterface $c) {
|
||||||
|
return new NextcloudGroupDataAccess($c);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isset($config['auth_type']) && !empty($config['auth_type']) && (strcmp($config['auth_type'], 'bearer') === 0)) {
|
||||||
|
// If the auth_type is set to "bearer", then use Bearer token endpoints
|
||||||
|
// For bearer tokens, we also need to register the bearer token auth middleware
|
||||||
|
$context->registerService(BearerAuthenticator::class, function(ContainerInterface $c) {
|
||||||
|
return new BearerAuthenticator($c);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService(BearerAuthMiddleware::class, function(ContainerInterface $c) {
|
||||||
|
return new BearerAuthMiddleware($c);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerMiddleware(BearerAuthMiddleware::class);
|
||||||
|
|
||||||
|
$context->registerService(UserBearerController::class, function (ContainerInterface $c) {
|
||||||
|
return new UserBearerController(
|
||||||
|
self::APP_ID,
|
||||||
|
$c->get(IRequest::class),
|
||||||
|
$c->get(UserService::class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService(GroupBearerController::class, function (ContainerInterface $c) {
|
||||||
|
return new GroupBearerController(
|
||||||
|
self::APP_ID,
|
||||||
|
$c->get(IRequest::class),
|
||||||
|
$c->get(GroupService::class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else if (!isset($config['auth_type']) || empty($config['auth_type']) || (strcmp($config['auth_type'], 'basic') === 0)) {
|
||||||
|
// Otherwise, if auth_type is set to "basic" or if it's not set at all, use Basic auth
|
||||||
|
$context->registerService(UserController::class, function (ContainerInterface $c) {
|
||||||
|
return new UserController(
|
||||||
|
self::APP_ID,
|
||||||
|
$c->get(IRequest::class),
|
||||||
|
$c->get(UserService::class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$context->registerService(GroupController::class, function (ContainerInterface $c) {
|
||||||
|
return new GroupController(
|
||||||
|
self::APP_ID,
|
||||||
|
$c->get(IRequest::class),
|
||||||
|
$c->get(GroupService::class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// In the case of any other auth_type value, complain with an error message
|
||||||
|
throw new Error("Unknown auth type was set in config file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called for starting (i.e., booting) the application
|
||||||
|
*
|
||||||
|
* Note: here the method body is empty, since we don't need to do any extra work in it
|
||||||
|
*/
|
||||||
|
public function boot(IBootContext $context): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
14
lib/Config/config.php
Normal file
14
lib/Config/config.php
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
/**
|
||||||
|
* Allowed value are 'basic' (for Basic Auth) and 'bearer' (for Bearer Token Auth)
|
||||||
|
* The value 'basic' can be considered the default one
|
||||||
|
*/
|
||||||
|
'auth_type' => 'bearer',
|
||||||
|
|
||||||
|
// Config values for JWTs
|
||||||
|
'jwt' => [
|
||||||
|
'secret' => 'secret'
|
||||||
|
]
|
||||||
|
];
|
99
lib/Controller/GroupBearerController.php
Normal file
99
lib/Controller/GroupBearerController.php
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Controller;
|
||||||
|
|
||||||
|
use OCP\AppFramework\ApiController;
|
||||||
|
use OCP\AppFramework\Http\Response;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Service\GroupService;
|
||||||
|
|
||||||
|
class GroupBearerController extends ApiController
|
||||||
|
{
|
||||||
|
/** @var GroupService */
|
||||||
|
private $groupService;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $appName,
|
||||||
|
IRequest $request,
|
||||||
|
GroupService $groupService
|
||||||
|
) {
|
||||||
|
parent::__construct(
|
||||||
|
$appName,
|
||||||
|
$request
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->groupService = $groupService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* @param string $filter
|
||||||
|
* @return SCIMListResponse
|
||||||
|
* returns a list of groups and their data
|
||||||
|
*/
|
||||||
|
public function index(string $filter = ''): SCIMListResponse
|
||||||
|
{
|
||||||
|
return $this->groupService->getAll($filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* gets group info
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
* @return SCIMJSONResponse
|
||||||
|
*/
|
||||||
|
// TODO: Add filtering support here as well
|
||||||
|
public function show(string $id): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
return $this->groupService->getOneById($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* @param string $displayName
|
||||||
|
* @param array $members
|
||||||
|
* @return SCIMJSONResponse
|
||||||
|
*/
|
||||||
|
public function create(string $displayName = '', array $members = []): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
return $this->groupService->create($displayName, $members);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
*
|
||||||
|
* @param string $displayName
|
||||||
|
* @param array $members
|
||||||
|
* @return SCIMJSONResponse
|
||||||
|
*/
|
||||||
|
public function update(string $id, string $displayName = '', array $members = []): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
return $this->groupService->update($id, $displayName, $members);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function destroy(string $id): Response
|
||||||
|
{
|
||||||
|
return $this->groupService->destroy($id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,146 +6,89 @@ namespace OCA\SCIMServiceProvider\Controller;
|
||||||
|
|
||||||
use OCP\AppFramework\ApiController;
|
use OCP\AppFramework\ApiController;
|
||||||
use OCP\AppFramework\Http\Response;
|
use OCP\AppFramework\Http\Response;
|
||||||
use OCP\IGroupManager;
|
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\IUserManager;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
|
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
|
||||||
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
|
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
|
||||||
use OCA\SCIMServiceProvider\Responses\SCIMErrorResponse;
|
use OCA\SCIMServiceProvider\Service\GroupService;
|
||||||
|
|
||||||
use OCA\SCIMServiceProvider\Service\SCIMGroup;
|
class GroupController extends ApiController
|
||||||
|
{
|
||||||
|
/** @var GroupService */
|
||||||
|
private $groupService;
|
||||||
|
|
||||||
class GroupController extends ApiController {
|
public function __construct(
|
||||||
|
string $appName,
|
||||||
|
IRequest $request,
|
||||||
|
GroupService $groupService
|
||||||
|
) {
|
||||||
|
parent::__construct(
|
||||||
|
$appName,
|
||||||
|
$request
|
||||||
|
);
|
||||||
|
|
||||||
/** @var LoggerInterface */
|
$this->groupService = $groupService;
|
||||||
private $logger;
|
}
|
||||||
private $SCIMGroup;
|
|
||||||
|
|
||||||
public function __construct(string $appName,
|
/**
|
||||||
IRequest $request,
|
* @NoCSRFRequired
|
||||||
IUserManager $userManager,
|
*
|
||||||
IGroupManager $groupManager,
|
* @param string $filter
|
||||||
LoggerInterface $logger,
|
* @return SCIMListResponse
|
||||||
SCIMGroup $SCIMGroup) {
|
* returns a list of groups and their data
|
||||||
parent::__construct($appName,
|
*/
|
||||||
$request,
|
public function index(string $filter = ''): SCIMListResponse
|
||||||
$userManager,
|
{
|
||||||
$groupManager);
|
return $this->groupService->getAll($filter);
|
||||||
|
}
|
||||||
|
|
||||||
$this->logger = $logger;
|
/**
|
||||||
$this->SCIMGroup = $SCIMGroup;
|
* @NoCSRFRequired
|
||||||
$this->groupManager = $groupManager;
|
*
|
||||||
$this->userManager = $userManager;
|
* gets group info
|
||||||
}
|
*
|
||||||
|
* @param string $id
|
||||||
|
* @return SCIMJSONResponse
|
||||||
|
*/
|
||||||
|
// TODO: Add filtering support here as well
|
||||||
|
public function show(string $id): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
return $this->groupService->getOneById($id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoCSRFRequired
|
* @NoCSRFRequired
|
||||||
*
|
*
|
||||||
* returns a list of groups and their data
|
* @param string $displayName
|
||||||
*/
|
* @param array $members
|
||||||
public function index(): SCIMListResponse {
|
* @return SCIMJSONResponse
|
||||||
$SCIMGroups = $this->groupManager->search('', null, 0);
|
*/
|
||||||
$SCIMGroups = array_map(function ($group) {
|
public function create(string $displayName = '', array $members = []): SCIMJSONResponse
|
||||||
return $this->SCIMGroup->get($group->getGID());
|
{
|
||||||
}, $SCIMGroups);
|
return $this->groupService->create($displayName, $members);
|
||||||
return new SCIMListResponse($SCIMGroups);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoCSRFRequired
|
* @NoCSRFRequired
|
||||||
*
|
*
|
||||||
* gets group info
|
* @param string $id
|
||||||
*
|
*
|
||||||
* @param string $id
|
* @param string $displayName
|
||||||
* @return SCIMJSONResponse
|
* @param array $members
|
||||||
* @throws Exception
|
* @return SCIMJSONResponse
|
||||||
*/
|
*/
|
||||||
public function show(string $id): SCIMJSONResponse {
|
public function update(string $id, string $displayName = '', array $members = []): SCIMJSONResponse
|
||||||
$group = $this->SCIMGroup->get($id);
|
{
|
||||||
if (empty($group)) {
|
return $this->groupService->update($id, $displayName, $members);
|
||||||
return new SCIMErrorResponse(['message' => 'Group not found'], 404);
|
}
|
||||||
}
|
|
||||||
return new SCIMJSONResponse($group);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoCSRFRequired
|
* @NoCSRFRequired
|
||||||
*
|
*
|
||||||
* @param string $displayName
|
* @param string $id
|
||||||
* @param array $members
|
* @return Response
|
||||||
* @return SCIMJSONResponse
|
*/
|
||||||
* @throws Exception
|
public function destroy(string $id): Response
|
||||||
*/
|
{
|
||||||
public function create(string $displayName = '',
|
return $this->groupService->destroy($id);
|
||||||
array $members = []): SCIMJSONResponse {
|
}
|
||||||
$id = urlencode($displayName);
|
|
||||||
// Validate name
|
|
||||||
if (empty($id)) {
|
|
||||||
$this->logger->error('Group name not supplied', ['app' => 'provisioning_api']);
|
|
||||||
return new SCIMErrorResponse(['message' => 'Invalid group name'], 400);
|
|
||||||
}
|
|
||||||
// Check if it exists
|
|
||||||
if ($this->groupManager->groupExists($id)) {
|
|
||||||
return new SCIMErrorResponse(['message' => 'Group exists'], 409);
|
|
||||||
}
|
|
||||||
$group = $this->groupManager->createGroup($id);
|
|
||||||
if ($group === null) {
|
|
||||||
return new SCIMErrorResponse(['message' => 'Not supported by backend'], 103);
|
|
||||||
}
|
|
||||||
$group->setDisplayName($displayName);
|
|
||||||
foreach ($members as $member) {
|
|
||||||
$this->logger->error('Group name not supplied' . $member['value'], ['app' => 'provisioning_api']);
|
|
||||||
$targetUser = $this->userManager->get($member['value']);
|
|
||||||
$group->addUser($targetUser);
|
|
||||||
}
|
|
||||||
return new SCIMJSONResponse($this->SCIMGroup->get($id));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @NoCSRFRequired
|
|
||||||
*
|
|
||||||
* @param string $id
|
|
||||||
*
|
|
||||||
* @param string $displayName
|
|
||||||
* @param array $members
|
|
||||||
* @return DataResponse
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function update(string $id,
|
|
||||||
string $displayName = '',
|
|
||||||
array $members = []): SCIMJSONResponse {
|
|
||||||
$group = $this->groupManager->get($id);
|
|
||||||
if (!$this->groupManager->groupExists($id)) {
|
|
||||||
return new SCIMErrorResponse(['message' => 'Group not found'], 404);
|
|
||||||
}
|
|
||||||
foreach ($members as $member) {
|
|
||||||
$targetUser = $this->userManager->get($member['value']);
|
|
||||||
$group->addUser($targetUser);
|
|
||||||
// todo implement member removal (:
|
|
||||||
}
|
|
||||||
return new SCIMJSONResponse($this->SCIMGroup->get($id));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @NoCSRFRequired
|
|
||||||
*
|
|
||||||
* @param string $id
|
|
||||||
* @return DataResponse
|
|
||||||
*/
|
|
||||||
public function destroy(string $id): Response {
|
|
||||||
$groupId = urldecode($id);
|
|
||||||
|
|
||||||
// Check it exists
|
|
||||||
if (!$this->groupManager->groupExists($groupId)) {
|
|
||||||
return new SCIMErrorResponse(['message' => 'Group not found'], 404);
|
|
||||||
} elseif ($groupId === 'admin' || !$this->groupManager->get($groupId)->delete()) {
|
|
||||||
// Cannot delete admin group
|
|
||||||
return new SCIMErrorResponse(['message' => 'Can\'t delete this group, not enough rights or admin group'], 403);
|
|
||||||
}
|
|
||||||
$response = new Response();
|
|
||||||
$response->setStatus(204);
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
61
lib/Controller/ServiceProviderConfigurationController.php
Normal file
61
lib/Controller/ServiceProviderConfigurationController.php
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Controller;
|
||||||
|
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Util\Util;
|
||||||
|
use OCP\AppFramework\ApiController;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use Opf\Util\Util as SCIMUtil;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class ServiceProviderConfigurationController extends ApiController
|
||||||
|
{
|
||||||
|
/** @var LoggerInterface */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
public function __construct(string $appName,
|
||||||
|
IRequest $request,
|
||||||
|
LoggerInterface $logger) {
|
||||||
|
parent::__construct($appName,
|
||||||
|
$request);
|
||||||
|
$this->logger = $logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*/
|
||||||
|
public function resourceTypes(): SCIMListResponse
|
||||||
|
{
|
||||||
|
$baseUrl =
|
||||||
|
$this->request->getServerProtocol() . "://"
|
||||||
|
. $this->request->getServerHost() . "/"
|
||||||
|
. Util::SCIM_APP_URL_PATH;
|
||||||
|
$resourceTypes = SCIMUtil::getResourceTypes($baseUrl);
|
||||||
|
return new SCIMListResponse($resourceTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*/
|
||||||
|
public function schemas(): SCIMListResponse
|
||||||
|
{
|
||||||
|
$schemas = SCIMUtil::getSchemas();
|
||||||
|
return new SCIMListResponse($schemas);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*/
|
||||||
|
public function serviceProviderConfig(): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
$serviceProviderConfig = SCIMUtil::getServiceProviderConfig();
|
||||||
|
return new SCIMJSONResponse($serviceProviderConfig);
|
||||||
|
}
|
||||||
|
}
|
121
lib/Controller/UserBearerController.php
Normal file
121
lib/Controller/UserBearerController.php
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Controller;
|
||||||
|
|
||||||
|
use OCP\AppFramework\ApiController;
|
||||||
|
use OCP\AppFramework\Http\Response;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Service\UserService;
|
||||||
|
|
||||||
|
class UserBearerController extends ApiController
|
||||||
|
{
|
||||||
|
/** @var UserService */
|
||||||
|
private $userService;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $appName,
|
||||||
|
IRequest $request,
|
||||||
|
UserService $userService
|
||||||
|
) {
|
||||||
|
parent::__construct(
|
||||||
|
$appName,
|
||||||
|
$request
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->userService = $userService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* @param string $filter
|
||||||
|
* @return SCIMListResponse
|
||||||
|
* returns a list of users and their data
|
||||||
|
*/
|
||||||
|
public function index(string $filter = ''): SCIMListResponse
|
||||||
|
{
|
||||||
|
return $this->userService->getAll($filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* gets user info
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
* @return SCIMJSONResponse
|
||||||
|
*/
|
||||||
|
// TODO: Add filtering support here as well
|
||||||
|
public function show(string $id): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
return $this->userService->getOneById($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* @param bool $active
|
||||||
|
* @param string $displayName
|
||||||
|
* @param array $emails
|
||||||
|
* @param string $externalId
|
||||||
|
* @param string $userName
|
||||||
|
* @return SCIMJSONResponse
|
||||||
|
*/
|
||||||
|
public function create(
|
||||||
|
bool $active = true,
|
||||||
|
string $displayName = '',
|
||||||
|
array $emails = [],
|
||||||
|
string $externalId = '',
|
||||||
|
string $userName = ''
|
||||||
|
): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
return $this->userService->create(
|
||||||
|
$active,
|
||||||
|
$displayName,
|
||||||
|
$emails,
|
||||||
|
$externalId,
|
||||||
|
$userName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
*
|
||||||
|
* @param bool $active
|
||||||
|
* @param string $displayName
|
||||||
|
* @param array $emails
|
||||||
|
* @return SCIMJSONResponse
|
||||||
|
*/
|
||||||
|
public function update(
|
||||||
|
string $id,
|
||||||
|
bool $active,
|
||||||
|
string $displayName = '',
|
||||||
|
array $emails = []
|
||||||
|
): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
return $this->userService->update($id, $active, $displayName, $emails);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
* @PublicPage
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function destroy(string $id): Response
|
||||||
|
{
|
||||||
|
return $this->userService->destroy($id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,172 +7,109 @@ namespace OCA\SCIMServiceProvider\Controller;
|
||||||
use OCP\AppFramework\ApiController;
|
use OCP\AppFramework\ApiController;
|
||||||
use OCP\AppFramework\Http\Response;
|
use OCP\AppFramework\Http\Response;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\IUserManager;
|
|
||||||
use OCP\Security\ISecureRandom;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
|
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
|
||||||
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
|
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
|
||||||
use OCA\SCIMServiceProvider\Responses\SCIMErrorResponse;
|
use OCA\SCIMServiceProvider\Service\UserService;
|
||||||
|
|
||||||
use OCA\SCIMServiceProvider\Service\SCIMUser;
|
class UserController extends ApiController
|
||||||
|
{
|
||||||
|
/** @var UserService */
|
||||||
|
private $userService;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $appName,
|
||||||
|
IRequest $request,
|
||||||
|
UserService $userService
|
||||||
|
) {
|
||||||
|
parent::__construct(
|
||||||
|
$appName,
|
||||||
|
$request
|
||||||
|
);
|
||||||
|
|
||||||
class UserController extends ApiController {
|
$this->userService = $userService;
|
||||||
|
}
|
||||||
|
|
||||||
/** @var LoggerInterface */
|
/**
|
||||||
private $logger;
|
* @NoCSRFRequired
|
||||||
/** @var ISecureRandom */
|
*
|
||||||
private $secureRandom;
|
* @param string $filter
|
||||||
private $SCIMUser;
|
* @return SCIMListResponse
|
||||||
|
* returns a list of users and their data
|
||||||
|
*/
|
||||||
|
public function index(string $filter = ''): SCIMListResponse
|
||||||
|
{
|
||||||
|
return $this->userService->getAll($filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*
|
||||||
|
* gets user info
|
||||||
|
*
|
||||||
|
* @param string $id
|
||||||
|
* @return SCIMJSONResponse
|
||||||
|
*/
|
||||||
|
// TODO: Add filtering support here as well
|
||||||
|
public function show(string $id): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
return $this->userService->getOneById($id);
|
||||||
|
}
|
||||||
|
|
||||||
public function __construct(string $appName,
|
/**
|
||||||
IRequest $request,
|
* @NoCSRFRequired
|
||||||
IUserManager $userManager,
|
*
|
||||||
LoggerInterface $logger,
|
* @param bool $active
|
||||||
ISecureRandom $secureRandom,
|
* @param string $displayName
|
||||||
SCIMUser $SCIMUser) {
|
* @param array $emails
|
||||||
parent::__construct($appName,
|
* @param string $externalId
|
||||||
$request,
|
* @param string $userName
|
||||||
$userManager);
|
* @return SCIMJSONResponse
|
||||||
|
*/
|
||||||
|
public function create(
|
||||||
|
bool $active = true,
|
||||||
|
string $displayName = '',
|
||||||
|
array $emails = [],
|
||||||
|
string $externalId = '',
|
||||||
|
string $userName = ''
|
||||||
|
): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
return $this->userService->create(
|
||||||
|
$active,
|
||||||
|
$displayName,
|
||||||
|
$emails,
|
||||||
|
$externalId,
|
||||||
|
$userName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$this->logger = $logger;
|
/**
|
||||||
$this->secureRandom = $secureRandom;
|
* @NoCSRFRequired
|
||||||
$this->SCIMUser = $SCIMUser;
|
*
|
||||||
$this->userManager = $userManager;
|
* @param string $id
|
||||||
}
|
*
|
||||||
|
* @param bool $active
|
||||||
|
* @param string $displayName
|
||||||
|
* @param array $emails
|
||||||
|
* @return SCIMJSONResponse
|
||||||
|
*/
|
||||||
|
public function update(
|
||||||
|
string $id,
|
||||||
|
bool $active,
|
||||||
|
string $displayName = '',
|
||||||
|
array $emails = []
|
||||||
|
): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
return $this->userService->update($id, $active, $displayName, $emails);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoCSRFRequired
|
* @NoCSRFRequired
|
||||||
*
|
*
|
||||||
* returns a list of users and their data
|
* @param string $id
|
||||||
*/
|
* @return Response
|
||||||
public function index(): SCIMListResponse {
|
*/
|
||||||
$users = [];
|
public function destroy(string $id): Response
|
||||||
$users = $this->userManager->search('', null, 0);
|
{
|
||||||
$userIds = array_keys($users);
|
return $this->userService->destroy($id);
|
||||||
|
}
|
||||||
$SCIMUsers = array();
|
|
||||||
foreach ($userIds as $userId) {
|
|
||||||
$userId = (string) $userId;
|
|
||||||
$SCIMUser = $this->SCIMUser->get($userId);
|
|
||||||
// Do not insert empty entry
|
|
||||||
if (!empty($SCIMUser)) {
|
|
||||||
$SCIMUsers[] = $SCIMUser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SCIMListResponse($SCIMUsers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @NoCSRFRequired
|
|
||||||
*
|
|
||||||
* gets user info
|
|
||||||
*
|
|
||||||
* @param string $id
|
|
||||||
* @return SCIMJSONResponse
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function show(string $id): SCIMJSONResponse {
|
|
||||||
$user = $this->SCIMUser->get($id);
|
|
||||||
// getUserData returns empty array if not enough permissions
|
|
||||||
if (empty($user)) {
|
|
||||||
return new SCIMErrorResponse(['message' => 'User not found'], 404);
|
|
||||||
}
|
|
||||||
return new SCIMJSONResponse($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @NoCSRFRequired
|
|
||||||
*
|
|
||||||
* @param bool $active
|
|
||||||
* @param string $displayName
|
|
||||||
* @param array $emails
|
|
||||||
* @param string $externalId
|
|
||||||
* @param string $userName
|
|
||||||
* @return SCIMJSONResponse
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function create(bool $active = true,
|
|
||||||
string $displayName = '',
|
|
||||||
array $emails = [],
|
|
||||||
string $externalId = '',
|
|
||||||
string $userName = ''): SCIMJSONResponse {
|
|
||||||
if ($this->userManager->userExists($userName)) {
|
|
||||||
$this->logger->error('Failed createUser attempt: User already exists.', ['app' => 'SCIMServiceProvider']);
|
|
||||||
return new SCIMErrorResponse(['message' => 'User already exists'], 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$newUser = $this->userManager->createUser($userName, $this->secureRandom->generate(64));
|
|
||||||
$this->logger->info('Successful createUser call with userid: ' . $userName, ['app' => 'SCIMServiceProvider']);
|
|
||||||
foreach ($emails as $email) {
|
|
||||||
$this->logger->error('Log email: ' . $email['value'], ['app' => 'SCIMServiceProvider']);
|
|
||||||
if ($email['primary'] === true) {
|
|
||||||
$newUser->setEMailAddress($email['value']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$newUser->setEnabled($active);
|
|
||||||
$this->SCIMUser->setExternalId($userName, $externalId);
|
|
||||||
return new SCIMJSONResponse($this->SCIMUser->get($userName));
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->logger->warning('Failed createUser attempt with SCIMException exeption.', ['app' => 'SCIMServiceProvider']);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @NoCSRFRequired
|
|
||||||
*
|
|
||||||
* @param string $id
|
|
||||||
*
|
|
||||||
* @param bool $active
|
|
||||||
* @param string $displayName
|
|
||||||
* @param array $emails
|
|
||||||
* @return DataResponse
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function update(string $id,
|
|
||||||
bool $active,
|
|
||||||
string $displayName = '',
|
|
||||||
array $emails = []): SCIMJSONResponse {
|
|
||||||
$targetUser = $this->userManager->get($id);
|
|
||||||
if ($targetUser === null) {
|
|
||||||
return new SCIMErrorResponse(['message' => 'User not found'], 404);
|
|
||||||
}
|
|
||||||
foreach ($emails as $email) {
|
|
||||||
if ($email['primary'] === true) {
|
|
||||||
$targetUser->setEMailAddress($email['value']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isset($active)) {
|
|
||||||
$targetUser->setEnabled($active);
|
|
||||||
}
|
|
||||||
return new SCIMJSONResponse($this->SCIMUser->get($id));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @NoCSRFRequired
|
|
||||||
*
|
|
||||||
* @param string $id
|
|
||||||
* @return DataResponse
|
|
||||||
*/
|
|
||||||
public function destroy(string $id): Response {
|
|
||||||
$targetUser = $this->userManager->get($id);
|
|
||||||
|
|
||||||
if ($targetUser === null) {
|
|
||||||
return new SCIMErrorResponse(['message' => 'User not found'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go ahead with the delete
|
|
||||||
if ($targetUser->delete()) {
|
|
||||||
$response = new Response();
|
|
||||||
$response->setStatus(204);
|
|
||||||
return $response;
|
|
||||||
} else {
|
|
||||||
return new SCIMErrorResponse(['message' => 'Couldn\'t delete user'], 503);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
173
lib/DataAccess/Groups/NextcloudGroupDataAccess.php
Normal file
173
lib/DataAccess/Groups/NextcloudGroupDataAccess.php
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\DataAccess\Groups;
|
||||||
|
|
||||||
|
use OCP\IGroup;
|
||||||
|
use OCP\IGroupManager;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class NextcloudGroupDataAccess
|
||||||
|
{
|
||||||
|
/** @var Psr\Log\LoggerInterface */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
/** @var \OCP\IUserManager */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
|
/** @var \OCP\IGroupManager */
|
||||||
|
private $groupManager;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$this->logger = $container->get(LoggerInterface::class);
|
||||||
|
$this->userManager = $container->get(IUserManager::class);
|
||||||
|
$this->groupManager = $container->get(IGroupManager::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read all groups
|
||||||
|
*/
|
||||||
|
public function getAll(): ?array
|
||||||
|
{
|
||||||
|
$ncGroups = $this->groupManager->search('', null, 0);
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupDataAccess::class . "] fetched " . count($ncGroups) . " groups"
|
||||||
|
);
|
||||||
|
|
||||||
|
return $ncGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a single group by ID
|
||||||
|
*/
|
||||||
|
public function getOneById($id): ?IGroup
|
||||||
|
{
|
||||||
|
$ncGroup = $this->groupManager->get($id);
|
||||||
|
|
||||||
|
if (!isset($ncGroup)) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudGroupDataAccess::class . "] group with ID: " . $id . " is null"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupDataAccess::class . "] fetched group with ID: " . $id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ncGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new group
|
||||||
|
*/
|
||||||
|
public function create($displayName): ?IGroup
|
||||||
|
{
|
||||||
|
// Note: the createGroup() function requires a $gid parameter
|
||||||
|
// However, looking at the NC DB, it seems that the gid of a group
|
||||||
|
// and its displayName can have the same value, hence here we pass the
|
||||||
|
// displayName parameter to createGroup() and don't need to generate
|
||||||
|
// a unique gid for a given group during creation
|
||||||
|
$createdNcGroup = $this->groupManager->createGroup($displayName);
|
||||||
|
|
||||||
|
if (!isset($createdNcGroup)) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudGroupDataAccess::class . "] creation of group with displayName: " . $displayName . " failed"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $createdNcGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing group by ID
|
||||||
|
*
|
||||||
|
* Note: here, we pass the second parameter, since it carries the data to be updated
|
||||||
|
* and we need to pass this data to the group that is to be updated
|
||||||
|
*/
|
||||||
|
public function update(string $id, IGroup $newGroupData): ?IGroup
|
||||||
|
{
|
||||||
|
$ncGroupToUpdate = $this->groupManager->get($id);
|
||||||
|
|
||||||
|
if (!isset($ncGroupToUpdate)) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudGroupDataAccess::class . "] group to be updated with ID: " . $id . " doesn't exist"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newGroupData->getDisplayName() !== null) {
|
||||||
|
$ncGroupToUpdate->setDisplayName($newGroupData->getDisplayName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newGroupData->getUsers() !== null && !empty($newGroupData->getUsers())) {
|
||||||
|
$newNcGroupMembers = [];
|
||||||
|
|
||||||
|
foreach ($newGroupData->getUsers() as $newNcGroupMember) {
|
||||||
|
// First check if the user is an existing one and only then try to place it as a member of the group
|
||||||
|
if ($this->userManager->userExists($newNcGroupMember->getUID())) {
|
||||||
|
$ncUserToAdd = $this->userManager->get($newNcGroupMember->getUID());
|
||||||
|
$newNcGroupMembers[] = $ncUserToAdd;
|
||||||
|
} else {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudGroupDataAccess::class . "] user from new group data with ID: " . $id . " doesn't exist"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentNcGroupMembers = $ncGroupToUpdate->getUsers();
|
||||||
|
if (isset($currentNcGroupMembers) && !empty($currentNcGroupMembers)) {
|
||||||
|
// If the group can't remove users from itself, then we abort and return null
|
||||||
|
if (!$ncGroupToUpdate->canRemoveUser()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, if we can remove users, then we remove all current users
|
||||||
|
foreach ($currentNcGroupMembers as $currentNcGroupMember) {
|
||||||
|
$ncGroupToUpdate->removeUser($currentNcGroupMember);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After having deleted the current members, we try to replace them with the new ones
|
||||||
|
if (!$ncGroupToUpdate->canAddUser()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($newNcGroupMembers as $newNcGroupMember) {
|
||||||
|
$ncGroupToUpdate->addUser($newNcGroupMember);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the now updated NC group
|
||||||
|
return $this->groupManager->get($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing group by ID
|
||||||
|
*/
|
||||||
|
public function delete($id): bool
|
||||||
|
{
|
||||||
|
$ncGroupToDelete = $this->groupManager->get($id);
|
||||||
|
|
||||||
|
if (!isset($ncGroupToDelete)) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudGroupDataAccess::class . "] group to be deleted with ID: " . $id . " doesn't exist"
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ncGroupToDelete->delete()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudGroupDataAccess::class . "] couldn't delete group with ID: " . $id
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
144
lib/DataAccess/Users/NextcloudUserDataAccess.php
Normal file
144
lib/DataAccess/Users/NextcloudUserDataAccess.php
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\DataAccess\Users;
|
||||||
|
|
||||||
|
use OCP\IConfig;
|
||||||
|
use OCP\IUser;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use OCP\Security\ISecureRandom;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class NextcloudUserDataAccess
|
||||||
|
{
|
||||||
|
/** @var Psr\Log\LoggerInterface */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
/** @var \OCP\IUserManager */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
|
/** @var \OCP\Security\ISecureRandom */
|
||||||
|
private $secureRandom;
|
||||||
|
|
||||||
|
/** @var IConfig */
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$this->logger = $container->get(LoggerInterface::class);
|
||||||
|
$this->secureRandom = $container->get(ISecureRandom::class);
|
||||||
|
$this->userManager = $container->get(IUserManager::class);
|
||||||
|
$this->config = $container->get(IConfig::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read all users
|
||||||
|
*/
|
||||||
|
public function getAll(): ?array
|
||||||
|
{
|
||||||
|
$ncUsers = $this->userManager->search('', null, 0);
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserDataAccess::class . "] fetched " . count($ncUsers) . " users"
|
||||||
|
);
|
||||||
|
|
||||||
|
return $ncUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a single user by ID
|
||||||
|
*/
|
||||||
|
public function getOneById($id): ?IUser
|
||||||
|
{
|
||||||
|
$ncUser = $this->userManager->get($id);
|
||||||
|
|
||||||
|
if (!isset($ncUser)) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudUserDataAccess::class . "] user with ID: " . $id . " is null"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserDataAccess::class . "] fetched user with ID: " . $id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ncUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user
|
||||||
|
*/
|
||||||
|
public function create($username): ?IUser
|
||||||
|
{
|
||||||
|
$createdNcUser = $this->userManager->createUser($username, $this->secureRandom->generate(64));
|
||||||
|
|
||||||
|
if ($createdNcUser === false) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudUserDataAccess::class . "] creation of user with userName: " . $username . " failed"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $createdNcUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing user by ID
|
||||||
|
*
|
||||||
|
* Note: here, we pass the second parameter, since it carries the data to be updated
|
||||||
|
* and we need to pass this data to the user that is to be updated
|
||||||
|
*/
|
||||||
|
public function update(string $id, IUser $newUserData): ?IUser
|
||||||
|
{
|
||||||
|
$ncUserToUpdate = $this->userManager->get($id);
|
||||||
|
|
||||||
|
if ($ncUserToUpdate === null) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudUserDataAccess::class . "] user to be updated with ID: " . $id . " doesn't exist"
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newUserData->getDisplayName() !== null) {
|
||||||
|
$ncUserToUpdate->setDisplayName($newUserData->getDisplayName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newUserData->isEnabled() !== null && $newUserData->isEnabled()) {
|
||||||
|
$ncUserToUpdate->setEnabled($newUserData->isEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newUserData->getEMailAddress() !== null && !empty($newUserData->getEMailAddress())) {
|
||||||
|
$ncUserToUpdate->setEMailAddress($newUserData->getEMailAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the now updated NC user
|
||||||
|
return $this->userManager->get($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing user by ID
|
||||||
|
*/
|
||||||
|
public function delete($id): bool
|
||||||
|
{
|
||||||
|
$ncUserToDelete = $this->userManager->get($id);
|
||||||
|
|
||||||
|
if ($ncUserToDelete === null) {
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudUserDataAccess::class . "] user to be deleted with ID: " . $id . " doesn't exist"
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ncUserToDelete->delete()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudUserDataAccess::class . "] couldn't delete user with ID: " . $id
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
9
lib/Exception/AuthException.php
Normal file
9
lib/Exception/AuthException.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class AuthException extends Exception
|
||||||
|
{
|
||||||
|
}
|
9
lib/Exception/ContentTypeException.php
Normal file
9
lib/Exception/ContentTypeException.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class ContentTypeException extends Exception
|
||||||
|
{
|
||||||
|
}
|
70
lib/Middleware/BearerAuthMiddleware.php
Normal file
70
lib/Middleware/BearerAuthMiddleware.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Middleware;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use OCA\SCIMServiceProvider\Controller\UserController;
|
||||||
|
use OCA\SCIMServiceProvider\Exception\AuthException;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMErrorResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Util\Authentication\BearerAuthenticator;
|
||||||
|
use OCP\AppFramework\Middleware;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
|
||||||
|
class BearerAuthMiddleware extends Middleware
|
||||||
|
{
|
||||||
|
/** @var IRequest */
|
||||||
|
private IRequest $request;
|
||||||
|
|
||||||
|
/** @var \OCA\SCIMServiceProvider\Util\Authentication\BearerAuthenticator */
|
||||||
|
private BearerAuthenticator $bearerAuthenticator;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$this->request = $container->get(IRequest::class);
|
||||||
|
$this->bearerAuthenticator = $container->get(BearerAuthenticator::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function beforeController($controller, $methodName)
|
||||||
|
{
|
||||||
|
$currentRoute = $this->request->getParams()["_route"];
|
||||||
|
$publicRoutes = [
|
||||||
|
"scimserviceprovider.service_provider_configuration.resource_types",
|
||||||
|
"scimserviceprovider.service_provider_configuration.schemas",
|
||||||
|
"scimserviceprovider.service_provider_configuration.service_provider_config"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Don't require an auth header for public routes
|
||||||
|
if (in_array($currentRoute, $publicRoutes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authHeader = $this->request->getHeader('Authorization');
|
||||||
|
|
||||||
|
if (empty($authHeader)) {
|
||||||
|
throw new AuthException("No Authorization header supplied");
|
||||||
|
}
|
||||||
|
|
||||||
|
$authHeaderSplit = explode(' ', $authHeader);
|
||||||
|
if (count($authHeaderSplit) !== 2 || strcmp($authHeaderSplit[0], "Bearer") !== 0) {
|
||||||
|
throw new AuthException("Incorrect Bearer token format");
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $authHeaderSplit[1];
|
||||||
|
|
||||||
|
// Currently the second parameter to authenticate() is an empty array
|
||||||
|
// (the second parameter is meant to carry authorization information)
|
||||||
|
if (!$this->bearerAuthenticator->authenticate($token, [])) {
|
||||||
|
throw new AuthException("Bearer token is invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterException($controller, $methodName, Exception $exception)
|
||||||
|
{
|
||||||
|
if ($exception instanceof AuthException) {
|
||||||
|
return new SCIMErrorResponse(['message' => $exception->getMessage()], 401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
60
lib/Middleware/ContentTypeMiddleware.php
Normal file
60
lib/Middleware/ContentTypeMiddleware.php
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Middleware;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use OCA\SCIMServiceProvider\Exception\ContentTypeException;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMErrorResponse;
|
||||||
|
use OCP\AppFramework\Middleware;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
|
||||||
|
class ContentTypeMiddleware extends Middleware
|
||||||
|
{
|
||||||
|
/** @var IRequest */
|
||||||
|
private $request;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$this->request = $container->get(IRequest::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function beforeController($controller, $methodName)
|
||||||
|
{
|
||||||
|
$requestMethod = $this->request->getMethod();
|
||||||
|
|
||||||
|
// If the incoming request is POST or PUT => check the Content-Type header and the request body
|
||||||
|
if (in_array(strtolower($requestMethod), array("post", "put"))) {
|
||||||
|
$contentTypeHeader = $this->request->getHeader("Content-Type");
|
||||||
|
if (!isset($contentTypeHeader) || empty($contentTypeHeader)) {
|
||||||
|
throw new ContentTypeException("Content-Type header not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept both "application/scim+json" and "application/json" as valid headers
|
||||||
|
// See https://www.rfc-editor.org/rfc/rfc7644.html#section-3.8
|
||||||
|
if (
|
||||||
|
strpos($contentTypeHeader, "application/scim+json") === false
|
||||||
|
&& strpos($contentTypeHeader, "application/json") === false
|
||||||
|
) {
|
||||||
|
throw new ContentTypeException("Content-Type header is not application/scim+json or application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the request body is indeed valid JSON
|
||||||
|
$requestBody = $this->request->getParams();
|
||||||
|
if (isset($requestBody) && !empty($requestBody)) {
|
||||||
|
$requestBody = array_keys($requestBody)[0];
|
||||||
|
|
||||||
|
if (json_decode($requestBody) === false) {
|
||||||
|
throw new ContentTypeException("Request body is not valid JSON");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterException($controller, $methodName, Exception $exception)
|
||||||
|
{
|
||||||
|
return new SCIMErrorResponse(['message' => $exception->getMessage()], 400);
|
||||||
|
}
|
||||||
|
}
|
164
lib/Repositories/Groups/NextcloudGroupRepository.php
Normal file
164
lib/Repositories/Groups/NextcloudGroupRepository.php
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Repositories\Groups;
|
||||||
|
|
||||||
|
use Opf\Models\SCIM\Standard\Groups\CoreGroup;
|
||||||
|
use Opf\Repositories\Repository;
|
||||||
|
use Opf\Util\Filters\FilterUtil;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class NextcloudGroupRepository extends Repository
|
||||||
|
{
|
||||||
|
/** @var Psr\Log\LoggerInterface */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
parent::__construct($container);
|
||||||
|
$this->dataAccess = $container->get('GroupDataAccess');
|
||||||
|
$this->adapter = $container->get('GroupAdapter');
|
||||||
|
$this->logger = $container->get(LoggerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read all groups in SCIM format
|
||||||
|
*/
|
||||||
|
public function getAll(
|
||||||
|
$filter = '',
|
||||||
|
$startIndex = 0,
|
||||||
|
$count = 0,
|
||||||
|
$attributes = [],
|
||||||
|
$excludedAttributes = []
|
||||||
|
): array {
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupRepository::class . "] reading all groups"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Read all NC groups
|
||||||
|
$ncGroups = $this->dataAccess->getAll();
|
||||||
|
$scimGroups = [];
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupRepository::class . "] fetched " . count($ncGroups) . " NC groups"
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($ncGroups as $ncGroup) {
|
||||||
|
$scimGroup = $this->adapter->getCoreGroup($ncGroup);
|
||||||
|
$scimGroups[] = $scimGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupRepository::class . "] transformed " . count($scimGroups) . " SCIM groups"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($filter) && !empty($filter)) {
|
||||||
|
$scimGroupsToFilter = [];
|
||||||
|
foreach ($scimGroups as $scimGroup) {
|
||||||
|
$scimGroupsToFilter[] = $scimGroup->toSCIM(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filteredScimData = FilterUtil::performFiltering($filter, $scimGroupsToFilter);
|
||||||
|
|
||||||
|
$scimGroups = [];
|
||||||
|
foreach ($filteredScimData as $filteredScimGroup) {
|
||||||
|
$scimGroup = new CoreGroup();
|
||||||
|
$scimGroup->fromSCIM($filteredScimGroup);
|
||||||
|
$scimGroups[] = $scimGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scimGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scimGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a single group by ID in SCIM format
|
||||||
|
*/
|
||||||
|
public function getOneById(
|
||||||
|
string $id,
|
||||||
|
$filter = '',
|
||||||
|
$startIndex = 0,
|
||||||
|
$count = 0,
|
||||||
|
$attributes = [],
|
||||||
|
$excludedAttributes = []
|
||||||
|
): ?CoreGroup {
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupRepository::class . "] reading group with ID: " . $id
|
||||||
|
);
|
||||||
|
|
||||||
|
$ncGroup = $this->dataAccess->getOneById($id);
|
||||||
|
return $this->adapter->getCoreGroup($ncGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a group from SCIM data
|
||||||
|
*/
|
||||||
|
public function create($object): ?CoreGroup
|
||||||
|
{
|
||||||
|
$scimGroupToCreate = new CoreGroup();
|
||||||
|
$scimGroupToCreate->fromSCIM($object);
|
||||||
|
|
||||||
|
$displayName = $scimGroupToCreate->getDisplayName();
|
||||||
|
$ncGroupCreated = $this->dataAccess->create($displayName);
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupRepository::class . "] creating group with displayName: " . $displayName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($ncGroupCreated)) {
|
||||||
|
// Set the rest of the properties of the NC group with the adapter
|
||||||
|
$ncGroupCreated = $this->adapter->getNCGroup($scimGroupToCreate, $ncGroupCreated);
|
||||||
|
return $this->adapter->getCoreGroup($ncGroupCreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudGroupRepository::class . "] creation of group with displayName: " . $displayName . " failed"
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a group by ID from SCIM data
|
||||||
|
*/
|
||||||
|
public function update(string $id, $object): ?CoreGroup
|
||||||
|
{
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupRepository::class . "] updating group with ID: " . $id
|
||||||
|
);
|
||||||
|
|
||||||
|
$scimGroupToUpdate = new CoreGroup();
|
||||||
|
$scimGroupToUpdate->fromSCIM($object);
|
||||||
|
|
||||||
|
$ncGroup = $this->dataAccess->getOneById($id);
|
||||||
|
|
||||||
|
if (isset($ncGroup)) {
|
||||||
|
$ncGroupToUpdate = $this->adapter->getNCGroup($scimGroupToUpdate, $ncGroup);
|
||||||
|
$ncGroupUpdated = $this->dataAccess->update($id, $ncGroupToUpdate);
|
||||||
|
|
||||||
|
if (isset($ncGroupUpdated)) {
|
||||||
|
return $this->adapter->getCoreGroup($ncGroupUpdated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudGroupRepository::class . "] update of group with ID: " . $id . " failed"
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a group by ID
|
||||||
|
*/
|
||||||
|
public function delete(string $id): bool
|
||||||
|
{
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudGroupRepository::class . "] deleting group with ID: " . $id
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->dataAccess->delete($id);
|
||||||
|
}
|
||||||
|
}
|
177
lib/Repositories/Users/NextcloudUserRepository.php
Normal file
177
lib/Repositories/Users/NextcloudUserRepository.php
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Repositories\Users;
|
||||||
|
|
||||||
|
use Opf\Models\SCIM\Standard\Users\CoreUser;
|
||||||
|
use Opf\Repositories\Repository;
|
||||||
|
use Opf\Util\Filters\FilterUtil;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class NextcloudUserRepository extends Repository
|
||||||
|
{
|
||||||
|
/** @var Psr\Log\LoggerInterface */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
parent::__construct($container);
|
||||||
|
$this->dataAccess = $container->get('UserDataAccess');
|
||||||
|
$this->adapter = $container->get('UserAdapter');
|
||||||
|
$this->logger = $container->get(LoggerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read all users in SCIM format
|
||||||
|
*/
|
||||||
|
public function getAll(
|
||||||
|
$filter = '',
|
||||||
|
$startIndex = 0,
|
||||||
|
$count = 0,
|
||||||
|
$attributes = [],
|
||||||
|
$excludedAttributes = []
|
||||||
|
): array {
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserRepository::class . "] reading all users"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Read all NC users
|
||||||
|
$ncUsers = $this->dataAccess->getAll();
|
||||||
|
$scimUsers = [];
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserRepository::class . "] fetched " . count($ncUsers) . " NC users"
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($ncUsers as $ncUser) {
|
||||||
|
$scimUser = $this->adapter->getCoreUser($ncUser);
|
||||||
|
$scimUsers[] = $scimUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserRepository::class . "] transformed " . count($scimUsers) . " SCIM users"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($filter) && !empty($filter)) {
|
||||||
|
$scimUsersToFilter = [];
|
||||||
|
foreach ($scimUsers as $scimUser) {
|
||||||
|
$scimUsersToFilter[] = $scimUser->toSCIM(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filteredScimData = FilterUtil::performFiltering($filter, $scimUsersToFilter);
|
||||||
|
|
||||||
|
$scimUsers = [];
|
||||||
|
foreach ($filteredScimData as $filteredScimUser) {
|
||||||
|
$scimUser = new CoreUser();
|
||||||
|
$scimUser->fromSCIM($filteredScimUser);
|
||||||
|
$scimUsers[] = $scimUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scimUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scimUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a single user by ID in SCIM format
|
||||||
|
*/
|
||||||
|
public function getOneById(
|
||||||
|
string $id,
|
||||||
|
$filter = '',
|
||||||
|
$startIndex = 0,
|
||||||
|
$count = 0,
|
||||||
|
$attributes = [],
|
||||||
|
$excludedAttributes = []
|
||||||
|
): ?CoreUser {
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserRepository::class . "] reading user with ID: " . $id
|
||||||
|
);
|
||||||
|
|
||||||
|
$ncUser = $this->dataAccess->getOneById($id);
|
||||||
|
$scimUser = $this->adapter->getCoreUser($ncUser);
|
||||||
|
|
||||||
|
if (isset($filter) && !empty($filter)) {
|
||||||
|
$scimUsersToFilter = array($scimUser->toSCIM(false));
|
||||||
|
$filteredScimData = FilterUtil::performFiltering($filter, $scimUsersToFilter);
|
||||||
|
|
||||||
|
if (!empty($filteredScimData)) {
|
||||||
|
$scimUser = new CoreUser();
|
||||||
|
$scimUser->fromSCIM($filteredScimData[0]);
|
||||||
|
return $scimUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scimUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a user from SCIM data
|
||||||
|
*/
|
||||||
|
public function create($object): ?CoreUser
|
||||||
|
{
|
||||||
|
$scimUserToCreate = new CoreUser();
|
||||||
|
$scimUserToCreate->fromSCIM($object);
|
||||||
|
|
||||||
|
$username = $scimUserToCreate->getUserName();
|
||||||
|
$ncUserCreated = $this->dataAccess->create($username);
|
||||||
|
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserRepository::class . "] creating user with userName: " . $username
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($ncUserCreated)) {
|
||||||
|
// Set the rest of the properties of the NC user via the adapter
|
||||||
|
$ncUserCreated = $this->adapter->getNCUser($scimUserToCreate, $ncUserCreated);
|
||||||
|
return $this->adapter->getCoreUser($ncUserCreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudUserRepository::class . "] creation of user with username: " . $username . " failed"
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a user by ID from SCIM data
|
||||||
|
*/
|
||||||
|
public function update(string $id, $object): ?CoreUser
|
||||||
|
{
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserRepository::class . "] updating user with ID: " . $id
|
||||||
|
);
|
||||||
|
|
||||||
|
$scimUserToUpdate = new CoreUser();
|
||||||
|
$scimUserToUpdate->fromSCIM($object);
|
||||||
|
|
||||||
|
$ncUser = $this->dataAccess->getOneById($id);
|
||||||
|
|
||||||
|
if (isset($ncUser)) {
|
||||||
|
$ncUserToUpdate = $this->adapter->getNCUser($scimUserToUpdate, $ncUser);
|
||||||
|
$ncUserUpdated = $this->dataAccess->update($id, $ncUserToUpdate);
|
||||||
|
|
||||||
|
if (isset($ncUserUpdated)) {
|
||||||
|
return $this->adapter->getCoreUser($ncUserUpdated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->error(
|
||||||
|
"[" . NextcloudUserRepository::class . "] update of user with ID: " . $id . " failed"
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a user by ID
|
||||||
|
*/
|
||||||
|
public function delete(string $id): bool
|
||||||
|
{
|
||||||
|
$this->logger->info(
|
||||||
|
"[" . NextcloudUserRepository::class . "] deleting user with ID: " . $id
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->dataAccess->delete($id);
|
||||||
|
}
|
||||||
|
}
|
157
lib/Service/GroupService.php
Normal file
157
lib/Service/GroupService.php
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Service;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMErrorResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Util\Util;
|
||||||
|
use OCP\AppFramework\Http\Response;
|
||||||
|
use OCP\IGroupManager;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class GroupService
|
||||||
|
{
|
||||||
|
/** @var LoggerInterface */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
/** @var \OCA\SCIMServiceProvider\Repositories\Groups\NextcloudGroupRepository */
|
||||||
|
private $repository;
|
||||||
|
|
||||||
|
/** @var IGroupManager */
|
||||||
|
private $groupManager;
|
||||||
|
|
||||||
|
/** @var IRequest */
|
||||||
|
private $request;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$this->logger = $container->get(LoggerInterface::class);
|
||||||
|
$this->repository = $container->get('GroupRepository');
|
||||||
|
$this->groupManager = $container->get(IGroupManager::class);
|
||||||
|
$this->request = $container->get(IRequest::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAll(string $filter = ''): SCIMListResponse
|
||||||
|
{
|
||||||
|
$this->logger->info("Reading all groups");
|
||||||
|
|
||||||
|
$baseUrl = $this->request->getServerProtocol() . "://"
|
||||||
|
. $this->request->getServerHost() . Util::SCIM_APP_URL_PATH;
|
||||||
|
|
||||||
|
$groups = $this->repository->getAll($filter);
|
||||||
|
|
||||||
|
$scimGroups = [];
|
||||||
|
if (!empty($groups)) {
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
$scimGroups[] = $group->toSCIM(false, $baseUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SCIMListResponse($scimGroups);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOneById(string $id): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
$this->logger->info("Reading group with ID: " . $id);
|
||||||
|
|
||||||
|
$baseUrl = $this->request->getServerProtocol() . "://" . $this->request->getServerHost() . Util::SCIM_APP_URL_PATH;
|
||||||
|
|
||||||
|
$group = $this->repository->getOneById($id);
|
||||||
|
if (!isset($group) || empty($group)) {
|
||||||
|
$this->logger->error("Group with ID " . $id . " not found");
|
||||||
|
return new SCIMErrorResponse(['message' => 'Group not found'], 404);
|
||||||
|
}
|
||||||
|
return new SCIMJSONResponse($group->toSCIM(false, $baseUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(string $displayName = '', array $members = []): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
$id = urlencode($displayName);
|
||||||
|
// Validate name
|
||||||
|
if (empty($id)) {
|
||||||
|
$this->logger->error('Group name not supplied', ['app' => 'provisioning_api']);
|
||||||
|
return new SCIMErrorResponse(['message' => 'Invalid group name'], 400);
|
||||||
|
}
|
||||||
|
// Check if it exists
|
||||||
|
if ($this->groupManager->groupExists($id)) {
|
||||||
|
$this->logger->error("Group to be created already exists");
|
||||||
|
return new SCIMErrorResponse(['message' => 'Group exists'], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->logger->info("Creating group with displayName: " . $displayName);
|
||||||
|
|
||||||
|
$baseUrl = $this->request->getServerProtocol() . "://" . $this->request->getServerHost() . Util::SCIM_APP_URL_PATH;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'displayName' => $displayName,
|
||||||
|
'members' => $members
|
||||||
|
];
|
||||||
|
|
||||||
|
$createdGroup = $this->repository->create($data);
|
||||||
|
if (isset($createdGroup) && !empty($createdGroup)) {
|
||||||
|
return new SCIMJSONResponse($createdGroup->toSCIM(false, $baseUrl), 201);
|
||||||
|
} else {
|
||||||
|
$this->logger->error("Creating group failed");
|
||||||
|
return new SCIMErrorResponse(['message' => 'Creating group failed'], 400);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->logger->warning('Failed createGroup attempt with SCIMException exception.', ['app' => 'SCIMServiceProvider']);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(string $id, string $displayName = '', array $members = []): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
$this->logger->info("Updating group with ID: " . $id);
|
||||||
|
|
||||||
|
$baseUrl = $this->request->getServerProtocol() . "://" . $this->request->getServerHost() . Util::SCIM_APP_URL_PATH;
|
||||||
|
|
||||||
|
$group = $this->repository->getOneById($id);
|
||||||
|
if (!isset($group) || empty($group)) {
|
||||||
|
$this->logger->error("Group with ID " . $id . " not found for update");
|
||||||
|
return new SCIMErrorResponse(['message' => 'Group not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'displayName' => $displayName,
|
||||||
|
'members' => $members
|
||||||
|
];
|
||||||
|
|
||||||
|
$updatedGroup = $this->repository->update($id, $data);
|
||||||
|
if (isset($updatedGroup) && !empty($updatedGroup)) {
|
||||||
|
return new SCIMJSONResponse($updatedGroup->toSCIM(false, $baseUrl));
|
||||||
|
} else {
|
||||||
|
$this->logger->error("Updating group with ID " . $id . " failed");
|
||||||
|
return new SCIMErrorResponse(['message' => 'Updating group failed'], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(string $id): Response
|
||||||
|
{
|
||||||
|
$this->logger->info("Deleting group with ID: " . $id);
|
||||||
|
|
||||||
|
if ($id === 'admin') {
|
||||||
|
// Cannot delete admin group
|
||||||
|
$this->logger->error("Deleting admin group is not allowed");
|
||||||
|
return new SCIMErrorResponse(['message' => 'Can\'t delete admin group'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleteRes = $this->repository->delete($id);
|
||||||
|
|
||||||
|
if ($deleteRes) {
|
||||||
|
$response = new Response();
|
||||||
|
$response->setStatus(204);
|
||||||
|
return $response;
|
||||||
|
} else {
|
||||||
|
$this->logger->error("Deletion of group with ID " . $id . " failed");
|
||||||
|
return new SCIMErrorResponse(['message' => 'Couldn\'t delete group'], 503);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
149
lib/Service/UserService.php
Normal file
149
lib/Service/UserService.php
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Service;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMErrorResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
|
||||||
|
use OCA\SCIMServiceProvider\Util\Util;
|
||||||
|
use OCP\AppFramework\Http\Response;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class UserService
|
||||||
|
{
|
||||||
|
/** @var LoggerInterface */
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
/** @var \OCA\SCIMServiceProvider\Repositories\Users\NextcloudUserRepository */
|
||||||
|
private $repository;
|
||||||
|
|
||||||
|
/** @var IRequest */
|
||||||
|
private $request;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$this->logger = $container->get(LoggerInterface::class);
|
||||||
|
$this->repository = $container->get('UserRepository');
|
||||||
|
$this->request = $container->get(IRequest::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAll(string $filter = ''): SCIMListResponse
|
||||||
|
{
|
||||||
|
$this->logger->info("Reading all users");
|
||||||
|
|
||||||
|
$baseUrl = $this->request->getServerProtocol() . "://"
|
||||||
|
. $this->request->getServerHost() . Util::SCIM_APP_URL_PATH;
|
||||||
|
|
||||||
|
$users = $this->repository->getAll($filter);
|
||||||
|
|
||||||
|
$scimUsers = [];
|
||||||
|
if (!empty($users)) {
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$scimUsers[] = $user->toSCIM(false, $baseUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SCIMListResponse($scimUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOneById(string $id): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
$this->logger->info("Reading user with ID: " . $id);
|
||||||
|
|
||||||
|
$baseUrl = $this->request->getServerProtocol() . "://" . $this->request->getServerHost() . Util::SCIM_APP_URL_PATH;
|
||||||
|
|
||||||
|
$user = $this->repository->getOneById($id);
|
||||||
|
if (!isset($user) || empty($user)) {
|
||||||
|
$this->logger->error("User with ID " . $id . " not found");
|
||||||
|
return new SCIMErrorResponse(['message' => 'User not found'], 404);
|
||||||
|
}
|
||||||
|
return new SCIMJSONResponse($user->toSCIM(false, $baseUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(
|
||||||
|
bool $active = true,
|
||||||
|
string $displayName = '',
|
||||||
|
array $emails = [],
|
||||||
|
string $externalId = '',
|
||||||
|
string $userName = ''
|
||||||
|
): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->logger->info("Creating user with userName: " . $userName);
|
||||||
|
|
||||||
|
$baseUrl = $this->request->getServerProtocol() . "://" . $this->request->getServerHost() . Util::SCIM_APP_URL_PATH;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'active' => $active,
|
||||||
|
'displayName' => $displayName,
|
||||||
|
'emails' => $emails,
|
||||||
|
'externalId' => $externalId,
|
||||||
|
'userName' => $userName
|
||||||
|
];
|
||||||
|
|
||||||
|
$createdUser = $this->repository->create($data);
|
||||||
|
if (isset($createdUser) && !empty($createdUser)) {
|
||||||
|
return new SCIMJSONResponse($createdUser->toSCIM(false, $baseUrl), 201);
|
||||||
|
} else {
|
||||||
|
$this->logger->error("Creating user failed");
|
||||||
|
return new SCIMErrorResponse(['message' => 'Creating user failed'], 400);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->logger->warning('Failed createUser attempt with SCIMException exeption.', ['app' => 'SCIMServiceProvider']);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(
|
||||||
|
string $id,
|
||||||
|
bool $active,
|
||||||
|
string $displayName = '',
|
||||||
|
array $emails = []
|
||||||
|
): SCIMJSONResponse
|
||||||
|
{
|
||||||
|
$this->logger->info("Updating user with ID: " . $id);
|
||||||
|
|
||||||
|
$baseUrl = $this->request->getServerProtocol() . "://" . $this->request->getServerHost() . Util::SCIM_APP_URL_PATH;
|
||||||
|
|
||||||
|
$user = $this->repository->getOneById($id);
|
||||||
|
if (!isset($user) || empty($user)) {
|
||||||
|
$this->logger->error("User with ID " . $id . " not found for update");
|
||||||
|
return new SCIMErrorResponse(['message' => 'User not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'active' => $active,
|
||||||
|
'displayName' => $displayName,
|
||||||
|
'emails' => $emails
|
||||||
|
];
|
||||||
|
|
||||||
|
$updatedUser = $this->repository->update($id, $data);
|
||||||
|
if (isset($updatedUser) && !empty($updatedUser)) {
|
||||||
|
return new SCIMJSONResponse($updatedUser->toSCIM(false, $baseUrl));
|
||||||
|
} else {
|
||||||
|
$this->logger->error("Updating user with ID " . $id . " failed");
|
||||||
|
return new SCIMErrorResponse(['message' => 'Updating user failed'], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(string $id): Response
|
||||||
|
{
|
||||||
|
$this->logger->info("Deleting user with ID: " . $id);
|
||||||
|
|
||||||
|
$deleteRes = $this->repository->delete($id);
|
||||||
|
|
||||||
|
if ($deleteRes) {
|
||||||
|
$response = new Response();
|
||||||
|
$response->setStatus(204);
|
||||||
|
return $response;
|
||||||
|
} else {
|
||||||
|
$this->logger->error("Deletion of user with ID " . $id . " failed");
|
||||||
|
return new SCIMErrorResponse(['message' => 'Couldn\'t delete user'], 503);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
lib/Util/Authentication/BearerAuthenticator.php
Normal file
56
lib/Util/Authentication/BearerAuthenticator.php
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Util\Authentication;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Opf\ScimServerPhp\Firebase\JWT\JWT;
|
||||||
|
use Opf\ScimServerPhp\Firebase\JWT\Key;
|
||||||
|
use OCA\SCIMServiceProvider\Util\Util;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use Opf\Util\Authentication\AuthenticatorInterface;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
class BearerAuthenticator implements AuthenticatorInterface
|
||||||
|
{
|
||||||
|
/** @var \Psr\Log\LoggerInterface */
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
|
/** @var \OCP\IUserManager */
|
||||||
|
private IUserManager $userManager;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$this->logger = $container->get(LoggerInterface::class);
|
||||||
|
$this->userManager = $container->get(IUserManager::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(string $credentials, array $authorizationInfo): bool
|
||||||
|
{
|
||||||
|
$jwtPayload = [];
|
||||||
|
$jwtSecret = Util::getConfigFile()['jwt']['secret'];
|
||||||
|
try {
|
||||||
|
$jwtPayload = (array) JWT::decode($credentials, new Key($jwtSecret, 'HS256'));
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->logger->error($e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the 'user' claim is missing from the JWT, then auth is considered to have failed
|
||||||
|
if (!isset($jwtPayload['user']) || empty($jwtPayload['user'])) {
|
||||||
|
$this->logger->error("No \"user\" claim found in JWT");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = $jwtPayload['user'];
|
||||||
|
|
||||||
|
// If we managed to find a user with that username, then auth succeeded
|
||||||
|
$user = $this->userManager->get($username);
|
||||||
|
if ($user !== null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->error("User with this username doesn't exist");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
16
lib/Util/Util.php
Normal file
16
lib/Util/Util.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\SCIMServiceProvider\Util;
|
||||||
|
|
||||||
|
class Util
|
||||||
|
{
|
||||||
|
public const SCIM_APP_URL_PATH = "index.php/apps/scimserviceprovider";
|
||||||
|
|
||||||
|
public static function getConfigFile()
|
||||||
|
{
|
||||||
|
$configFilePath = dirname(__DIR__) . '/Config/config.php';
|
||||||
|
$config = require($configFilePath);
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
}
|
779
tests/postman/api_tests.postman_collection.json
Normal file
779
tests/postman/api_tests.postman_collection.json
Normal file
|
@ -0,0 +1,779 @@
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "65bcae79-ee78-4eb8-92cc-f23f21913bb9",
|
||||||
|
"name": "SCIM Nextcloud App Collection",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Groups",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create a single group",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 201\", () => {",
|
||||||
|
" pm.response.to.have.status(201);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().displayName).to.eql(\"createdtestgroup\");",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains a valid non-null group ID (the ID of the group which was created)\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.not.be.null;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.collectionVariables.set(\"testGroupId\", pm.response.json().id);"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"displayName\": \"createdtestgroup\",\n \"members\": []\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Groups",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Groups"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read a single group",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains the group ID of the group we want to read\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testGroupId'));",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().displayName).to.eql(\"createdtestgroup\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Groups/{{testGroupId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Groups",
|
||||||
|
"{{testGroupId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read all groups",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
|
||||||
|
" var resources = pm.response.json().Resources.map(x => x.displayName);",
|
||||||
|
" pm.expect(resources).to.contain(\"createdtestgroup\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Groups",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Groups"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update a single group",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains the group ID of the group we want to read\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testGroupId'));",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains group with displayName \\\"updatedtestgroup\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().displayName).to.eql(\"updatedtestgroup\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"displayName\": \"updatedtestgroup\",\n \"members\": []\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Groups/{{testGroupId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Groups",
|
||||||
|
"{{testGroupId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete a single group",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 204\", () => {",
|
||||||
|
" pm.response.to.have.status(204);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Groups/{{testGroupId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Groups",
|
||||||
|
"{{testGroupId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"type": "basic",
|
||||||
|
"basic": [
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"value": "admin",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "username",
|
||||||
|
"value": "admin",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ResourceTypes",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Read all ResourceTypes",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains exactly one entry\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources.length).to.eql(2);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains ResourceType with id \\\"User\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources[0].id).to.eql(\"User\");",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains ResourceType with id \\\"Group\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources[1].id).to.eql(\"Group\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer: {{jwt_token}}",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/ResourceTypes",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"ResourceTypes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Schemas",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Read all Schemas",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains exactly four entries\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources.length).to.eql(4);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Group\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources[0].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:Group\");",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources[1].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\");",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:User\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources[2].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:User\");",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Schema\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources[3].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:Schema\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer: {{jwt_token}}",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Schemas",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Schemas"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ServiceProviderConfigs",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Read all ServiceProviderConfigs",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains a ServiceProviderConfig with a correct schema\", () => {",
|
||||||
|
" pm.expect(pm.response.json().schemas).to.include(\"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer: {{jwt_token}}",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/ServiceProviderConfig",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"ServiceProviderConfig"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Users",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create a single user",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 201\", () => {",
|
||||||
|
" pm.response.to.have.status(201);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().userName).to.eql(\"createdtestuser\");",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains a valid non-null user ID (the ID of the user which was created)\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.not.be.null;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.collectionVariables.set(\"testUserId\", pm.response.json().id);"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"userName\": \"createdtestuser\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Users",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Users"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read a single user",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains the user ID of the user we want to read\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testUserId'));",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().userName).to.eql(\"createdtestuser\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Users/{{testUserId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Users",
|
||||||
|
"{{testUserId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read all users",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
|
||||||
|
" var resources = pm.response.json().Resources.map(x => x.userName);",
|
||||||
|
" pm.expect(resources).to.contain(\"createdtestuser\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Users",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Users"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update a single user",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains the user ID of the user we want to read\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testUserId'));",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains user with displayName \\\"updatedtestuser\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().displayName).to.eql(\"updatedtestuser\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"displayName\": \"updatedtestuser\",\n \"active\": false\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Users/{{testUserId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Users",
|
||||||
|
"{{testUserId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete a single user",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 204\", () => {",
|
||||||
|
" pm.response.to.have.status(204);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"userName\": \"updatedtestuser\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/Users/{{testUserId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"Users",
|
||||||
|
"{{testUserId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"type": "basic",
|
||||||
|
"basic": [
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"value": "admin",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "username",
|
||||||
|
"value": "admin",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"type": "basic",
|
||||||
|
"basic": [
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"value": "admin",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "username",
|
||||||
|
"value": "admin",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "testUserId",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "testGroupId",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "url",
|
||||||
|
"value": "http://localhost:8888/index.php/apps/scimserviceprovider",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
583
tests/postman/bearer_token_api_tests.postman_collection.json
Normal file
583
tests/postman/bearer_token_api_tests.postman_collection.json
Normal file
|
@ -0,0 +1,583 @@
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "606af599-3dec-46c8-9464-f52a7fd8f5b7",
|
||||||
|
"name": "SCIM Nextcloud App Collection (Bearer Token)",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Users",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create a single user",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 201\", () => {",
|
||||||
|
" pm.response.to.have.status(201);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().userName).to.eql(\"createdtestuser\");",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains a valid non-null user ID (the ID of the user which was created)\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.not.be.null;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.collectionVariables.set(\"testUserId\", pm.response.json().id);"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"userName\": \"createdtestuser\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/bearer/Users",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"bearer",
|
||||||
|
"Users"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read a single user",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains the user ID of the user we want to read\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testUserId'));",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().userName).to.eql(\"createdtestuser\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/bearer/Users/{{testUserId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"bearer",
|
||||||
|
"Users",
|
||||||
|
"{{testUserId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read all users",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
|
||||||
|
" var resources = pm.response.json().Resources.map(x => x.userName);",
|
||||||
|
" pm.expect(resources).to.contain(\"createdtestuser\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/bearer/Users",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"bearer",
|
||||||
|
"Users"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update a single user",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains the user ID of the user we want to read\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testUserId'));",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains user with displayName \\\"updatedtestuser\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().displayName).to.eql(\"updatedtestuser\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"displayName\": \"updatedtestuser\",\n \"active\": false\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/bearer/Users/{{testUserId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"bearer",
|
||||||
|
"Users",
|
||||||
|
"{{testUserId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete a single user",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 204\", () => {",
|
||||||
|
" pm.response.to.have.status(204);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"userName\": \"updatedtestuser\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/bearer/Users/{{testUserId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"bearer",
|
||||||
|
"Users",
|
||||||
|
"{{testUserId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Groups",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create a single group",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 201\", () => {",
|
||||||
|
" pm.response.to.have.status(201);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().displayName).to.eql(\"createdtestgroup\");",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains a valid non-null group ID (the ID of the group which was created)\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.not.be.null;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.collectionVariables.set(\"testGroupId\", pm.response.json().id);"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"displayName\": \"createdtestgroup\",\n \"members\": []\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/bearer/Groups",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"bearer",
|
||||||
|
"Groups"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read a single group",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains the group ID of the group we want to read\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testGroupId'));",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().displayName).to.eql(\"createdtestgroup\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/bearer/Groups/{{testGroupId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"bearer",
|
||||||
|
"Groups",
|
||||||
|
"{{testGroupId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Read all groups",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
|
||||||
|
" var resources = pm.response.json().Resources.map(x => x.displayName);",
|
||||||
|
" pm.expect(resources).to.contain(\"createdtestgroup\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/bearer/Groups",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"bearer",
|
||||||
|
"Groups"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update a single group",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 200\", () => {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body is not empty\", () => {",
|
||||||
|
" pm.expect(pm.response.json()).to.not.be.empty;",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains the group ID of the group we want to read\", () => {",
|
||||||
|
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testGroupId'));",
|
||||||
|
"});",
|
||||||
|
"",
|
||||||
|
"pm.test(\"Response body contains group with displayName \\\"updatedtestgroup\\\"\", () => {",
|
||||||
|
" pm.expect(pm.response.json().displayName).to.eql(\"updatedtestgroup\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"displayName\": \"updatedtestgroup\",\n \"members\": []\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/bearer/Groups/{{testGroupId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"bearer",
|
||||||
|
"Groups",
|
||||||
|
"{{testGroupId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete a single group",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Response status code is 204\", () => {",
|
||||||
|
" pm.response.to.have.status(204);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{url}}/bearer/Groups/{{testGroupId}}",
|
||||||
|
"host": [
|
||||||
|
"{{url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"bearer",
|
||||||
|
"Groups",
|
||||||
|
"{{testGroupId}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"bearer": [
|
||||||
|
{
|
||||||
|
"key": "token",
|
||||||
|
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.Oetm7xvhkYbiItRiqNx-z7LZ6ZkmDe1z_95igbPUSjA",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "testUserId",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "testGroupId",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "url",
|
||||||
|
"value": "http://localhost:8888/index.php/apps/scimserviceprovider",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in a new issue