Skip to content
28 changes: 26 additions & 2 deletions lib/Controller/SAMLController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use OCA\User_SAML\UserResolver;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
Expand All @@ -29,6 +30,8 @@
use OCP\Security\ICrypto;
use OCP\Security\ITrustedDomainHelper;
use OCP\Server;
use OCP\User\Events\UserLoggedInEvent;
use OCP\User\Events\UserLoggedOutEvent;
use OneLogin\Saml2\Auth;
use OneLogin\Saml2\Error;
use OneLogin\Saml2\Settings;
Expand Down Expand Up @@ -63,6 +66,7 @@ class SAMLController extends Controller {
*/
private $crypto;
private ITrustedDomainHelper $trustedDomainHelper;
private IEventDispatcher $eventDispatcher;

public function __construct(
string $appName,
Expand All @@ -78,7 +82,8 @@ public function __construct(
UserResolver $userResolver,
UserData $userData,
ICrypto $crypto,
ITrustedDomainHelper $trustedDomainHelper
ITrustedDomainHelper $trustedDomainHelper,
IEventDispatcher $eventDispatcher
) {
parent::__construct($appName, $request);
$this->session = $session;
Expand All @@ -93,6 +98,7 @@ public function __construct(
$this->userData = $userData;
$this->crypto = $crypto;
$this->trustedDomainHelper = $trustedDomainHelper;
$this->eventDispatcher = $eventDispatcher;
}

/**
Expand Down Expand Up @@ -222,25 +228,31 @@ public function login(int $idp = 1): Http\RedirectResponse {

$response->addCookie('saml_data', $data, null, 'None');
break;


case 'environment-variable':
$ssoUrl = $originalUrl;
if (empty($ssoUrl)) {
$ssoUrl = $this->urlGenerator->getAbsoluteURL('/');
}
$this->session->set('user_saml.samlUserData', $_SERVER);

try {
$this->userData->setAttributes($this->session->get('user_saml.samlUserData'));
$this->autoprovisionIfPossible();
$user = $this->userResolver->findExistingUser($this->userBackend->getCurrentUserId());
$firstLogin = $user->updateLastLoginTimestamp();
if ($firstLogin) {
$this->userBackend->initializeHomeDir($user->getUID());
}
}
$this->eventDispatcher->dispatchTyped(new UserLoggedInEvent($user, $user->getUID(), null, false));

} catch (NoUserFoundException $e) {
if ($e->getMessage()) {
$this->logger->warning('Error while trying to login using sso environment variable: ' . $e->getMessage(), ['app' => 'user_saml']);
}
$ssoUrl = $this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.notProvisioned');

} catch (UserFilterViolationException $e) {
$this->logger->info(
'SAML filter constraints not met: {msg}',
Expand All @@ -251,11 +263,14 @@ public function login(int $idp = 1): Http\RedirectResponse {
);
$ssoUrl = $this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.notPermitted');
}

$response = new Http\RedirectResponse($ssoUrl);
if (isset($e)) {
$this->session->clear();
}
break;


default:
throw new \Exception(
sprintf(
Expand Down Expand Up @@ -367,11 +382,13 @@ public function assertionConsumerService(): Http\RedirectResponse {
try {
$this->userData->setAttributes($auth->getAttributes());
$this->autoprovisionIfPossible();

} catch (NoUserFoundException $e) {
$this->logger->error($e->getMessage(), ['app' => $this->appName]);
$response = new Http\RedirectResponse($this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.notProvisioned'));
$response->invalidateCookie('saml_data');
return $response;

} catch (UserFilterViolationException $e) {
$this->logger->error($e->getMessage(), ['app' => $this->appName]);
$response = new Http\RedirectResponse($this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.notPermitted'));
Expand Down Expand Up @@ -413,6 +430,8 @@ public function assertionConsumerService(): Http\RedirectResponse {
$response->addCookie('_shibsession_', 'authenticated');
}

$this->eventDispatcher->dispatchTyped(new UserLoggedInEvent($user, $user->getUID(), null, false));

$response->invalidateCookie('saml_data');
return $response;
}
Expand All @@ -425,6 +444,8 @@ public function assertionConsumerService(): Http\RedirectResponse {
* @throws Error
*/
public function singleLogoutService(): Http\RedirectResponse {
$user = $this->userResolver->findExistingUser($this->userBackend->getCurrentUserId());

$isFromGS = ($this->config->getSystemValue('gs.enabled', false) &&
$this->config->getSystemValue('gss.mode', '') === 'master');

Expand Down Expand Up @@ -486,10 +507,13 @@ public function singleLogoutService(): Http\RedirectResponse {
} catch (Error $e) {
$this->logger->warning($e->getMessage(), ['exception' => $e, 'app' => $this->appName]);
$this->userSession->logout();
$this->eventDispatcher->dispatchTyped(new UserLoggedOutEvent($user));
}
}

if (!empty($targetUrl) && !$auth->getLastErrorReason()) {
$this->userSession->logout();
$this->eventDispatcher->dispatchTyped(new UserLoggedOutEvent($user));
}
}
if (empty($targetUrl)) {
Expand Down
65 changes: 36 additions & 29 deletions lib/UserBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use OCP\User\Backend\IGetDisplayNameBackend;
use OCP\User\Backend\IGetHomeBackend;
use OCP\User\Events\UserChangedEvent;
use OCP\User\Events\UserDeletedEvent;
use OCP\User\Events\UserFirstTimeLoggedInEvent;
use OCP\UserInterface;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -103,41 +104,43 @@ protected function userExistsInDatabase(string $uid): bool {
* @param array $attributes
*/
public function createUserIfNotExists(string $uid, array $attributes = []): void {
if (!$this->userExistsInDatabase($uid)) {
$values = [
'uid' => $uid,
];

// Try to get the mapped home directory of the user
try {
$home = $this->getAttributeValue('saml-attribute-mapping-home_mapping', $attributes);
} catch (\InvalidArgumentException) {
$home = '';
}
if ($this->userExistsInDatabase($uid)) {
return;
}

if ($home !== '') {
//if attribute's value is an absolute path take this, otherwise append it to data dir
//check for / at the beginning or pattern c:\ resp. c:/
if ($home[0] !== '/'
&& !(strlen($home) > 3 && ctype_alpha($home[0])
&& $home[1] === ':' && ($home[2] === '\\' || $home[2] === '/'))
) {
$home = $this->config->getSystemValue('datadirectory',
\OC::$SERVERROOT.'/data') . '/' . $home;
}
$values = [
'uid' => $uid,
];

$values['home'] = $home;
}
// Try to get the mapped home directory of the user
try {
$home = $this->getAttributeValue('saml-attribute-mapping-home_mapping', $attributes);
} catch (\InvalidArgumentException) {
$home = '';
}

$qb = $this->db->getQueryBuilder();
$qb->insert('user_saml_users');
foreach ($values as $column => $value) {
$qb->setValue($column, $qb->createNamedParameter($value));
if ($home !== '') {
//if attribute's value is an absolute path take this, otherwise append it to data dir
//check for / at the beginning or pattern c:\ resp. c:/
if ($home[0] !== '/'
&& !(strlen($home) > 3 && ctype_alpha($home[0])
&& $home[1] === ':' && ($home[2] === '\\' || $home[2] === '/'))
) {
$home = $this->config->getSystemValue('datadirectory',
\OC::$SERVERROOT.'/data') . '/' . $home;
}
$qb->execute();

$this->initializeHomeDir($uid);
$values['home'] = $home;
}

$qb = $this->db->getQueryBuilder();
$qb->insert('user_saml_users');
foreach ($values as $column => $value) {
$qb->setValue($column, $qb->createNamedParameter($value));
}
$qb->execute();

$this->initializeHomeDir($uid);
}

/**
Expand Down Expand Up @@ -169,6 +172,10 @@ public function deleteUser($uid) {
$affected = $qb->delete('user_saml_users')
->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))
->executeStatement();

$user = $this->userManager->get($uid);
$this->eventDispatcher->dispatchTyped(new UserDeletedEvent($user));

return $affected > 0;
}

Expand Down
12 changes: 11 additions & 1 deletion tests/unit/Controller/SAMLControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use OCA\User_SAML\UserResolver;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
Expand All @@ -25,6 +26,7 @@
use OCP\IUserSession;
use OCP\Security\ICrypto;
use OCP\Security\ITrustedDomainHelper;
use OCP\User\Events\UserLoggedInEvent;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
Expand Down Expand Up @@ -57,6 +59,7 @@ class SAMLControllerTest extends TestCase {
/** @var SAMLController */
private $samlController;
private ITrustedDomainHelper|MockObject $trustedDomainController;
private IEventDispatcher $eventDispatcher;

protected function setUp(): void {
parent::setUp();
Expand All @@ -74,6 +77,7 @@ protected function setUp(): void {
$this->userData = $this->createMock(UserData::class);
$this->crypto = $this->createMock(ICrypto::class);
$this->trustedDomainController = $this->createMock(ITrustedDomainHelper::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);

$this->l->expects($this->any())->method('t')->willReturnCallback(
function ($param) {
Expand All @@ -100,7 +104,8 @@ function ($param) {
$this->userResolver,
$this->userData,
$this->crypto,
$this->trustedDomainController
$this->trustedDomainController,
$this->eventDispatcher,
);
}

Expand Down Expand Up @@ -321,6 +326,11 @@ public function testLoginWithEnvVariable(array $samlUserData, string $redirect,
->method('createUserIfNotExists')
->with('MyUid');

$this->eventDispatcher
->expects($this->once())
->method('dispatchTyped')
->with(new UserLoggedInEvent($user, 'MyUid', null, false));

$expected = new RedirectResponse($redirect);
$result = $this->samlController->login(1);
$this->assertEquals($expected, $result);
Expand Down