Skip to content
Commits on Source (2)
vendor
\ No newline at end of file
vendor
/lib/Vendor
# 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.
## Limitations
---
- doesn't accept `application/scim+json` content-type, but only `application/json`
- doesn't implement `meta:createdAt` nor `meta:lastModified` due to this [bug](https://github.com/nextcloud/server/issues/22640) (return unix epoch instead).
## Table of content
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
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
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
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
- [ ] Meta (Create our own table)
- [ ] Meta -> ([can't implement yet](https://github.com/nextcloud/server/issues/22640))
- createdAt
- 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
- [ ] group member removal
- [ ] pagination
......@@ -39,19 +119,19 @@ For CI, there is still [a bug](https://github.com/Kong/insomnia/issues/4747) we
- [ ] test psalm
- [ ] test insomnia
- [ ] publish app on app store
- [ ] lib user scim php
- [ ] accept first email, even if not primary
- [ ] Allow for simultaneous usage of basic auth and bearer token auth (see **Authentication TODOs / Open issues**)
## Quick "Deploy" to test
### Authentication TODOs / Open issues
#### Support for simultaneously using basic auth and bearer token auth in parallel
Solution idea:
```
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
```
- 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
- 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
- In case of `Bearer`, the current implementation of bearer token authentication via JWTs can be used
- 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
## Disclaimer
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
......
<?php
return [
'resources' => [
'user' => ['url' => '/Users'],
'group' => ['url' => '/Groups']
$routes = [
'routes' => [
['name' => 'service_provider_configuration#resource_types', 'url' => '/ResourceTypes', 'verb' => 'GET'],
['name' => 'service_provider_configuration#schemas', 'url' => '/Schemas', 'verb' => 'GET'],
['name' => 'service_provider_configuration#service_provider_config', 'url' => '/ServiceProviderConfig', 'verb' => 'GET'],
]
];
$config = require dirname(__DIR__) . '/lib/Config/config.php';
$userAndGroupRoutes = [];
if (isset($config['auth_type']) && !empty($config['auth_type']) && (strcmp($config['auth_type'], 'bearer') === 0)) {
$userAndGroupRoutes = [
['name' => 'user_bearer#index', 'url' => '/bearer/Users', 'verb' => 'GET'],
['name' => 'user_bearer#show', 'url' => '/bearer/Users/{id}', 'verb' => 'GET'],
['name' => 'user_bearer#create', 'url' => '/bearer/Users', 'verb' => 'POST'],
['name' => 'user_bearer#update', 'url' => '/bearer/Users/{id}', 'verb' => 'PUT'],
['name' => 'user_bearer#destroy', 'url' => '/bearer/Users/{id}', 'verb' => 'DELETE'],
['name' => 'group_bearer#index', 'url' => '/bearer/Groups', 'verb' => 'GET'],
['name' => 'group_bearer#show', 'url' => '/bearer/Groups/{id}', 'verb' => 'GET'],
['name' => 'group_bearer#create', 'url' => '/bearer/Groups', 'verb' => 'POST'],
['name' => 'group_bearer#update', 'url' => '/bearer/Groups/{id}', 'verb' => 'PUT'],
['name' => 'group_bearer#destroy', 'url' => '/bearer/Groups/{id}', 'verb' => 'DELETE'],
];
} else if (!isset($config['auth_type']) || empty($config['auth_type']) || (strcmp($config['auth_type'], 'basic') === 0)) {
$userAndGroupRoutes = [
['name' => 'user#index', 'url' => '/Users', 'verb' => 'GET'],
['name' => 'user#show', 'url' => '/Users/{id}', 'verb' => 'GET'],
['name' => 'user#create', 'url' => '/Users', 'verb' => 'POST'],
['name' => 'user#update', 'url' => '/Users/{id}', 'verb' => 'PUT'],
['name' => 'user#destroy', 'url' => '/Users/{id}', 'verb' => 'DELETE'],
['name' => 'group#index', 'url' => '/Groups', 'verb' => 'GET'],
['name' => 'group#show', 'url' => '/Groups/{id}', 'verb' => 'GET'],
['name' => 'group#create', 'url' => '/Groups', 'verb' => 'POST'],
['name' => 'group#update', 'url' => '/Groups/{id}', 'verb' => 'PUT'],
['name' => 'group#destroy', 'url' => '/Groups/{id}', 'verb' => 'DELETE'],
];
}
$routes['routes'] = array_merge($routes['routes'], $userAndGroupRoutes);
return $routes;
......@@ -11,6 +11,25 @@
},
"scripts": {
"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"
}
}
This diff is collapsed.
<?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;
}
}
<?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;
}
}
<?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
{
}
}
<?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'
]
];
<?php
declare(strict_types=1);
namespace OCA\SCIMServiceProvider\Controller;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
use OCA\SCIMServiceProvider\Service\GroupService;
class GroupBearerController extends ApiController
{
/** @var GroupService */
private $groupService;
public function __construct(
string $appName,
IRequest $request,
GroupService $groupService
) {
parent::__construct(
$appName,
$request
);
$this->groupService = $groupService;
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param string $filter
* @return SCIMListResponse
* returns a list of groups and their data
*/
public function index(string $filter = ''): SCIMListResponse
{
return $this->groupService->getAll($filter);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* gets group info
*
* @param string $id
* @return SCIMJSONResponse
*/
// TODO: Add filtering support here as well
public function show(string $id): SCIMJSONResponse
{
return $this->groupService->getOneById($id);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param string $displayName
* @param array $members
* @return SCIMJSONResponse
*/
public function create(string $displayName = '', array $members = []): SCIMJSONResponse
{
return $this->groupService->create($displayName, $members);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param string $id
*
* @param string $displayName
* @param array $members
* @return SCIMJSONResponse
*/
public function update(string $id, string $displayName = '', array $members = []): SCIMJSONResponse
{
return $this->groupService->update($id, $displayName, $members);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param string $id
* @return Response
*/
public function destroy(string $id): Response
{
return $this->groupService->destroy($id);
}
}
......@@ -6,146 +6,89 @@ namespace OCA\SCIMServiceProvider\Controller;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\Response;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
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 */
private $logger;
private $SCIMGroup;
$this->groupService = $groupService;
}
public function __construct(string $appName,
IRequest $request,
IUserManager $userManager,
IGroupManager $groupManager,
LoggerInterface $logger,
SCIMGroup $SCIMGroup) {
parent::__construct($appName,
$request,
$userManager,
$groupManager);
/**
* @NoCSRFRequired
*
* @param string $filter
* @return SCIMListResponse
* returns a list of groups and their data
*/
public function index(string $filter = ''): SCIMListResponse
{
return $this->groupService->getAll($filter);
}
$this->logger = $logger;
$this->SCIMGroup = $SCIMGroup;
$this->groupManager = $groupManager;
$this->userManager = $userManager;
}
/**
* @NoCSRFRequired
*
* 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
*
* returns a list of groups and their data
*/
public function index(): SCIMListResponse {
$SCIMGroups = $this->groupManager->search('', null, 0);
$SCIMGroups = array_map(function ($group) {
return $this->SCIMGroup->get($group->getGID());
}, $SCIMGroups);
return new SCIMListResponse($SCIMGroups);
}
/**
* @NoCSRFRequired
*
* @param string $displayName
* @param array $members
* @return SCIMJSONResponse
*/
public function create(string $displayName = '', array $members = []): SCIMJSONResponse
{
return $this->groupService->create($displayName, $members);
}
/**
* @NoCSRFRequired
*
* gets group info
*
* @param string $id
* @return SCIMJSONResponse
* @throws Exception
*/
public function show(string $id): SCIMJSONResponse {
$group = $this->SCIMGroup->get($id);
if (empty($group)) {
return new SCIMErrorResponse(['message' => 'Group not found'], 404);
}
return new SCIMJSONResponse($group);
}
/**
* @NoCSRFRequired
*
* @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
*
* @param string $displayName
* @param array $members
* @return SCIMJSONResponse
* @throws Exception
*/
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)) {
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;
}
/**
* @NoCSRFRequired
*
* @param string $id
* @return Response
*/
public function destroy(string $id): Response
{
return $this->groupService->destroy($id);
}
}
<?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);
}
}
<?php
declare(strict_types=1);
namespace OCA\SCIMServiceProvider\Controller;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
use OCA\SCIMServiceProvider\Service\UserService;
class UserBearerController extends ApiController
{
/** @var UserService */
private $userService;
public function __construct(
string $appName,
IRequest $request,
UserService $userService
) {
parent::__construct(
$appName,
$request
);
$this->userService = $userService;
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param string $filter
* @return SCIMListResponse
* returns a list of users and their data
*/
public function index(string $filter = ''): SCIMListResponse
{
return $this->userService->getAll($filter);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* gets user info
*
* @param string $id
* @return SCIMJSONResponse
*/
// TODO: Add filtering support here as well
public function show(string $id): SCIMJSONResponse
{
return $this->userService->getOneById($id);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param bool $active
* @param string $displayName
* @param array $emails
* @param string $externalId
* @param string $userName
* @return SCIMJSONResponse
*/
public function create(
bool $active = true,
string $displayName = '',
array $emails = [],
string $externalId = '',
string $userName = ''
): SCIMJSONResponse
{
return $this->userService->create(
$active,
$displayName,
$emails,
$externalId,
$userName
);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param string $id
*
* @param bool $active
* @param string $displayName
* @param array $emails
* @return SCIMJSONResponse
*/
public function update(
string $id,
bool $active,
string $displayName = '',
array $emails = []
): SCIMJSONResponse
{
return $this->userService->update($id, $active, $displayName, $emails);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param string $id
* @return Response
*/
public function destroy(string $id): Response
{
return $this->userService->destroy($id);
}
}
......@@ -7,172 +7,109 @@ namespace OCA\SCIMServiceProvider\Controller;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface;
use OCA\SCIMServiceProvider\Responses\SCIMListResponse;
use OCA\SCIMServiceProvider\Responses\SCIMJSONResponse;
use OCA\SCIMServiceProvider\Responses\SCIMErrorResponse;
use OCA\SCIMServiceProvider\Service\SCIMUser;
class UserController extends ApiController {
/** @var LoggerInterface */
private $logger;
/** @var ISecureRandom */
private $secureRandom;
private $SCIMUser;
public function __construct(string $appName,
IRequest $request,
IUserManager $userManager,
LoggerInterface $logger,
ISecureRandom $secureRandom,
SCIMUser $SCIMUser) {
parent::__construct($appName,
$request,
$userManager);
$this->logger = $logger;
$this->secureRandom = $secureRandom;
$this->SCIMUser = $SCIMUser;
$this->userManager = $userManager;
}
/**
* @NoCSRFRequired
*
* returns a list of users and their data
*/
public function index(): SCIMListResponse {
$users = [];
$users = $this->userManager->search('', null, 0);
$userIds = array_keys($users);
$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);
}
}
use OCA\SCIMServiceProvider\Service\UserService;
class UserController extends ApiController
{
/** @var UserService */
private $userService;
public function __construct(
string $appName,
IRequest $request,
UserService $userService
) {
parent::__construct(
$appName,
$request
);
$this->userService = $userService;
}
/**
* @NoCSRFRequired
*
* @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
*
* 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
*
* @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
*
* @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
*
* @param string $id
* @return Response
*/
public function destroy(string $id): Response
{
return $this->userService->destroy($id);
}
}
<?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;
}
}
<?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;
}
}
<?php
namespace OCA\SCIMServiceProvider\Exception;
use Exception;
class AuthException extends Exception
{
}
\ No newline at end of file
<?php
namespace OCA\SCIMServiceProvider\Exception;
use Exception;
class ContentTypeException extends Exception
{
}
<?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);
}
}
}
<?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);
}
}
\ No newline at end of file