diff --git a/Web/Services/index.php b/Web/Services/index.php
index 615a13912..49aa7dfd9 100644
--- a/Web/Services/index.php
+++ b/Web/Services/index.php
@@ -56,6 +56,20 @@
'You must be authenticated in order to access this service.
' . $server->GetFullServiceUrl(WebServices::Login)
);
}
+
+ $userSession = ServiceLocator::GetUserSession();
+ // Admin users can always use the API
+ if ($userSession->IsAdmin) {
+ return;
+ }
+
+ // Check if the user is allowed API access to the route
+ if (!$registry->IsUserAllowedApiAccess(routeName: $routeName, userId: $userSession->UserId)) {
+ $app->halt(
+ RestResponse::FORBIDDEN,
+ 'You are not authorized to access this service.
'
+ );
+ }
}
});
@@ -84,7 +98,12 @@ function RegisterHelp(SlimWebServiceRegistry $registry, \Slim\Slim $app)
function RegisterAuthentication(SlimServer $server, SlimWebServiceRegistry $registry)
{
- $webService = new AuthenticationWebService($server, new WebServiceAuthentication(PluginManager::Instance()->LoadAuthentication(), new UserSessionRepository()));
+ $api_access_group_id = GetConfigGroup(config_group: "Authentication.group");
+ $webService = new AuthenticationWebService(
+ $server,
+ new WebServiceAuthentication(PluginManager::Instance()->LoadAuthentication(), new UserSessionRepository()),
+ api_access_group_id: $api_access_group_id
+ );
$category = new SlimWebServiceRegistryCategory('Authentication');
$category->AddPost('SignOut/', [$webService, 'SignOut'], WebServices::Logout);
@@ -97,7 +116,10 @@ function RegisterReservations(SlimServer $server, SlimWebServiceRegistry $regist
$readService = new ReservationsWebService($server, new ReservationViewRepository(), new PrivacyFilter(new ReservationAuthorization(PluginManager::Instance()->LoadAuthorization())), new AttributeService(new AttributeRepository()));
$writeService = new ReservationWriteWebService($server, new ReservationSaveController(new ReservationPresenterFactory()));
- $category = new SlimWebServiceRegistryCategory('Reservations');
+ $roGroupId = GetConfigGroup('Reservations.ro.group');
+ $rwGroupId = GetConfigGroup('Reservations.rw.group');
+ $category = new SlimWebServiceRegistryCategory('Reservations', roGroupId: $roGroupId, rwGroupId: $rwGroupId);
+
$category->AddSecurePost('/', [$writeService, 'Create'], WebServices::CreateReservation);
$category->AddSecureGet('/', [$readService, 'GetReservations'], WebServices::AllReservations);
$category->AddSecureGet('/:referenceNumber', [$readService, 'GetReservation'], WebServices::GetReservation);
@@ -116,7 +138,10 @@ function RegisterResources(SlimServer $server, SlimWebServiceRegistry $registry)
$attributeService = new AttributeService(new AttributeRepository());
$webService = new ResourcesWebService($server, $resourceRepository, $attributeService, new ReservationViewRepository());
$writeWebService = new ResourcesWriteWebService($server, new ResourceSaveController($resourceRepository, new ResourceRequestValidator($attributeService)));
- $category = new SlimWebServiceRegistryCategory('Resources');
+
+ $roGroupId = GetConfigGroup('Resources.ro.group');
+ $category = new SlimWebServiceRegistryCategory('Resources', roGroupId: $roGroupId);
+
$category->AddGet('/Status', [$webService, 'GetStatuses'], WebServices::GetStatuses);
$category->AddSecureGet('/', [$webService, 'GetAll'], WebServices::AllResources);
$category->AddSecureGet('/Status/Reasons', [$webService, 'GetStatusReasons'], WebServices::GetStatusReasons);
@@ -133,7 +158,10 @@ function RegisterResources(SlimServer $server, SlimWebServiceRegistry $registry)
function RegisterAccessories(SlimServer $server, SlimWebServiceRegistry $registry)
{
$webService = new AccessoriesWebService($server, new ResourceRepository(), new AccessoryRepository());
- $category = new SlimWebServiceRegistryCategory('Accessories');
+
+ $roGroupId = GetConfigGroup('Accessories.ro.group');
+ $category = new SlimWebServiceRegistryCategory('Accessories', roGroupId: $roGroupId);
+
$category->AddSecureGet('/', [$webService, 'GetAll'], WebServices::AllAccessories);
$category->AddSecureGet('/:accessoryId', [$webService, 'GetAccessory'], WebServices::GetAccessory);
$registry->AddCategory($category);
@@ -147,7 +175,10 @@ function RegisterUsers(SlimServer $server, SlimWebServiceRegistry $registry)
$server,
new UserSaveController(new ManageUsersServiceFactory(), new UserRequestValidator($attributeService, new UserRepository()))
);
- $category = new SlimWebServiceRegistryCategory('Users');
+
+ $roGroupId = GetConfigGroup('Users.ro.group');
+ $category = new SlimWebServiceRegistryCategory('Users', roGroupId: $roGroupId);
+
$category->AddSecureGet('/', [$webService, 'GetUsers'], WebServices::AllUsers);
$category->AddSecureGet('/:userId', [$webService, 'GetUser'], WebServices::GetUser);
$category->AddAdminPost('/', [$writeWebService, 'Create'], WebServices::CreateUser);
@@ -160,7 +191,10 @@ function RegisterUsers(SlimServer $server, SlimWebServiceRegistry $registry)
function RegisterSchedules(SlimServer $server, SlimWebServiceRegistry $registry)
{
$webService = new SchedulesWebService($server, new ScheduleRepository(), new PrivacyFilter(new ReservationAuthorization(PluginManager::Instance()->LoadAuthorization())));
- $category = new SlimWebServiceRegistryCategory('Schedules');
+
+ $roGroupId = GetConfigGroup('Schedules.ro.group');
+ $category = new SlimWebServiceRegistryCategory('Schedules', roGroupId: $roGroupId);
+
$category->AddSecureGet('/', [$webService, 'GetSchedules'], WebServices::AllSchedules);
$category->AddSecureGet('/:scheduleId', [$webService, 'GetSchedule'], WebServices::GetSchedule);
$category->AddSecureGet('/:scheduleId/Slots', [$webService, 'GetSlots'], WebServices::GetScheduleSlots);
@@ -172,7 +206,9 @@ function RegisterAttributes(SlimServer $server, SlimWebServiceRegistry $registry
$webService = new AttributesWebService($server, new AttributeService(new AttributeRepository()));
$writeWebService = new AttributesWriteWebService($server, new AttributeSaveController(new AttributeRepository()));
- $category = new SlimWebServiceRegistryCategory('Attributes');
+ $roGroupId = GetConfigGroup('Attributes.ro.group');
+ $category = new SlimWebServiceRegistryCategory('Attributes', roGroupId: $roGroupId);
+
$category->AddSecureGet('Category/:categoryId', [$webService, 'GetAttributes'], WebServices::AllCustomAttributes);
$category->AddSecureGet('/:attributeId', [$webService, 'GetAttribute'], WebServices::GetCustomAttribute);
$category->AddAdminPost('/', [$writeWebService, 'Create'], WebServices::CreateCustomAttribute);
@@ -187,7 +223,8 @@ function RegisterGroups(SlimServer $server, SlimWebServiceRegistry $registry)
$webService = new GroupsWebService($server, $groupRepository, $groupRepository);
$writeWebService = new GroupsWriteWebService($server, new GroupSaveController($groupRepository, new ResourceRepository(), new ScheduleRepository()));
- $category = new SlimWebServiceRegistryCategory('Groups');
+ $roGroupId = GetConfigGroup('Groups.ro.group');
+ $category = new SlimWebServiceRegistryCategory('Groups', roGroupId: $roGroupId);
$category->AddSecureGet('/', [$webService, 'GetGroups'], WebServices::AllGroups);
$category->AddSecureGet('/:groupId', [$webService, 'GetGroup'], WebServices::GetGroup);
@@ -211,7 +248,10 @@ function RegisterAccounts(SlimServer $server, SlimWebServiceRegistry $registry)
$webService = new AccountWebService($server, $controller);
- $category = new SlimWebServiceRegistryCategory('Accounts');
+ $roGroupId = GetConfigGroup('Accounts.ro.group');
+ $rwGroupId = GetConfigGroup('Accounts.rw.group');
+ $category = new SlimWebServiceRegistryCategory('Accounts', roGroupId: $roGroupId, rwGroupId: $rwGroupId);
+
$category->AddPost('/', [$webService, 'Create'], WebServices::CreateAccount);
$category->AddSecurePost('/:userId', [$webService, 'Update'], WebServices::UpdateAccount);
$category->AddSecurePost('/:userId/Password', [$webService, 'UpdatePassword'], WebServices::UpdateAccountPassword);
@@ -219,3 +259,30 @@ function RegisterAccounts(SlimServer $server, SlimWebServiceRegistry $registry)
$registry->AddCategory($category);
}
+
+function GetConfigGroup(string $config_group): string|null {
+ $group_name = Configuration::Instance()->GetSectionKey(ConfigSection::API, $config_group) ?? '';
+ if ($group_name == '') {
+ return null;
+ }
+ $groupRepository = new GroupRepository();
+ $groups = $groupRepository->GetList()->Results();
+ foreach ($groups as $group) {
+ if ($group->Name == $group_name) {
+ return $group->Id();
+ }
+ }
+ die("Unable to find group: '$group_name' for API group '$config_group'. Please contact the administrator to resolve this issue in the `config.php` file.");
+ return null;
+}
+
+function IsUserInGroup(string|int $groupId, string|int $userId): bool {
+ $groupRepository = new GroupRepository();
+ $group = $groupRepository->LoadById($groupId);
+ foreach ($group->UserIds() as $groupUserId) {
+ if ($groupUserId == $userId) {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/WebServices/AuthenticationWebService.php b/WebServices/AuthenticationWebService.php
index 76f23de83..a76f1ce3f 100644
--- a/WebServices/AuthenticationWebService.php
+++ b/WebServices/AuthenticationWebService.php
@@ -25,11 +25,13 @@ class AuthenticationWebService
* @var IWebServiceAuthentication
*/
private $authentication;
+ private int|string|null $api_access_group_id; // If specified and user not in group then authentication will be denied
- public function __construct(IRestServer $server, IWebServiceAuthentication $authentication)
+ public function __construct(IRestServer $server, IWebServiceAuthentication $authentication, int|string|null $api_access_group_id = null)
{
$this->server = $server;
$this->authentication = $authentication;
+ $this->api_access_group_id = $api_access_group_id;
}
/**
@@ -51,6 +53,15 @@ public function Authenticate()
$isValid = $this->authentication->Validate($username, $password);
if ($isValid) {
Log::Debug('WebService Authenticate, user %s was authenticated', $username);
+ $session = $this->authentication->Login($username);
+
+ if (!$session->IsAdmin && is_numeric($this->api_access_group_id)) {
+ if (!IsUserInGroup(groupId: $this->api_access_group_id, userId: $session->UserId)) {
+ Log::Debug('WebService Authenticate, user %s was denied API access', $username);
+ $this->server->WriteResponse(AuthenticationResponse::NotAuthorized(), statusCode: RestResponse::FORBIDDEN);
+ return;
+ }
+ }
$version = 0;
$reader = ServiceLocator::GetDatabase()->Query(new GetVersionCommand());
@@ -59,7 +70,6 @@ public function Authenticate()
}
$reader->Free();
- $session = $this->authentication->Login($username);
Log::Debug('SessionToken=%s', $session->SessionToken);
$this->server->WriteResponse(AuthenticationResponse::Success($this->server, $session, $version));
} else {
diff --git a/WebServices/Responses/AuthenticationResponse.php b/WebServices/Responses/AuthenticationResponse.php
index 4939e537c..ac938aec5 100644
--- a/WebServices/Responses/AuthenticationResponse.php
+++ b/WebServices/Responses/AuthenticationResponse.php
@@ -45,6 +45,13 @@ public static function Failed()
return $response;
}
+ public static function NotAuthorized()
+ {
+ $response = new AuthenticationResponse();
+ $response->message = 'Login failed. API access not authorized.';
+ return $response;
+ }
+
public static function Example()
{
return new ExampleAuthenticationResponse();
diff --git a/config/config.devel.php b/config/config.devel.php
index 8f9438095..f9df64c1d 100644
--- a/config/config.devel.php
+++ b/config/config.devel.php
@@ -213,3 +213,49 @@
$conf['settings']['logging']['folder'] = '/var/log/librebooking/log'; //Absolute path to folder were the log will be written, writing permissions to the folder are required
$conf['settings']['logging']['level'] = 'debug'; //Set to none disable logs, error to only log errors or debug to log all messages to the app.log file
$conf['settings']['logging']['sql'] = 'false'; //Set to true no enable the creation of and sql.log file
+
+
+/**
+ * API Granularity Settings
+ */
+$conf['settings']['api']['Authentication.group'] = ''; // If a group is specified then a user must be in the group in order to sucessfully authenticate. Unless the user is an Admin.
+/**
+ * API access restrictions. These only provide additional restrictions. They do
+ * not provide additional permissions.
+ *
+ * If desired can specify a single group to limit access to an API category.
+ * Access per category can be limited to RO (Read-Only) and/or RW (Read-Write)
+ * access.
+ * RO access means they can only do GET actions.
+ * RW access means they can do GET/POST/PUT/DELETE actions.
+ * If a group is specified and the user is not in the group then they will be
+ * denied access, unless the user is an Admin.
+ * If a group is NOT specified then normal access permissions will apply.
+ */
+
+$conf['settings']['api']['Accessories.ro.group'] = '';
+// NOTE: There are no "write" APIs for `Accessories`
+
+$conf['settings']['api']['Accounts.ro.group'] = '';
+$conf['settings']['api']['Accounts.rw.group'] = '';
+
+$conf['settings']['api']['Attributes.ro.group'] = '';
+// NOTE: Only application administrators can "write" to `Attributes`
+
+$conf['settings']['api']['Groups.ro.group'] = '';
+// NOTE: Only application administrators can "write" to `Groups`
+
+$conf['settings']['api']['Reservations.ro.group'] = '';
+$conf['settings']['api']['Reservations.rw.group'] = '';
+
+$conf['settings']['api']['Resources.ro.group'] = '';
+// NOTE: Only application administrators can "write" to `Resources`
+
+$conf['settings']['api']['Schedules.ro.group'] = '';
+// NOTE: There are no "write" APIs for `Schedules`
+
+$conf['settings']['api']['Users.ro.group'] = '';
+// NOTE: Only application administrators can "write" to `Users`
+
+$conf['settings']['api']['Schedules.ro.group'] = '';
+// NOTE: There are no "write" APIs for `Schedules`
diff --git a/config/config.dist.php b/config/config.dist.php
index 15a2ee7f5..2def7023f 100644
--- a/config/config.dist.php
+++ b/config/config.dist.php
@@ -269,3 +269,49 @@
$conf['settings']['delete.old.data']['delete.old.announcements'] = 'false'; //Choose if this feature deletes old announcements from database
$conf['settings']['delete.old.data']['delete.old.blackouts'] = 'false'; //Choose if this feature deletes old blackouts from database
$conf['settings']['delete.old.data']['delete.old.reservations'] = 'false'; //Choose if this feature deletes old reservations from database
+
+
+/**
+ * API Granularity Settings
+ */
+$conf['settings']['api']['Authentication.group'] = ''; // If a group is specified then a user must be in the group in order to sucessfully authenticate. Unless the user is an Admin.
+/**
+ * API access restrictions. These only provide additional restrictions. They do
+ * not provide additional permissions.
+ *
+ * If desired can specify a single group to limit access to an API category.
+ * Access per category can be limited to RO (Read-Only) and/or RW (Read-Write)
+ * access.
+ * RO access means they can only do GET actions.
+ * RW access means they can do GET/POST/PUT/DELETE actions.
+ * If a group is specified and the user is not in the group then they will be
+ * denied access, unless the user is an Admin.
+ * If a group is NOT specified then normal access permissions will apply.
+ */
+
+$conf['settings']['api']['Accessories.ro.group'] = '';
+// NOTE: There are no "write" APIs for `Accessories`
+
+$conf['settings']['api']['Accounts.ro.group'] = '';
+$conf['settings']['api']['Accounts.rw.group'] = '';
+
+$conf['settings']['api']['Attributes.ro.group'] = '';
+// NOTE: Only application administrators can "write" to `Attributes`
+
+$conf['settings']['api']['Groups.ro.group'] = '';
+// NOTE: Only application administrators can "write" to `Groups`
+
+$conf['settings']['api']['Reservations.ro.group'] = '';
+$conf['settings']['api']['Reservations.rw.group'] = '';
+
+$conf['settings']['api']['Resources.ro.group'] = '';
+// NOTE: Only application administrators can "write" to `Resources`
+
+$conf['settings']['api']['Schedules.ro.group'] = '';
+// NOTE: There are no "write" APIs for `Schedules`
+
+$conf['settings']['api']['Users.ro.group'] = '';
+// NOTE: Only application administrators can "write" to `Users`
+
+$conf['settings']['api']['Schedules.ro.group'] = '';
+// NOTE: There are no "write" APIs for `Schedules`
diff --git a/lib/WebService/RestResponse.php b/lib/WebService/RestResponse.php
index 8eaf6fa8d..b1cd6b3c5 100644
--- a/lib/WebService/RestResponse.php
+++ b/lib/WebService/RestResponse.php
@@ -6,6 +6,7 @@ class RestResponse
public const CREATED_CODE = 201;
public const BAD_REQUEST_CODE = 400;
public const UNAUTHORIZED_CODE = 401;
+ public const FORBIDDEN = 403;
public const NOT_FOUND_CODE = 404;
public const SERVER_ERROR = 500;
diff --git a/lib/WebService/Slim/SlimWebServiceRegistry.php b/lib/WebService/Slim/SlimWebServiceRegistry.php
index 12f7cd7aa..07a34351a 100644
--- a/lib/WebService/Slim/SlimWebServiceRegistry.php
+++ b/lib/WebService/Slim/SlimWebServiceRegistry.php
@@ -3,6 +3,40 @@
require_once(ROOT_DIR . 'lib/external/Slim/Slim.php');
require_once(ROOT_DIR . 'lib/Common/namespace.php');
+class ApiPermissions
+{
+ public function __construct(
+ public bool $isWrite,
+ public int|string|null $roGroupId,
+ public int|string|null $rwGroupId
+ ) { }
+
+ public function IsUserAllowedApiAccess(int|string $userId): bool
+ {
+ if ($this->isWrite) {
+ // If a write API, then check if a RW group is set and verify access.
+ // If no RW group, then check if a RO group is set and verify access
+ if (is_numeric($this->rwGroupId)) {
+ return IsUserInGroup(groupId: $this->rwGroupId, userId: $userId);
+ }
+ if (is_numeric($this->roGroupId)) {
+ return IsUserInGroup(groupId: $this->roGroupId, userId: $userId);
+ }
+ return true;
+ }
+
+ if (is_numeric($this->roGroupId)) {
+ return IsUserInGroup(groupId: $this->roGroupId, userId: $userId);
+ }
+ return true;
+ }
+
+ public function IsSet(): bool {
+ return (is_numeric($this->roGroupId) || is_numeric($this->rwGroupId));
+ }
+
+}
+
class SlimWebServiceRegistry
{
/**
@@ -25,6 +59,11 @@ class SlimWebServiceRegistry
*/
private $adminRoutes = [];
+ /**
+ * @var array
+ */
+ private $apiPermissionRoutes = [];
+
public function __construct(Slim\Slim $slim)
{
$this->slim = $slim;
@@ -37,17 +76,26 @@ public function AddCategory(SlimWebServiceRegistryCategory $category)
{
foreach ($category->Gets() as $registration) {
$this->slim->get($registration->Route(), $registration->Callback())->name($registration->RouteName());
- $this->SecureRegistration($registration);
+ $this->SecureRegistration(
+ $registration,
+ apiPermissions: new ApiPermissions(isWrite: false, roGroupId: $category->GetRoGroupId(), rwGroupId: $category->GetRwGroupId())
+ );
}
foreach ($category->Posts() as $registration) {
$this->slim->post($registration->Route(), $registration->Callback())->name($registration->RouteName());
- $this->SecureRegistration($registration);
+ $this->SecureRegistration(
+ $registration,
+ apiPermissions: new ApiPermissions(isWrite: true, roGroupId: $category->GetRoGroupId(), rwGroupId: $category->GetRwGroupId())
+ );
}
foreach ($category->Deletes() as $registration) {
$this->slim->delete($registration->Route(), $registration->Callback())->name($registration->RouteName());
- $this->SecureRegistration($registration);
+ $this->SecureRegistration(
+ $registration,
+ apiPermissions: new ApiPermissions(isWrite: true, roGroupId: $category->GetRoGroupId(), rwGroupId: $category->GetRwGroupId())
+ );
}
$this->categories[] = $category;
@@ -90,8 +138,15 @@ public function IsLimitedToAdmin($routeName)
return array_key_exists($routeName, $this->adminRoutes);
}
- private function SecureRegistration(SlimServiceRegistration $registration)
+ public function IsUserAllowedApiAccess(string $routeName, int|string $userId): bool
{
+ if (!array_key_exists($routeName, $this->apiPermissionRoutes)) {
+ return true;
+ }
+ return $this->apiPermissionRoutes[$routeName]->IsUserAllowedApiAccess(userId: $userId);
+ }
+
+ private function SecureRegistration(SlimServiceRegistration $registration, ApiPermissions $apiPermissions) {
if ($registration->IsSecure()) {
$this->secureRoutes[$registration->RouteName()] = true;
}
@@ -99,5 +154,9 @@ private function SecureRegistration(SlimServiceRegistration $registration)
if ($registration->IsLimitedToAdmin()) {
$this->adminRoutes[$registration->RouteName()] = true;
}
+
+ if ($apiPermissions->IsSet()) {
+ $this->apiPermissionRoutes[$registration->RouteName()] = $apiPermissions;
+ }
}
}
diff --git a/lib/WebService/Slim/SlimWebServiceRegistryCategory.php b/lib/WebService/Slim/SlimWebServiceRegistryCategory.php
index b425a2037..42bff0913 100644
--- a/lib/WebService/Slim/SlimWebServiceRegistryCategory.php
+++ b/lib/WebService/Slim/SlimWebServiceRegistryCategory.php
@@ -6,10 +6,14 @@ class SlimWebServiceRegistryCategory
private $gets = [];
private $posts = [];
private $deletes = [];
+ private int|string|null $roGroupId; // User group allowed Read-Only access to API Category
+ private int|string|null $rwGroupId; // User group allowed Read-Write access to API Category
- public function __construct($name)
+ public function __construct($name, int|string|null $roGroupId = null, int|string|null $rwGroupId = null)
{
$this->name = $name;
+ $this->roGroupId = $roGroupId;
+ $this->rwGroupId = $rwGroupId;
}
/**
@@ -36,6 +40,28 @@ public function Deletes()
return $this->deletes;
}
+ public function GetRoGroupId(): int|string|null {
+ return $this->roGroupId;
+ }
+
+ public function GetRwGroupId(): int|string|null {
+ return $this->rwGroupId;
+ }
+
+ public function UserAllowedRoAccess(int|string $userId): bool {
+ if (is_null($this->roGroupId)) {
+ return true;
+ }
+ return IsUserInGroup(groupId: $this->roGroupId, userId: $userId);
+ }
+
+ public function UserAllowedRwAccess(int|string $userId): bool {
+ if (is_null($this->rwGroupId)) {
+ return true;
+ }
+ return IsUserInGroup(groupId: $this->rwGroupId, userId: $userId);
+ }
+
public function AddGet($route, $callback, $routeName)
{
$this->gets[] = new SlimServiceRegistration($this->name, $route, $callback, $routeName);