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:
Pierre Ozoux 2023-03-03 11:21:28 +00:00
commit 50266601a7
28 changed files with 8038 additions and 2362 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
vendor vendor
/lib/Vendor

116
README.md
View file

@ -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

View file

@ -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;

View file

@ -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

File diff suppressed because it is too large Load diff

View 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;
}
}

View 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
View 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
View 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'
]
];

View 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);
}
}

View file

@ -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;
}
} }

View 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);
}
}

View 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);
}
}

View file

@ -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);
}
}
} }

View 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;
}
}

View 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;
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace OCA\SCIMServiceProvider\Exception;
use Exception;
class AuthException extends Exception
{
}

View file

@ -0,0 +1,9 @@
<?php
namespace OCA\SCIMServiceProvider\Exception;
use Exception;
class ContentTypeException extends Exception
{
}

View 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);
}
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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
View 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);
}
}
}

View 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
View 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;
}
}

View 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"
}
]
}

View 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"
}
]
}