[full-ci] feat: implement OIDC authentication option (#1676)
* feat: implement Bearer Token authentication option * fix
This commit is contained in:
@@ -338,6 +338,7 @@ config = {
|
||||
"FRONTEND_READONLY_USER_ATTRIBUTES": "user.onPremisesSamAccountName,user.displayName,user.mail,user.passwordProfile,user.accountEnabled,user.appRoleAssignments",
|
||||
"OC_LDAP_SERVER_WRITE_ENABLED": False,
|
||||
"OC_EXCLUDE_RUN_SERVICES": "idm",
|
||||
"OC_LDAP_USER_ENABLED_ATTRIBUTE": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1076,6 +1077,7 @@ def localApiTests(name, suites, storage = "decomposed", extra_environment = {},
|
||||
"WITH_REMOTE_PHP": with_remote_php,
|
||||
"COLLABORATION_SERVICE_URL": "http://wopi-fakeoffice:9300",
|
||||
"OC_STORAGE_PATH": "$HOME/.opencloud/storage/users",
|
||||
"USE_BEARER_TOKEN": True,
|
||||
}
|
||||
|
||||
for item in extra_environment:
|
||||
|
||||
@@ -197,34 +197,6 @@ class GraphHelper {
|
||||
return $baseUrl . '/graph/v1beta1/' . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $baseUrl
|
||||
* @param string $xRequestId
|
||||
* @param string $method
|
||||
* @param string $path
|
||||
* @param string|null $body
|
||||
* @param array|null $headers
|
||||
*
|
||||
* @return RequestInterface
|
||||
*/
|
||||
public static function createRequest(
|
||||
string $baseUrl,
|
||||
string $xRequestId,
|
||||
string $method,
|
||||
string $path,
|
||||
?string $body = null,
|
||||
?array $headers = []
|
||||
): RequestInterface {
|
||||
$fullUrl = self::getFullUrl($baseUrl, $path);
|
||||
return HttpRequestHelper::createRequest(
|
||||
$fullUrl,
|
||||
$xRequestId,
|
||||
$method,
|
||||
$headers,
|
||||
$body
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $baseUrl
|
||||
* @param string $xRequestId
|
||||
@@ -1908,7 +1880,7 @@ class GraphHelper {
|
||||
string $permissionsId
|
||||
): ResponseInterface {
|
||||
$url = self::getBetaFullUrl($baseUrl, "drives/$spaceId/items/$itemId/permissions/$permissionsId");
|
||||
return HttpRequestHelper::sendRequestOnce(
|
||||
return HttpRequestHelper::sendRequest(
|
||||
$url,
|
||||
$xRequestId,
|
||||
'PATCH',
|
||||
@@ -2264,7 +2236,7 @@ class GraphHelper {
|
||||
): ResponseInterface {
|
||||
$url = self::getBetaFullUrl($baseUrl, "drives/$spaceId/root/permissions/$permissionsId");
|
||||
|
||||
return HttpRequestHelper::sendRequestOnce(
|
||||
return HttpRequestHelper::sendRequest(
|
||||
$url,
|
||||
$xRequestId,
|
||||
'PATCH',
|
||||
|
||||
@@ -74,6 +74,7 @@ class HttpRequestHelper {
|
||||
* than download it all up-front.
|
||||
* @param int|null $timeout
|
||||
* @param Client|null $client
|
||||
* @param string|null $bearerToken
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws GuzzleException
|
||||
@@ -90,7 +91,8 @@ class HttpRequestHelper {
|
||||
?CookieJar $cookies = null,
|
||||
bool $stream = false,
|
||||
?int $timeout = 0,
|
||||
?Client $client = null
|
||||
?Client $client = null,
|
||||
?string $bearerToken = null
|
||||
): ResponseInterface {
|
||||
if ($client === null) {
|
||||
$client = self::createClient(
|
||||
@@ -99,7 +101,8 @@ class HttpRequestHelper {
|
||||
$config,
|
||||
$cookies,
|
||||
$stream,
|
||||
$timeout
|
||||
$timeout,
|
||||
$bearerToken
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,6 +203,13 @@ class HttpRequestHelper {
|
||||
} else {
|
||||
$debugResponses = false;
|
||||
}
|
||||
// use basic auth for 'public' user or no user
|
||||
if ($user === 'public' || $user === null || $user === '') {
|
||||
$bearerToken = null;
|
||||
} else {
|
||||
$useBearerToken = TokenHelper::useBearerToken();
|
||||
$bearerToken = $useBearerToken ? TokenHelper::getTokens($user, $password, $url)['access_token'] : null;
|
||||
}
|
||||
|
||||
$sendRetryLimit = self::numRetriesOnHttpTooEarly();
|
||||
$sendCount = 0;
|
||||
@@ -217,7 +227,8 @@ class HttpRequestHelper {
|
||||
$cookies,
|
||||
$stream,
|
||||
$timeout,
|
||||
$client
|
||||
$client,
|
||||
$bearerToken,
|
||||
);
|
||||
|
||||
if ($response->getStatusCode() >= 400
|
||||
@@ -348,6 +359,7 @@ class HttpRequestHelper {
|
||||
* @param bool $stream Set to true to stream a response rather
|
||||
* than download it all up-front.
|
||||
* @param int|null $timeout
|
||||
* @param string|null $bearerToken
|
||||
*
|
||||
* @return Client
|
||||
*/
|
||||
@@ -357,10 +369,13 @@ class HttpRequestHelper {
|
||||
?array $config = null,
|
||||
?CookieJar $cookies = null,
|
||||
?bool $stream = false,
|
||||
?int $timeout = 0
|
||||
?int $timeout = 0,
|
||||
?string $bearerToken = null
|
||||
): Client {
|
||||
$options = [];
|
||||
if ($user !== null) {
|
||||
if ($bearerToken !== null) {
|
||||
$options['headers']['Authorization'] = 'Bearer ' . $bearerToken;
|
||||
} elseif ($user !== null) {
|
||||
$options['auth'] = [$user, $password];
|
||||
}
|
||||
if ($config !== null) {
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
<?php
|
||||
/**
|
||||
* @author Viktor Scharf <v.scharf@opencloud.eu>
|
||||
* @copyright Copyright (c) 2025 Viktor Scharf <v.scharf@opencloud.eu>
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License,
|
||||
* as published by the Free Software Foundation;
|
||||
* either version 3 of the License, or any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
namespace TestHelpers;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Helper for obtaining bearer tokens for users
|
||||
*/
|
||||
class TokenHelper {
|
||||
private const LOGON_URL = '/signin/v1/identifier/_/logon';
|
||||
private const REDIRECT_URL = '/oidc-callback.html';
|
||||
private const TOKEN_URL = '/konnect/v1/token';
|
||||
|
||||
// Static cache [username => token_data]
|
||||
private static array $tokenCache = [];
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public static function useBearerToken(): bool {
|
||||
return \getenv('USE_BEARER_TOKEN') === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts base URL from a full URL
|
||||
*
|
||||
* @param string $url
|
||||
*
|
||||
* @return string the base URL
|
||||
*/
|
||||
private static function extractBaseUrl(string $url): string {
|
||||
return preg_replace('#(https?://[^/]+).*#', '$1', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access and refresh tokens for a user
|
||||
* Uses cache to avoid unnecessary requests
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $url
|
||||
*
|
||||
* @return array ['access_token' => string, 'refresh_token' => string, 'expires_at' => int]
|
||||
* @throws GuzzleException
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function getTokens(string $username, string $password, string $url): array {
|
||||
// Extract base URL. I need to send $url to get correct server in case of multiple servers (ocm suite)
|
||||
$baseUrl = self::extractBaseUrl($url);
|
||||
$cacheKey = $username . '|' . $baseUrl;
|
||||
|
||||
// Check cache
|
||||
if (isset(self::$tokenCache[$cacheKey])) {
|
||||
$cachedToken = self::$tokenCache[$cacheKey];
|
||||
|
||||
// Check if access token has expired
|
||||
if (time() < $cachedToken['expires_at']) {
|
||||
return $cachedToken;
|
||||
}
|
||||
|
||||
$refreshedToken = self::refreshToken($cachedToken['refresh_token'], $baseUrl);
|
||||
$tokenData = [
|
||||
'access_token' => $refreshedToken['access_token'],
|
||||
'refresh_token' => $refreshedToken['refresh_token'],
|
||||
'expires_at' => time() + 300 // 5 minutes
|
||||
];
|
||||
self::$tokenCache[$cacheKey] = $tokenData;
|
||||
return $tokenData;
|
||||
}
|
||||
|
||||
// Get new tokens
|
||||
$cookieJar = new CookieJar();
|
||||
|
||||
$continueUrl = self::getAuthorizedEndPoint($username, $password, $baseUrl, $cookieJar);
|
||||
$code = self::getCode($continueUrl, $baseUrl, $cookieJar);
|
||||
$tokens = self::getToken($code, $baseUrl, $cookieJar);
|
||||
|
||||
$tokenData = [
|
||||
'access_token' => $tokens['access_token'],
|
||||
'refresh_token' => $tokens['refresh_token'],
|
||||
'expires_at' => time() + 290 // set expiry to 290 seconds to allow for some buffer
|
||||
];
|
||||
|
||||
// Save to cache
|
||||
self::$tokenCache[$cacheKey] = $tokenData;
|
||||
|
||||
return $tokenData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh token
|
||||
*
|
||||
* @param string $refreshToken
|
||||
* @param string $baseUrl
|
||||
*
|
||||
* @return array
|
||||
* @throws GuzzleException
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function refreshToken(string $refreshToken, string $baseUrl): array {
|
||||
$client = new Client(
|
||||
[
|
||||
'verify' => false,
|
||||
'http_errors' => false,
|
||||
'allow_redirects' => false
|
||||
]
|
||||
);
|
||||
|
||||
$response = $client->post(
|
||||
$baseUrl . self::TOKEN_URL,
|
||||
[
|
||||
'form_params' => [
|
||||
'client_id' => 'web',
|
||||
'refresh_token' => $refreshToken,
|
||||
'grant_type' => 'refresh_token'
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
throw new Exception(
|
||||
\sprintf(
|
||||
'Token refresh failed: Expected status code 200 but received %d. Message: %s',
|
||||
$response->getStatusCode(),
|
||||
$response->getReasonPhrase()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
if (!isset($data['access_token']) || !isset($data['refresh_token'])) {
|
||||
throw new Exception('Missing tokens in refresh response');
|
||||
}
|
||||
|
||||
return [
|
||||
'access_token' => $data['access_token'],
|
||||
'refresh_token' => $data['refresh_token']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached tokens for a specific user
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $url
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function clearUserTokens(string $username, string $url): void {
|
||||
$baseUrl = self::extractBaseUrl($url);
|
||||
$cacheKey = $username . '|' . $baseUrl;
|
||||
unset(self::$tokenCache[$cacheKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached tokens
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function clearAllTokens(): void {
|
||||
self::$tokenCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $baseUrl
|
||||
* @param CookieJar $cookieJar
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public static function makeLoginRequest(
|
||||
string $username,
|
||||
string $password,
|
||||
string $baseUrl,
|
||||
CookieJar $cookieJar
|
||||
): \Psr\Http\Message\ResponseInterface {
|
||||
$client = new Client(
|
||||
[
|
||||
'verify' => false,
|
||||
'http_errors' => false,
|
||||
'allow_redirects' => false,
|
||||
'cookies' => $cookieJar
|
||||
]
|
||||
);
|
||||
|
||||
return $client->post(
|
||||
$baseUrl . self::LOGON_URL,
|
||||
[
|
||||
'headers' => [
|
||||
'Kopano-Konnect-XSRF' => '1',
|
||||
'Referer' => $baseUrl,
|
||||
'Content-Type' => 'application/json'
|
||||
],
|
||||
'json' => [
|
||||
'params' => [$username, $password, '1'],
|
||||
'hello' => [
|
||||
'scope' => 'openid profile offline_access email',
|
||||
'client_id' => 'web',
|
||||
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
|
||||
'flow' => 'oidc'
|
||||
]
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1: Login and get continue_uri
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $baseUrl
|
||||
* @param CookieJar $cookieJar
|
||||
*
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function getAuthorizedEndPoint(
|
||||
string $username,
|
||||
string $password,
|
||||
string $baseUrl,
|
||||
CookieJar $cookieJar
|
||||
): string {
|
||||
$response = self::makeLoginRequest($username, $password, $baseUrl, $cookieJar);
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
throw new Exception(
|
||||
\sprintf(
|
||||
'Logon failed: Expected status code 200 but received %d. Message: %s',
|
||||
$response->getStatusCode(),
|
||||
$response->getReasonPhrase()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
if (!isset($data['hello']['continue_uri'])) {
|
||||
throw new Exception('Missing continue_uri in logon response');
|
||||
}
|
||||
|
||||
return $data['hello']['continue_uri'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2: Authorization and get code
|
||||
*
|
||||
* @param string $continueUrl
|
||||
* @param string $baseUrl
|
||||
* @param CookieJar $cookieJar
|
||||
*
|
||||
* @return string
|
||||
* @throws GuzzleException
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function getCode(string $continueUrl, string $baseUrl, CookieJar $cookieJar): string {
|
||||
$client = new Client(
|
||||
[
|
||||
'verify' => false,
|
||||
'http_errors' => false,
|
||||
'allow_redirects' => false, // Disable automatic redirects
|
||||
'cookies' => $cookieJar
|
||||
]
|
||||
);
|
||||
|
||||
$params = [
|
||||
'client_id' => 'web',
|
||||
'prompt' => 'none',
|
||||
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
|
||||
'response_mode' => 'query',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'openid profile offline_access email'
|
||||
];
|
||||
|
||||
$response = $client->get(
|
||||
$continueUrl,
|
||||
[
|
||||
'query' => $params
|
||||
]
|
||||
);
|
||||
|
||||
if ($response->getStatusCode() !== 302) {
|
||||
// Add debugging to understand what is happening
|
||||
$body = $response->getBody()->getContents();
|
||||
throw new Exception(
|
||||
\sprintf(
|
||||
'Authorization failed: Expected status code 302 but received %d. Message: %s. Body: %s',
|
||||
$response->getStatusCode(),
|
||||
$response->getReasonPhrase(),
|
||||
$body
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$location = $response->getHeader('Location')[0] ?? '';
|
||||
|
||||
if (empty($location)) {
|
||||
throw new Exception('Missing Location header in authorization response');
|
||||
}
|
||||
|
||||
parse_str(parse_url($location, PHP_URL_QUERY), $queryParams);
|
||||
|
||||
// Check for errors
|
||||
if (isset($queryParams['error'])) {
|
||||
throw new Exception(
|
||||
\sprintf(
|
||||
'Authorization error: %s - %s',
|
||||
$queryParams['error'],
|
||||
urldecode($queryParams['error_description'] ?? 'No description')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!isset($queryParams['code'])) {
|
||||
throw new Exception('Missing auth code in redirect URL. Location: ' . $location);
|
||||
}
|
||||
|
||||
return $queryParams['code'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3: Get token
|
||||
*
|
||||
* @param string $code
|
||||
* @param string $baseUrl
|
||||
* @param CookieJar $cookieJar
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws GuzzleException
|
||||
* @throws Exception
|
||||
*
|
||||
*/
|
||||
private static function getToken(string $code, string $baseUrl, CookieJar $cookieJar): array {
|
||||
$client = new Client(
|
||||
[
|
||||
'verify' => false,
|
||||
'http_errors' => false,
|
||||
'allow_redirects' => false,
|
||||
'cookies' => $cookieJar
|
||||
]
|
||||
);
|
||||
|
||||
$response = $client->post(
|
||||
$baseUrl . self::TOKEN_URL,
|
||||
[
|
||||
'form_params' => [
|
||||
'client_id' => 'web',
|
||||
'code' => $code,
|
||||
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
|
||||
'grant_type' => 'authorization_code'
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
throw new Exception(
|
||||
\sprintf(
|
||||
'Token request failed: Expected status code 200 but received %d. Message: %s',
|
||||
$response->getStatusCode(),
|
||||
$response->getReasonPhrase()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
if (!isset($data['access_token']) || !isset($data['refresh_token'])) {
|
||||
throw new Exception('Missing tokens in response');
|
||||
}
|
||||
|
||||
return [
|
||||
'access_token' => $data['access_token'],
|
||||
'refresh_token' => $data['refresh_token']
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ use Behat\Behat\Context\Context;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TestHelpers\HttpRequestHelper;
|
||||
use TestHelpers\BehatHelper;
|
||||
use TestHelpers\TokenHelper;
|
||||
use TestHelpers\WebDavHelper;
|
||||
|
||||
/**
|
||||
@@ -714,4 +715,27 @@ class AuthContext implements Context {
|
||||
);
|
||||
$this->featureContext->setResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When user :user should not be able to log in with wrong password :password
|
||||
*
|
||||
* @param string $user
|
||||
* @param string $password
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function userShouldNotBeAbleToLogInWithWrongPassword(
|
||||
string $user,
|
||||
string $password
|
||||
): void {
|
||||
TokenHelper::clearUserTokens($user, $this->featureContext->getBaseUrl());
|
||||
$response = TokenHelper::makeLoginRequest(
|
||||
$user,
|
||||
$password,
|
||||
$this->featureContext->getBaseUrl(),
|
||||
new \GuzzleHttp\Cookie\CookieJar()
|
||||
);
|
||||
// why is not 401 returned?
|
||||
$this->featureContext->theHTTPStatusCodeShouldBe(204, 'should not be able to log in', $response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use TestHelpers\GraphHelper;
|
||||
use TestHelpers\WebDavHelper;
|
||||
use TestHelpers\HttpRequestHelper;
|
||||
use TestHelpers\BehatHelper;
|
||||
use TestHelpers\TokenHelper;
|
||||
|
||||
require_once 'bootstrap.php';
|
||||
|
||||
@@ -2864,6 +2865,7 @@ class GraphContext implements Context {
|
||||
);
|
||||
$this->featureContext->theHTTPStatusCodeShouldBe(200, '', $response);
|
||||
$this->featureContext->updateUsernameInCreatedUserList($byUser, $userName);
|
||||
TokenHelper::clearUserTokens($byUser, $this->featureContext->getBaseUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,7 @@ use TestHelpers\WebDavHelper;
|
||||
use TestHelpers\GraphHelper;
|
||||
use Laminas\Ldap\Exception\LdapException;
|
||||
use Laminas\Ldap\Ldap;
|
||||
use TestHelpers\TokenHelper;
|
||||
|
||||
/**
|
||||
* Functions for provisioning of users and groups
|
||||
@@ -558,110 +559,65 @@ trait Provisioning {
|
||||
*/
|
||||
public function usersHaveBeenCreated(
|
||||
TableNode $table,
|
||||
bool $useDefault=true,
|
||||
bool $initialize=true
|
||||
bool $useDefault = true,
|
||||
bool $initialize = true
|
||||
) {
|
||||
$this->verifyTableNodeColumns($table, ['username'], ['displayname', 'email', 'password']);
|
||||
$table = $table->getColumnsHash();
|
||||
$users = $this->buildUsersAttributesArray($useDefault, $table);
|
||||
|
||||
$requests = [];
|
||||
$client = HttpRequestHelper::createClient(
|
||||
$this->getAdminUsername(),
|
||||
$this->getAdminPassword()
|
||||
);
|
||||
|
||||
foreach ($users as $userAttributes) {
|
||||
$userName = $userAttributes['userid'];
|
||||
$password = $userAttributes['password'];
|
||||
$displayName = $userAttributes['displayName'];
|
||||
$email = $userAttributes['email'];
|
||||
|
||||
if ($this->isTestingWithLdap()) {
|
||||
$this->createLdapUser($userAttributes);
|
||||
} else {
|
||||
$attributesToCreateUser['userid'] = $userAttributes['userid'];
|
||||
$attributesToCreateUser['password'] = $userAttributes['password'];
|
||||
$attributesToCreateUser['displayname'] = $userAttributes['displayName'];
|
||||
if ($userAttributes['email'] === null) {
|
||||
Assert::assertArrayHasKey(
|
||||
'userid',
|
||||
$userAttributes,
|
||||
__METHOD__ . " userAttributes array does not have key 'userid'"
|
||||
try {
|
||||
$this->createLdapUser($userAttributes);
|
||||
} catch (LdapException $exception) {
|
||||
throw new Exception(
|
||||
__METHOD__ . " cannot create a LDAP user with provided data. Error: $exception"
|
||||
);
|
||||
$attributesToCreateUser['email'] = $userAttributes['userid'] . '@opencloud.eu';
|
||||
} else {
|
||||
$attributesToCreateUser['email'] = $userAttributes['email'];
|
||||
}
|
||||
$body = GraphHelper::prepareCreateUserPayload(
|
||||
$attributesToCreateUser['userid'],
|
||||
$attributesToCreateUser['password'],
|
||||
$attributesToCreateUser['email'],
|
||||
$attributesToCreateUser['displayname']
|
||||
);
|
||||
$request = GraphHelper::createRequest(
|
||||
} else {
|
||||
// Use the same logic as userHasBeenCreated for email generation
|
||||
if ($email === null) {
|
||||
$email = $this->getEmailAddressForUser($userName);
|
||||
if ($email === null) {
|
||||
// escape @ & space if present in userId
|
||||
$email = \str_replace(["@", " "], "", $userName) . '@opencloud.eu';
|
||||
}
|
||||
}
|
||||
|
||||
$userName = $this->getActualUsername($userName);
|
||||
$userName = \trim($userName);
|
||||
|
||||
$response = GraphHelper::createUser(
|
||||
$this->getBaseUrl(),
|
||||
$this->getStepLineRef(),
|
||||
"POST",
|
||||
'users',
|
||||
$body,
|
||||
$this->getAdminUsername(),
|
||||
$this->getAdminPassword(),
|
||||
$userName,
|
||||
$password,
|
||||
$email,
|
||||
$displayName,
|
||||
);
|
||||
// Add the request to the $requests array so that they can be sent in parallel.
|
||||
$requests[] = $request;
|
||||
|
||||
Assert::assertEquals(
|
||||
201,
|
||||
$response->getStatusCode(),
|
||||
__METHOD__ . " cannot create user '$userName' using Graph API.\nResponse:" .
|
||||
json_encode($this->getJsonDecodedResponse($response))
|
||||
);
|
||||
|
||||
$userId = $this->getJsonDecodedResponse($response)['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$exceptionToThrow = null;
|
||||
if (!$this->isTestingWithLdap()) {
|
||||
$results = HttpRequestHelper::sendBatchRequest($requests, $client);
|
||||
// Check all requests to inspect failures.
|
||||
foreach ($results as $key => $e) {
|
||||
if ($e instanceof ClientException) {
|
||||
$responseBody = $this->getJsonDecodedResponse($e->getResponse());
|
||||
$httpStatusCode = $e->getResponse()->getStatusCode();
|
||||
$graphStatusCode = $responseBody['error']['code'];
|
||||
$messageText = $responseBody['error']['message'];
|
||||
$exceptionToThrow = new Exception(
|
||||
__METHOD__ .
|
||||
" Unexpected failure when creating the user '" .
|
||||
$users[$key]['userid'] . "'" .
|
||||
"\nHTTP status $httpStatusCode " .
|
||||
"\nGraph status $graphStatusCode " .
|
||||
"\nError message $messageText"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->addUserToCreatedUsersList($userName, $password, $displayName, $email, $userId ?? null);
|
||||
|
||||
// Create requests for setting displayname and email for the newly created users.
|
||||
// These values cannot be set while creating the user, so we have to edit the newly created user to set these values.
|
||||
foreach ($users as $userAttributes) {
|
||||
if (!$this->isTestingWithLdap()) {
|
||||
// for graph api, we need to save the user id to be able to add it in some group
|
||||
// can be fetched with the "onPremisesSamAccountName" i.e. userid
|
||||
$response = $this->graphContext->adminHasRetrievedUserUsingTheGraphApi($userAttributes['userid']);
|
||||
$userAttributes['id'] = $this->getJsonDecodedResponse($response)['id'];
|
||||
} else {
|
||||
$userAttributes['id'] = null;
|
||||
}
|
||||
$this->addUserToCreatedUsersList(
|
||||
$userAttributes['userid'],
|
||||
$userAttributes['password'],
|
||||
$userAttributes['displayName'],
|
||||
$userAttributes['email'],
|
||||
$userAttributes['id']
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($exceptionToThrow)) {
|
||||
throw $exceptionToThrow;
|
||||
}
|
||||
|
||||
foreach ($users as $user) {
|
||||
Assert::assertTrue(
|
||||
$this->userExists($user["userid"]),
|
||||
"User '" . $user["userid"] . "' should exist but does not exist"
|
||||
);
|
||||
}
|
||||
|
||||
if ($initialize) {
|
||||
foreach ($users as $user) {
|
||||
$this->initializeUser($user['userid'], $user['password']);
|
||||
if ($initialize) {
|
||||
$this->initializeUser($userName, $password);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -841,45 +797,16 @@ trait Provisioning {
|
||||
*/
|
||||
public function userHasBeenDeleted(string $user): void {
|
||||
$user = $this->getActualUsername($user);
|
||||
if ($this->userExists($user)) {
|
||||
if ($this->isTestingWithLdap() && \in_array($user, $this->ldapCreatedUsers)) {
|
||||
$this->deleteLdapUser($user);
|
||||
} else {
|
||||
$response = $this->deleteUser($user);
|
||||
$this->theHTTPStatusCodeShouldBe(204, "", $response);
|
||||
WebDavHelper::removeSpaceIdReferenceForUser($user);
|
||||
}
|
||||
if ($this->isTestingWithLdap() && \in_array($user, $this->ldapCreatedUsers)) {
|
||||
$this->deleteLdapUser($user);
|
||||
} else {
|
||||
$response = $this->deleteUser($user);
|
||||
$this->theHTTPStatusCodeShouldBe(204, "", $response);
|
||||
WebDavHelper::removeSpaceIdReferenceForUser($user);
|
||||
}
|
||||
Assert::assertFalse(
|
||||
$this->userExists($user),
|
||||
"User '$user' should not exist but does exist"
|
||||
);
|
||||
$this->rememberThatUserIsNotExpectedToExist($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given these users have been initialized:
|
||||
* expects a table of users with the heading
|
||||
* "|username|password|"
|
||||
*
|
||||
* @param TableNode $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function theseUsersHaveBeenInitialized(TableNode $table): void {
|
||||
foreach ($table as $row) {
|
||||
if (!isset($row ['password'])) {
|
||||
$password = $this->getPasswordForUser($row ['username']);
|
||||
} else {
|
||||
$password = $row ['password'];
|
||||
}
|
||||
$this->initializeUser(
|
||||
$row ['username'],
|
||||
$password
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get all the existing groups
|
||||
*
|
||||
@@ -961,13 +888,14 @@ trait Provisioning {
|
||||
$url = $this->getBaseUrl()
|
||||
. "/ocs/v$this->ocsApiVersion.php/cloud/users/$user";
|
||||
}
|
||||
|
||||
HttpRequestHelper::get(
|
||||
$url,
|
||||
$this->getStepLineRef(),
|
||||
$user,
|
||||
$password
|
||||
);
|
||||
if ($password !== '') {
|
||||
HttpRequestHelper::get(
|
||||
$url,
|
||||
$this->getStepLineRef(),
|
||||
$user,
|
||||
$password
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1162,12 +1090,6 @@ trait Provisioning {
|
||||
}
|
||||
|
||||
$this->addUserToCreatedUsersList($user, $password, $displayName, $email, $userId);
|
||||
|
||||
Assert::assertTrue(
|
||||
$this->userExists($user),
|
||||
"User '$user' should exist but does not exist"
|
||||
);
|
||||
|
||||
$this->initializeUser($user, $password);
|
||||
}
|
||||
|
||||
@@ -1999,21 +1921,15 @@ trait Provisioning {
|
||||
$this->usingServer('LOCAL');
|
||||
foreach ($this->createdUsers as $userData) {
|
||||
$user = $userData['actualUsername'];
|
||||
TokenHelper::clearUserTokens($user, $this->getBaseUrl());
|
||||
$this->deleteUser($user);
|
||||
Assert::assertFalse(
|
||||
$this->userExists($user),
|
||||
"User '$user' should not exist but does exist"
|
||||
);
|
||||
$this->rememberThatUserIsNotExpectedToExist($user);
|
||||
}
|
||||
$this->usingServer('REMOTE');
|
||||
foreach ($this->createdRemoteUsers as $userData) {
|
||||
$user = $userData['actualUsername'];
|
||||
TokenHelper::clearUserTokens($user, $this->getBaseUrl());
|
||||
$this->deleteUser($user);
|
||||
Assert::assertFalse(
|
||||
$this->userExists($user),
|
||||
"User '$user' should not exist but does exist"
|
||||
);
|
||||
$this->rememberThatUserIsNotExpectedToExist($user);
|
||||
}
|
||||
$this->usingServer($previousServer);
|
||||
|
||||
@@ -219,7 +219,7 @@ Feature: edit user
|
||||
When the user "Brian" resets the password of user "Carol" to "newpassword" using the Graph API
|
||||
Then the HTTP status code should be "403"
|
||||
And the content of file "resetpassword.txt" for user "Carol" using password "1234" should be "test file for reset password"
|
||||
But user "Carol" using password "newpassword" should not be able to download file "resetpassword.txt"
|
||||
And user "Carol" should not be able to log in with wrong password "newpassword"
|
||||
Examples:
|
||||
| user-role | user-role-2 |
|
||||
| Space Admin | Space Admin |
|
||||
|
||||
@@ -542,6 +542,7 @@ Feature: enable or disable sync of incoming shares
|
||||
| sharee | Brian |
|
||||
| shareType | user |
|
||||
| permissionsRole | Viewer |
|
||||
And user "Brian" has a share "textfile0.txt" synced
|
||||
And the user "Admin" has deleted a user "Alice"
|
||||
When user "Brian" disables sync of share "textfile0.txt" using the Graph API
|
||||
Then the HTTP status code should be "204"
|
||||
@@ -820,6 +821,7 @@ Feature: enable or disable sync of incoming shares
|
||||
| sharee | Brian |
|
||||
| shareType | user |
|
||||
| permissionsRole | Viewer |
|
||||
And user "Brian" has a share "<resource>" synced
|
||||
And user "Brian" has disabled sync of last shared resource
|
||||
When user "Brian" disables sync of share "<resource>" using the Graph API
|
||||
Then the HTTP status code should be "409"
|
||||
|
||||
@@ -14,7 +14,7 @@ Feature: reset user password via CLI command
|
||||
But the command output should not contain "Failed to update user password: entry does not exist"
|
||||
And the administrator has started the server
|
||||
And user "Alice" should be able to create folder "newFolder" using password "newpass"
|
||||
But user "Alice" should not be able to create folder "anotherFolder" using password "%alt1%"
|
||||
But user "Alice" should not be able to log in with wrong password "%alt1%"
|
||||
|
||||
|
||||
Scenario: try to reset password of non-existing user
|
||||
|
||||
Reference in New Issue
Block a user