mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-20 17:15:51 +02:00
Allow to automatically assign SAML users to a group based on SAML attributes
This commit is contained in:
parent
6a06a24296
commit
5e85c52a57
6 changed files with 151 additions and 11 deletions
8
.env
8
.env
|
@ -79,6 +79,14 @@ ERROR_PAGE_SHOW_HELP=1
|
|||
# Set this to 1 to enable SAML single sign on
|
||||
SAML_ENABLED=0
|
||||
|
||||
# A JSON encoded array of role mappings in the form { "saml_role": PARTDB_GROUP_ID, "*": PARTDB_GROUP_ID }
|
||||
SAML_ROLE_MAPPING="{}"
|
||||
# A mapping could look like the following
|
||||
#SAML_ROLE_MAPPING='{ "*": 2, "editor": 3, "admin": 1 }'
|
||||
|
||||
# When this is set to 1, the group of SAML users will be updated everytime they login based on their SAML roles
|
||||
SAML_UPDATE_GROUP_ON_LOGIN=1
|
||||
|
||||
# The entity ID of your SAML IDP (e.g. the realm name of your Keycloak server)
|
||||
SAML_IDP_ENTITY_ID="https://idp.changeme.invalid/realms/master"
|
||||
# The URL of your SAML IDP SingleSignOnService (e.g. the endpoint of your Keycloak server)
|
||||
|
|
|
@ -119,3 +119,5 @@ parameters:
|
|||
env(TRUSTED_HOSTS): '' # Trust all host names by default
|
||||
|
||||
env(DEFAULT_URI): 'https://partdb.changeme.invalid/'
|
||||
|
||||
env(SAML_ROLE_MAPPING): '{}'
|
||||
|
|
|
@ -128,7 +128,13 @@ services:
|
|||
####################################################################################################################
|
||||
|
||||
saml_user_factory:
|
||||
class: App\Security\SamlUserFactory
|
||||
alias: App\Security\SamlUserFactory
|
||||
public: true
|
||||
|
||||
App\Security\SamlUserFactory:
|
||||
arguments:
|
||||
$saml_role_mapping: '%env(json:SAML_ROLE_MAPPING)%'
|
||||
$update_group_on_login: '%env(bool:SAML_UPDATE_GROUP_ON_LOGIN)%'
|
||||
|
||||
####################################################################################################################
|
||||
# Cache
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
# Service overrides for the test environment
|
||||
|
||||
services:
|
||||
saml_user_factory:
|
||||
class: App\Security\SamlUserFactory
|
||||
public: true
|
||||
|
||||
App\Security\SamlUserFactory:
|
||||
public: true
|
|
@ -20,12 +20,32 @@
|
|||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\UserSystem\Group;
|
||||
use App\Entity\UserSystem\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Hslavich\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken;
|
||||
use Hslavich\OneloginSamlBundle\Security\User\SamlUserFactoryInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class SamlUserFactory implements SamlUserFactoryInterface
|
||||
class SamlUserFactory implements SamlUserFactoryInterface, EventSubscriberInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private array $saml_role_mapping;
|
||||
private bool $update_group_on_login;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, ?array $saml_role_mapping, bool $update_group_on_login)
|
||||
{
|
||||
$this->em = $entityManager;
|
||||
if ($saml_role_mapping) {
|
||||
$this->saml_role_mapping = $saml_role_mapping;
|
||||
} else {
|
||||
$this->saml_role_mapping = [];
|
||||
}
|
||||
$this->update_group_on_login = $update_group_on_login;
|
||||
}
|
||||
|
||||
public const SAML_PASSWORD_PLACEHOLDER = '!!SAML!!';
|
||||
|
||||
public function createUser($username, array $attributes = []): UserInterface
|
||||
|
@ -37,8 +57,98 @@ class SamlUserFactory implements SamlUserFactoryInterface
|
|||
//This is a SAML user now!
|
||||
$user->setSamlUser(true);
|
||||
|
||||
//Update basic user information
|
||||
$user->setSamlAttributes($attributes);
|
||||
|
||||
//Check if we can find a group for this user based on the SAML attributes
|
||||
$group = $this->mapSAMLAttributesToLocalGroup($attributes);
|
||||
$user->setGroup($group);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called after a successful authentication. It is used to update the group of the user,
|
||||
* based on the new SAML attributes.
|
||||
* @param AuthenticationSuccessEvent $event
|
||||
* @return void
|
||||
*/
|
||||
public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void
|
||||
{
|
||||
if (! $this->update_group_on_login) {
|
||||
return;
|
||||
}
|
||||
|
||||
$token = $event->getAuthenticationToken();
|
||||
$user = $token->getUser();
|
||||
//Only update the group if the user is a SAML user
|
||||
if (! $token instanceof SamlToken || ! $user instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if we can find a group for this user based on the SAML attributes
|
||||
$group = $this->mapSAMLAttributesToLocalGroup($token->getAttributes());
|
||||
//If needed update the group of the user and save it to DB
|
||||
if ($group !== $user->getGroup()) {
|
||||
$user->setGroup($group);
|
||||
$this->em->flush($user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the given SAML attributes to a local group.
|
||||
* @param array $attributes The SAML attributes
|
||||
* @return Group|null
|
||||
*/
|
||||
public function mapSAMLAttributesToLocalGroup(array $attributes): ?Group
|
||||
{
|
||||
//Extract the roles from the SAML attributes
|
||||
$roles = $attributes['group'] ?? [];
|
||||
$group_id = $this->mapSAMLRolesToLocalGroupID($roles);
|
||||
|
||||
//Check if we can find a group with the given ID
|
||||
if ($group_id !== null) {
|
||||
$group = $this->em->find(Group::class, $group_id);
|
||||
if ($group !== null) {
|
||||
return $group;
|
||||
}
|
||||
}
|
||||
|
||||
//If no group was found, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a list of SAML roles to a local group ID.
|
||||
* @param array $roles The list of SAML roles
|
||||
* @param array $map|null The mapping from SAML roles. If null, the global mapping will be used.
|
||||
* @return int|null The ID of the local group or null if no mapping was found.
|
||||
*/
|
||||
public function mapSAMLRolesToLocalGroupID(array $roles, array $map = null): ?int
|
||||
{
|
||||
$map = $map ?? $this->saml_role_mapping;
|
||||
|
||||
//Iterate over all roles and check if we have a mapping for it.
|
||||
foreach ($roles as $role) {
|
||||
if (array_key_exists($role, $map)) {
|
||||
//We use the first available mapping
|
||||
return (int) $map[$role];
|
||||
}
|
||||
}
|
||||
|
||||
//If no applicable mapping was found, check if we have a default mapping
|
||||
if (array_key_exists('*', $map)) {
|
||||
return (int) $map['*'];
|
||||
}
|
||||
|
||||
//If no mapping was found, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
AuthenticationSuccessEvent::class => 'onAuthenticationSuccess',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -62,4 +62,27 @@ class SamlUserFactoryTest extends WebTestCase
|
|||
$this->assertEquals('IT', $user->getDepartment());
|
||||
$this->assertEquals('j.doe@invalid.invalid', $user->getEmail());
|
||||
}
|
||||
|
||||
public function testMapSAMLRolesToLocalGroupID()
|
||||
{
|
||||
$mapping = [
|
||||
'employee' => 1,
|
||||
'admin' => 2,
|
||||
'manager' => 3,
|
||||
'administrator' => 2,
|
||||
'*' => 4,
|
||||
];
|
||||
|
||||
//Test if mapping works
|
||||
$this->assertEquals(1, $this->service->mapSAMLRolesToLocalGroupID(['employee'], $mapping));
|
||||
//Only the first valid mapping should be used
|
||||
$this->assertEquals(1, $this->service->mapSAMLRolesToLocalGroupID(['employee', 'admin'], $mapping));
|
||||
$this->assertSame(2, $this->service->mapSAMLRolesToLocalGroupID(['does_not_matter', 'admin', 'employee'], $mapping));
|
||||
//Test if mapping is case sensitive
|
||||
$this->assertEquals(4, $this->service->mapSAMLRolesToLocalGroupID(['ADMIN'], $mapping));
|
||||
|
||||
//Test that wildcard mapping works
|
||||
$this->assertEquals(4, $this->service->mapSAMLRolesToLocalGroupID(['entry1', 'entry2'], $mapping));
|
||||
$this->assertEquals(4, $this->service->mapSAMLRolesToLocalGroupID([], $mapping));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue