mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch 'cl-1.4.x' into feat-user-identities-cl-1.4.x
This commit is contained in:
@@ -80,3 +80,4 @@ _APP_DOCKER_HUB_USERNAME=
|
||||
_APP_DOCKER_HUB_PASSWORD=
|
||||
_APP_CONSOLE_GITHUB_SECRET=
|
||||
_APP_CONSOLE_GITHUB_APP_ID=
|
||||
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
[submodule "app/console"]
|
||||
path = app/console
|
||||
url = https://github.com/appwrite/console
|
||||
branch = 2.3.4
|
||||
branch = disallow-personal-data
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
- Add error attribute to indexes and attributes [#4575](https://github.com/appwrite/appwrite/pull/4575)
|
||||
- Add new index validation rules [#5710](https://github.com/appwrite/appwrite/pull/5710)
|
||||
- Added support for disallowing passwords that contain personal data [#5371](https://github.com/appwrite/appwrite/pull/5371)
|
||||
|
||||
## Fixes
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ ENV VITE_APPWRITE_GROWTH_ENDPOINT=$VITE_APPWRITE_GROWTH_ENDPOINT
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
FROM appwrite/base:0.2.2 as final
|
||||
FROM appwrite/base:0.4.2 as final
|
||||
|
||||
LABEL maintainer="team@appwrite.io"
|
||||
|
||||
|
||||
@@ -175,6 +175,16 @@ return [
|
||||
'description' => 'Passwords do not match. Please check the password and confirm password.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_PASSWORD_RECENTLY_USED => [
|
||||
'name' => Exception::USER_PASSWORD_RECENTLY_USED,
|
||||
'description' => 'The password you are trying to use is similar to your previous password. Please choose a stronger password.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_PASSWORD_PERSONAL_DATA => [
|
||||
'name' => Exception::USER_PASSWORD_PERSONAL_DATA,
|
||||
'description' => 'The password you are trying to use contains references to your name, email, phone or userID. Please choose a different password and try again.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_SESSION_NOT_FOUND => [
|
||||
'name' => Exception::USER_SESSION_NOT_FOUND,
|
||||
'description' => 'The current user session could not be found.',
|
||||
@@ -210,6 +220,21 @@ return [
|
||||
'description' => 'Missing ID from OAuth2 provider.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_OAUTH2_BAD_REQUEST => [
|
||||
'name' => Exception::USER_OAUTH2_BAD_REQUEST,
|
||||
'description' => 'OAuth2 provider rejected the bad request.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_OAUTH2_UNAUTHORIZED => [
|
||||
'name' => Exception::USER_OAUTH2_UNAUTHORIZED,
|
||||
'description' => 'OAuth2 provider rejected the unauthorized request.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::USER_OAUTH2_PROVIDER_ERROR => [
|
||||
'name' => Exception::USER_OAUTH2_PROVIDER_ERROR,
|
||||
'description' => 'OAuth2 provider returned some error.',
|
||||
'code' => 424,
|
||||
],
|
||||
|
||||
/** Teams */
|
||||
Exception::TEAM_NOT_FOUND => [
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -31,6 +31,8 @@ return [
|
||||
'audio/ogg', // Ogg Vorbis RFC 5334
|
||||
'audio/vorbis', // Vorbis RFC 5215
|
||||
'audio/vnd.wav', // wav RFC 2361
|
||||
'audio/aac', //AAC audio
|
||||
'audio/x-hx-aac-adts', // AAC audio
|
||||
|
||||
// Microsoft Word
|
||||
'application/msword',
|
||||
|
||||
@@ -410,7 +410,7 @@ return [
|
||||
'variables' => [
|
||||
[
|
||||
'name' => '_APP_SMS_PROVIDER',
|
||||
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic, telesign, msg91, and vonage.",
|
||||
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, text-magic, telesign, msg91, and vonage.",
|
||||
'introduction' => '0.15.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
|
||||
+127
-44
@@ -2,6 +2,7 @@
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\OAuth2\Exception as OAuth2Exception;
|
||||
use Appwrite\Auth\Validator\Password;
|
||||
use Appwrite\Auth\Validator\Phone;
|
||||
use Appwrite\Detector\Detector;
|
||||
@@ -43,6 +44,7 @@ use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Appwrite\Auth\Validator\PasswordHistory;
|
||||
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||
use Appwrite\Auth\Validator\PersonalData;
|
||||
|
||||
$oauthDefaultSuccess = '/auth/oauth2/success';
|
||||
$oauthDefaultFailure = '/auth/oauth2/failure';
|
||||
@@ -202,6 +204,13 @@ App::post('/v1/account')
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
$personalDataValidator = new PersonalData($userId, $email, $name, null);
|
||||
if (!$personalDataValidator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_PERSONAL_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
try {
|
||||
@@ -440,11 +449,13 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
|
||||
->label('docs', false)
|
||||
->param('projectId', '', new Text(1024), 'Project ID.')
|
||||
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
|
||||
->param('code', '', new Text(2048), 'OAuth2 code.')
|
||||
->param('code', '', new Text(2048, 0), 'OAuth2 code.', true)
|
||||
->param('state', '', new Text(2048), 'Login state params.', true)
|
||||
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
|
||||
->param('error_description', '', new Text(2048, 0), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->action(function (string $projectId, string $provider, string $code, string $state, Request $request, Response $response) {
|
||||
->action(function (string $projectId, string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response) {
|
||||
|
||||
$domain = $request->getHostname();
|
||||
$protocol = $request->getProtocol();
|
||||
@@ -453,7 +464,13 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
|
||||
. \http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state]));
|
||||
. \http_build_query([
|
||||
'project' => $projectId,
|
||||
'code' => $code,
|
||||
'state' => $state,
|
||||
'error' => $error,
|
||||
'error_description' => $error_description
|
||||
]));
|
||||
});
|
||||
|
||||
App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
|
||||
@@ -465,11 +482,13 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
|
||||
->label('docs', false)
|
||||
->param('projectId', '', new Text(1024), 'Project ID.')
|
||||
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
|
||||
->param('code', '', new Text(2048), 'OAuth2 code.')
|
||||
->param('code', '', new Text(2048, 0), 'OAuth2 code.', true)
|
||||
->param('state', '', new Text(2048), 'Login state params.', true)
|
||||
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
|
||||
->param('error_description', '', new Text(2048, 0), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->action(function (string $projectId, string $provider, string $code, string $state, Request $request, Response $response) {
|
||||
->action(function (string $projectId, string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response) {
|
||||
|
||||
$domain = $request->getHostname();
|
||||
$protocol = $request->getProtocol();
|
||||
@@ -478,7 +497,13 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
|
||||
. \http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state]));
|
||||
. \http_build_query([
|
||||
'project' => $projectId,
|
||||
'code' => $code,
|
||||
'state' => $state,
|
||||
'error' => $error,
|
||||
'error_description' => $error_description
|
||||
]));
|
||||
});
|
||||
|
||||
App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
@@ -494,8 +519,10 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->label('abuse-key', 'ip:{ip}')
|
||||
->label('docs', false)
|
||||
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
|
||||
->param('code', '', new Text(2048), 'OAuth2 code.')
|
||||
->param('code', '', new Text(2048, 0), 'OAuth2 code.', true)
|
||||
->param('state', '', new Text(2048), 'OAuth2 state params.', true)
|
||||
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
|
||||
->param('error_description', '', new Text(2048, 0), 'Human-readable text providing additional information about the error returned from the OAuth2 provider.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
@@ -503,7 +530,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->inject('dbForProject')
|
||||
->inject('geodb')
|
||||
->inject('events')
|
||||
->action(function (string $provider, string $code, string $state, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $events) use ($oauthDefaultSuccess) {
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $events) use ($oauthDefaultSuccess) {
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
|
||||
@@ -513,27 +540,22 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
$appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
|
||||
$providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false;
|
||||
|
||||
if (!$providerEnabled) {
|
||||
throw new Exception(Exception::PROJECT_PROVIDER_DISABLED, 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.');
|
||||
}
|
||||
|
||||
if (!empty($appSecret) && isset($appSecret['version'])) {
|
||||
$key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']);
|
||||
$appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag']));
|
||||
}
|
||||
|
||||
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
|
||||
|
||||
if (!\class_exists($className)) {
|
||||
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
|
||||
}
|
||||
|
||||
$providers = Config::getParam('providers');
|
||||
$providerName = $providers[$provider]['name'] ?? '';
|
||||
|
||||
/** @var Appwrite\Auth\OAuth2 $oauth2 */
|
||||
$oauth2 = new $className($appId, $appSecret, $callback);
|
||||
|
||||
if (!empty($state)) {
|
||||
try {
|
||||
$state = \array_merge($defaultState, $oauth2->parseState($state));
|
||||
} catch (\Exception$exception) {
|
||||
} catch (\Exception $exception) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to parse login state params as passed from OAuth2 provider');
|
||||
}
|
||||
} else {
|
||||
@@ -547,27 +569,66 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
if (!empty($state['failure']) && !$validateURL->isValid($state['failure'])) {
|
||||
throw new Exception(Exception::PROJECT_INVALID_FAILURE_URL);
|
||||
}
|
||||
|
||||
$accessToken = $oauth2->getAccessToken($code);
|
||||
$refreshToken = $oauth2->getRefreshToken($code);
|
||||
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
|
||||
|
||||
if (empty($accessToken)) {
|
||||
if (!empty($state['failure'])) {
|
||||
$response->redirect($state['failure'], 301, 0);
|
||||
$failure = [];
|
||||
if (!empty($state['failure'])) {
|
||||
$failure = URLParser::parse($state['failure']);
|
||||
}
|
||||
$failureRedirect = (function (string $type, ?string $message = null, ?int $code = null) use ($failure, $response) {
|
||||
$exception = new Exception($type, $message, $code);
|
||||
if (!empty($failure)) {
|
||||
$query = URLParser::parseQuery($failure['query']);
|
||||
$query['error'] = json_encode([
|
||||
'message' => $exception->getMessage(),
|
||||
'type' => $exception->getType(),
|
||||
'code' => !\is_null($code) ? $code : $exception->getCode(),
|
||||
]);
|
||||
$failure['query'] = URLParser::unparseQuery($query);
|
||||
$response->redirect(URLParser::unparse($failure), 301);
|
||||
}
|
||||
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to obtain access token');
|
||||
throw $exception;
|
||||
});
|
||||
|
||||
if (!$providerEnabled) {
|
||||
$failureRedirect(Exception::PROJECT_PROVIDER_DISABLED, 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.');
|
||||
}
|
||||
|
||||
if (!empty($error)) {
|
||||
$message = 'The ' . $providerName . ' OAuth2 provider returned an error: ' . $error;
|
||||
if (!empty($error_description)) {
|
||||
$message .= ': ' . $error_description;
|
||||
}
|
||||
$failureRedirect(Exception::USER_OAUTH2_PROVIDER_ERROR, $message);
|
||||
}
|
||||
|
||||
if (empty($code)) {
|
||||
$failureRedirect(Exception::USER_OAUTH2_PROVIDER_ERROR, 'Missing OAuth2 code. Please contact the Appwrite team for additional support.');
|
||||
}
|
||||
|
||||
if (!empty($appSecret) && isset($appSecret['version'])) {
|
||||
$key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']);
|
||||
$appSecret = OpenSSL::decrypt($appSecret['data'], $appSecret['method'], $key, 0, \hex2bin($appSecret['iv']), \hex2bin($appSecret['tag']));
|
||||
}
|
||||
|
||||
$accessToken = '';
|
||||
$refreshToken = '';
|
||||
$accessTokenExpiry = 0;
|
||||
|
||||
try {
|
||||
$accessToken = $oauth2->getAccessToken($code);
|
||||
$refreshToken = $oauth2->getRefreshToken($code);
|
||||
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
|
||||
} catch (OAuth2Exception $ex) {
|
||||
$failureRedirect(
|
||||
$ex->getType(),
|
||||
'Failed to obtain access token. The ' . $providerName . ' OAuth2 provider returned an error: ' . $ex->getMessage(),
|
||||
$ex->getCode(),
|
||||
);
|
||||
}
|
||||
|
||||
$oauth2ID = $oauth2->getUserID($accessToken);
|
||||
|
||||
if (empty($oauth2ID)) {
|
||||
if (!empty($state['failure'])) {
|
||||
$response->redirect($state['failure'], 301, 0);
|
||||
}
|
||||
|
||||
throw new Exception(Exception::USER_MISSING_ID);
|
||||
$failureRedirect(Exception::USER_MISSING_ID);
|
||||
}
|
||||
|
||||
$name = $oauth2->getUserName($accessToken);
|
||||
@@ -644,7 +705,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
$total = $dbForProject->count('users', max: APP_LIMIT_USERS);
|
||||
|
||||
if ($total >= $limit) {
|
||||
throw new Exception(Exception::USER_COUNT_EXCEEDED);
|
||||
$failureRedirect(Exception::USER_COUNT_EXCEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,7 +744,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
]);
|
||||
Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
|
||||
} catch (Duplicate $th) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS);
|
||||
$failureRedirect(Exception::USER_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -692,7 +753,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
Authorization::setRole(Role::users()->toString());
|
||||
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
throw new Exception(Exception::USER_BLOCKED); // User is in status blocked
|
||||
$failureRedirect(Exception::USER_BLOCKED); // User is in status blocked
|
||||
}
|
||||
|
||||
$identity = $dbForProject->findOne('identities', [
|
||||
@@ -955,7 +1016,7 @@ App::post('/v1/account/sessions/magic-url')
|
||||
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$userId = $userId == 'unique()' ? ID::unique() : $userId;
|
||||
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||
|
||||
$user->setAttributes([
|
||||
'$id' => $userId,
|
||||
@@ -1208,7 +1269,7 @@ App::post('/v1/account/sessions/phone')
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_TOKEN)
|
||||
->label('abuse-limit', 10)
|
||||
->label('abuse-key', 'url:{url},email:{param-email}')
|
||||
->label('abuse-key', 'url:{url},email:{param-phone}')
|
||||
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
|
||||
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
|
||||
->inject('request')
|
||||
@@ -1873,16 +1934,22 @@ App::patch('/v1/account/password')
|
||||
|
||||
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$history = [];
|
||||
$history = $user->getAttribute('passwordHistory', []);
|
||||
if ($historyLimit > 0) {
|
||||
$history = $user->getAttribute('passwordHistory', []);
|
||||
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
||||
if (!$validator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED, 'The password was recently used', 409);
|
||||
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED);
|
||||
}
|
||||
|
||||
$history[] = $newPassword;
|
||||
array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||
$history = array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||
}
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
$personalDataValidator = new PersonalData($user->getId(), $user->getAttribute('email'), $user->getAttribute('name'), $user->getAttribute('phone'));
|
||||
if (!$personalDataValidator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_PERSONAL_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
$user
|
||||
@@ -2486,8 +2553,9 @@ App::put('/v1/account/recovery')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('events')
|
||||
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Document $user, Database $dbForProject, Event $events) {
|
||||
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Document $user, Database $dbForProject, Document $project, Event $events) {
|
||||
if ($password !== $passwordAgain) {
|
||||
throw new Exception(Exception::USER_PASSWORD_MISMATCH);
|
||||
}
|
||||
@@ -2507,8 +2575,23 @@ App::put('/v1/account/recovery')
|
||||
|
||||
Authorization::setRole(Role::user($profile->getId())->toString());
|
||||
|
||||
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
|
||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$history = $profile->getAttribute('passwordHistory', []);
|
||||
if ($historyLimit > 0) {
|
||||
$validator = new PasswordHistory($history, $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'));
|
||||
if (!$validator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED);
|
||||
}
|
||||
|
||||
$history[] = $newPassword;
|
||||
$history = array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||
}
|
||||
|
||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
|
||||
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
|
||||
->setAttribute('password', $newPassword)
|
||||
->setAttribute('passwordHistory', $history)
|
||||
->setAttribute('passwordUpdate', DateTime::now())
|
||||
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
||||
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
|
||||
|
||||
@@ -4,6 +4,7 @@ use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
App::init()
|
||||
->groups(['console'])
|
||||
@@ -38,3 +39,58 @@ App::get('/v1/console/variables')
|
||||
|
||||
$response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES);
|
||||
});
|
||||
|
||||
|
||||
App::post('/v1/console/assistant')
|
||||
->desc('Ask Query')
|
||||
->groups(['api', 'assistant'])
|
||||
->label('scope', 'public')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION])
|
||||
->label('sdk.namespace', 'assistant')
|
||||
->label('sdk.method', 'chat')
|
||||
->label('sdk.description', '/docs/references/assistant/chat.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_TEXT)
|
||||
->label('abuse-limit', 15)
|
||||
->label('abuse-key', 'userId:{userId}')
|
||||
->param('query', '', new Text(2000), 'Query')
|
||||
->inject('response')
|
||||
->action(function (string $query, Response $response) {
|
||||
$ch = curl_init('http://appwrite-assistant:3003/');
|
||||
$responseHeaders = [];
|
||||
$query = json_encode(['prompt' => $query]);
|
||||
$headers = ['accept: text/event-stream'];
|
||||
$handleEvent = function ($ch, $data) use ($response) {
|
||||
$response->chunk($data);
|
||||
|
||||
return \strlen($data);
|
||||
};
|
||||
|
||||
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $handleEvent);
|
||||
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 9000);
|
||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
|
||||
$len = strlen($header);
|
||||
$header = explode(':', $header, 2);
|
||||
|
||||
if (count($header) < 2) { // ignore invalid headers
|
||||
return $len;
|
||||
}
|
||||
|
||||
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
|
||||
|
||||
return $len;
|
||||
});
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
|
||||
|
||||
curl_exec($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
$response->chunk('', true);
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ App::post('/v1/projects')
|
||||
}
|
||||
|
||||
$auth = Config::getParam('auth', []);
|
||||
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordDictionary' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG];
|
||||
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordDictionary' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, 'personalDataCheck' => false];
|
||||
foreach ($auth as $index => $method) {
|
||||
$auths[$method['key'] ?? ''] = true;
|
||||
}
|
||||
@@ -615,7 +615,7 @@ App::patch('/v1/projects/:projectId/auth/password-history')
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/auth/password-dictionary')
|
||||
->desc('Update authentication password disctionary status. Use this endpoint to enable or disable the dicitonary check for user password')
|
||||
->desc('Update authentication password dictionary status. Use this endpoint to enable or disable the dicitonary check for user password')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
@@ -645,6 +645,37 @@ App::patch('/v1/projects/:projectId/auth/password-dictionary')
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/auth/personal-data')
|
||||
->desc('Enable or disable checking user passwords for similarity with their personal data.')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'updatePersonalDataCheck')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('enabled', false, new Boolean(false), 'Set whether or not to check a password for similarity with personal data. Default is false.')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['personalDataCheck'] = $enabled;
|
||||
|
||||
$dbForConsole->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('auths', $auths));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
App::patch('/v1/projects/:projectId/auth/max-sessions')
|
||||
->desc('Update Project user sessions limit')
|
||||
->groups(['api', 'projects'])
|
||||
|
||||
@@ -38,6 +38,7 @@ use MaxMind\Db\Reader;
|
||||
use Utopia\Validator\Integer;
|
||||
use Appwrite\Auth\Validator\PasswordHistory;
|
||||
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||
use Appwrite\Auth\Validator\PersonalData;
|
||||
|
||||
/** TODO: Remove function when we move to using utopia/platform */
|
||||
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $events): Document
|
||||
@@ -62,6 +63,13 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||
? ID::unique()
|
||||
: ID::custom($userId);
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
$personalDataValidator = new PersonalData($userId, $email, $name, $phone);
|
||||
if (!$personalDataValidator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_PERSONAL_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
|
||||
$user = $dbForProject->createDocument('users', new Document([
|
||||
'$id' => $userId,
|
||||
@@ -881,19 +889,25 @@ App::patch('/v1/users/:userId/password')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
|
||||
$personalDataValidator = new PersonalData($userId, $user->getAttribute('email'), $user->getAttribute('name'), $user->getAttribute('phone'));
|
||||
if (!$personalDataValidator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_PERSONAL_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
|
||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$history = [];
|
||||
$history = $user->getAttribute('passwordHistory', []);
|
||||
if ($historyLimit > 0) {
|
||||
$history = $user->getAttribute('passwordHistory', []);
|
||||
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
||||
if (!$validator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED, 'The password was recently used', 409);
|
||||
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED);
|
||||
}
|
||||
|
||||
$history[] = $newPassword;
|
||||
array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||
$history = array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||
}
|
||||
|
||||
$user
|
||||
|
||||
@@ -562,6 +562,7 @@ App::error()
|
||||
->setParam('projectName', $project->getAttribute('name'))
|
||||
->setParam('projectURL', $project->getAttribute('url'))
|
||||
->setParam('message', $error->getMessage())
|
||||
->setParam('type', $type)
|
||||
->setParam('code', $code)
|
||||
->setParam('trace', $trace)
|
||||
;
|
||||
|
||||
@@ -29,7 +29,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
|
||||
$parts = explode('.', $match);
|
||||
|
||||
if (count($parts) !== 2) {
|
||||
throw new Exception('Too less or too many parts', 400, Exception::GENERAL_ARGUMENT_INVALID);
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
|
||||
}
|
||||
|
||||
$namespace = $parts[0] ?? '';
|
||||
|
||||
@@ -36,7 +36,6 @@ use Utopia\App;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Database;
|
||||
@@ -817,7 +816,6 @@ foreach ($locales as $locale) {
|
||||
if (!\file_exists($path)) {
|
||||
$path = __DIR__ . '/config/locale/translations/' . \substr($code, 0, 2) . '.json'; // if `ar-ae` doesn't exist, look for `ar`
|
||||
if (!\file_exists($path)) {
|
||||
var_dump('Unable to find tralsnations for ' . $locale['code'] . ' so using en.json');
|
||||
$path = __DIR__ . '/config/locale/translations/en.json'; // if none translation exists, use default from `en.json`
|
||||
}
|
||||
}
|
||||
|
||||
+141
-84
File diff suppressed because one or more lines are too long
+3
-3
@@ -64,7 +64,7 @@
|
||||
"utopia-php/queue": "0.5.*",
|
||||
"utopia-php/registry": "0.5.*",
|
||||
"utopia-php/storage": "0.14.*",
|
||||
"utopia-php/swoole": "0.5.*",
|
||||
"utopia-php/swoole": "0.8.*",
|
||||
"utopia-php/websocket": "0.1.*",
|
||||
"resque/php-resque": "1.3.6",
|
||||
"matomo/device-detector": "6.1.*",
|
||||
@@ -86,8 +86,8 @@
|
||||
"appwrite/sdk-generator": "0.33.*",
|
||||
"ext-fileinfo": "*",
|
||||
"phpunit/phpunit": "9.5.20",
|
||||
"squizlabs/php_codesniffer": "^3.6",
|
||||
"swoole/ide-helper": "4.8.9",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"swoole/ide-helper": "5.0.2",
|
||||
"textalk/websocket": "1.5.7"
|
||||
},
|
||||
"provide": {
|
||||
|
||||
Generated
+16
-26
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f9e35e81b051baad38f0cb0e8fa611fe",
|
||||
"content-hash": "d8f45e92243913c5898f4c6b483ed770",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -2351,28 +2351,28 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/swoole",
|
||||
"version": "0.5.0",
|
||||
"version": "0.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/swoole.git",
|
||||
"reference": "c2a3a4f944a2f22945af3cbcb95b13f0769628b1"
|
||||
"reference": "5b60e7f730641cc182bc36b1f9939d4a76d3439b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/c2a3a4f944a2f22945af3cbcb95b13f0769628b1",
|
||||
"reference": "c2a3a4f944a2f22945af3cbcb95b13f0769628b1",
|
||||
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/5b60e7f730641cc182bc36b1f9939d4a76d3439b",
|
||||
"reference": "5b60e7f730641cc182bc36b1f9939d4a76d3439b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-swoole": "*",
|
||||
"php": ">=8.0",
|
||||
"utopia-php/framework": "0.*.*"
|
||||
"utopia-php/framework": "0.28.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"swoole/ide-helper": "4.8.3",
|
||||
"vimeo/psalm": "4.15.0"
|
||||
"swoole/ide-helper": "5.0.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -2396,9 +2396,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/swoole/issues",
|
||||
"source": "https://github.com/utopia-php/swoole/tree/0.5.0"
|
||||
"source": "https://github.com/utopia-php/swoole/tree/0.8.0"
|
||||
},
|
||||
"time": "2022-10-19T22:19:07+00:00"
|
||||
"time": "2023-07-25T10:29:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/system",
|
||||
@@ -4886,16 +4886,16 @@
|
||||
},
|
||||
{
|
||||
"name": "swoole/ide-helper",
|
||||
"version": "4.8.9",
|
||||
"version": "5.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/swoole/ide-helper.git",
|
||||
"reference": "8f82ba3b6af04a5bccb97c1654af992d1ee8b0fe"
|
||||
"reference": "16cfee44a6ec92254228c39bcab2fb8ae74cc2ea"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/swoole/ide-helper/zipball/8f82ba3b6af04a5bccb97c1654af992d1ee8b0fe",
|
||||
"reference": "8f82ba3b6af04a5bccb97c1654af992d1ee8b0fe",
|
||||
"url": "https://api.github.com/repos/swoole/ide-helper/zipball/16cfee44a6ec92254228c39bcab2fb8ae74cc2ea",
|
||||
"reference": "16cfee44a6ec92254228c39bcab2fb8ae74cc2ea",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "library",
|
||||
@@ -4912,19 +4912,9 @@
|
||||
"description": "IDE help files for Swoole.",
|
||||
"support": {
|
||||
"issues": "https://github.com/swoole/ide-helper/issues",
|
||||
"source": "https://github.com/swoole/ide-helper/tree/4.8.9"
|
||||
"source": "https://github.com/swoole/ide-helper/tree/5.0.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://gitee.com/swoole/swoole?donate=true",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/swoole",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2022-04-18T20:38:04+00:00"
|
||||
"time": "2023-03-20T06:05:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
|
||||
@@ -690,6 +690,14 @@ services:
|
||||
- _APP_CONNECTIONS_QUEUE
|
||||
- _APP_REGION
|
||||
|
||||
appwrite-assistant:
|
||||
container_name: appwrite-assistant
|
||||
image: appwrite/assistant:0.1.0
|
||||
networks:
|
||||
- appwrite
|
||||
environment:
|
||||
- OPENAI_API_KEY
|
||||
|
||||
openruntimes-executor:
|
||||
container_name: openruntimes-executor
|
||||
hostname: exc1
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Appwrite\Auth;
|
||||
|
||||
use Appwrite\Auth\OAuth2\Exception;
|
||||
|
||||
abstract class OAuth2
|
||||
{
|
||||
/**
|
||||
@@ -73,6 +75,13 @@ abstract class OAuth2
|
||||
*/
|
||||
abstract public function refreshTokens(string $refreshToken): array;
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getUserID(string $accessToken): string;
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
*
|
||||
@@ -148,11 +157,11 @@ abstract class OAuth2
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAccessTokenExpiry(string $code): string
|
||||
public function getAccessTokenExpiry(string $code): int
|
||||
{
|
||||
$tokens = $this->getTokens($code);
|
||||
|
||||
return $tokens['expires_in'] ?? '';
|
||||
return $tokens['expires_in'] ?? 0;
|
||||
}
|
||||
|
||||
// The parseState function was designed specifically for Amazon OAuth2 Adapter to override.
|
||||
@@ -195,8 +204,14 @@ abstract class OAuth2
|
||||
// Send the request & save response to $response
|
||||
$response = \curl_exec($ch);
|
||||
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
\curl_close($ch);
|
||||
|
||||
if ($code != 200) {
|
||||
throw new Exception($response, $code);
|
||||
}
|
||||
|
||||
return (string)$response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\OAuth2;
|
||||
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
|
||||
class Exception extends AppwriteException
|
||||
{
|
||||
protected string $response = '';
|
||||
protected string $error = '';
|
||||
protected string $errorDescription = '';
|
||||
|
||||
public function __construct(string $response = '', int $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
$this->response = $response;
|
||||
$this->message = $response;
|
||||
$decoded = json_decode($response, true);
|
||||
if (\is_array($decoded)) {
|
||||
$this->error = $decoded['error'];
|
||||
$this->errorDescription = $decoded['error_description'];
|
||||
$this->message = $this->error . ': ' . $this->errorDescription;
|
||||
}
|
||||
$type = match ($code) {
|
||||
400 => AppwriteException::USER_OAUTH2_BAD_REQUEST,
|
||||
401 => AppwriteException::USER_OAUTH2_UNAUTHORIZED,
|
||||
default => AppwriteException::USER_OAUTH2_PROVIDER_ERROR
|
||||
};
|
||||
|
||||
parent::__construct($type, $this->message, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error parameter from the response.
|
||||
*
|
||||
* See https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 for more information.
|
||||
*/
|
||||
public function getError(): string
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error_description parameter from the response.
|
||||
*
|
||||
* See https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 for more information.
|
||||
*/
|
||||
public function getErrorDescription(): string
|
||||
{
|
||||
return $this->errorDescription;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Validator;
|
||||
|
||||
/**
|
||||
* Validates user password string against their personal data
|
||||
*/
|
||||
class PersonalData extends Password
|
||||
{
|
||||
public function __construct(
|
||||
protected ?string $userId = null,
|
||||
protected ?string $email = null,
|
||||
protected ?string $name = null,
|
||||
protected ?string $phone = null,
|
||||
protected bool $strict = false
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description.
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Password must not include any personal data like your name, email, phone number, etc.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($password): bool
|
||||
{
|
||||
if (!parent::isValid($password)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->strict) {
|
||||
$password = strtolower($password);
|
||||
$this->userId = strtolower($this->userId);
|
||||
$this->email = strtolower($this->email);
|
||||
$this->name = strtolower($this->name);
|
||||
$this->phone = strtolower($this->phone);
|
||||
}
|
||||
|
||||
if ($this->userId && strpos($password, $this->userId) !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->email && strpos($password, $this->email) !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->email && strpos($password, explode('@', $this->email)[0] ?? '') !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->name && strpos($password, $this->name) !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->phone && strpos($password, str_replace('+', '', $this->phone)) !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->phone && strpos($password, $this->phone) !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is array
|
||||
*
|
||||
* Function will return true if object is array.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* Returns validator type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_STRING;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ class Exception extends \Exception
|
||||
public const USER_SESSION_ALREADY_EXISTS = 'user_session_already_exists';
|
||||
public const USER_NOT_FOUND = 'user_not_found';
|
||||
public const USER_PASSWORD_RECENTLY_USED = 'password_recently_used';
|
||||
public const USER_PASSWORD_PERSONAL_DATA = 'password_personal_data';
|
||||
public const USER_EMAIL_ALREADY_EXISTS = 'user_email_already_exists';
|
||||
public const USER_PASSWORD_MISMATCH = 'user_password_mismatch';
|
||||
public const USER_SESSION_NOT_FOUND = 'user_session_not_found';
|
||||
@@ -78,6 +79,9 @@ class Exception extends \Exception
|
||||
public const USER_PHONE_ALREADY_EXISTS = 'user_phone_already_exists';
|
||||
public const USER_PHONE_NOT_FOUND = 'user_phone_not_found';
|
||||
public const USER_MISSING_ID = 'user_missing_id';
|
||||
public const USER_OAUTH2_BAD_REQUEST = 'user_oauth2_bad_request';
|
||||
public const USER_OAUTH2_UNAUTHORIZED = 'user_oauth2_unauthorized';
|
||||
public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error';
|
||||
|
||||
/** Teams */
|
||||
public const TEAM_NOT_FOUND = 'team_not_found';
|
||||
|
||||
@@ -132,6 +132,12 @@ class Project extends Model
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('authPersonalDataCheck', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not to check the user password for similarity with their personal data.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('providers', [
|
||||
'type' => Response::MODEL_PROVIDER,
|
||||
'description' => 'List of Providers.',
|
||||
@@ -307,6 +313,7 @@ class Project extends Model
|
||||
$document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT);
|
||||
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
|
||||
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
|
||||
$document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false);
|
||||
|
||||
foreach ($auth as $index => $method) {
|
||||
$key = $method['key'];
|
||||
|
||||
@@ -1336,7 +1336,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$this->assertEquals(409, $response['headers']['status-code']);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$headers = array_merge($this->getHeaders(), [
|
||||
'x-appwrite-mode' => 'admin',
|
||||
@@ -1348,7 +1348,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$this->assertEquals(409, $response['headers']['status-code']);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
|
||||
/**
|
||||
@@ -1497,6 +1497,126 @@ class ProjectsConsoleClientTest extends Scope
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateProject
|
||||
*/
|
||||
public function testUpdateDisallowPersonalData($data): void
|
||||
{
|
||||
$id = $data['projectId'] ?? '';
|
||||
|
||||
/**
|
||||
* Enable Disallowing of Personal Data
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/personal-data', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(true, $response['body']['authPersonalDataCheck']);
|
||||
|
||||
/**
|
||||
* Test for failure
|
||||
*/
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
$password = 'password';
|
||||
$name = 'username';
|
||||
$userId = ID::unique();
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $email,
|
||||
'name' => $name,
|
||||
'userId' => $userId
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
$this->assertEquals(400, $response['body']['code']);
|
||||
$this->assertEquals(Exception::USER_PASSWORD_PERSONAL_DATA, $response['body']['type']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $name,
|
||||
'name' => $name,
|
||||
'userId' => $userId
|
||||
]);
|
||||
|
||||
$phone = '+123456789';
|
||||
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge($this->getHeaders(), [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin',
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $phone,
|
||||
'name' => $name,
|
||||
'userId' => $userId,
|
||||
'phone' => $phone
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
$this->assertEquals(400, $response['body']['code']);
|
||||
$this->assertEquals(Exception::USER_PASSWORD_PERSONAL_DATA, $response['body']['type']);
|
||||
|
||||
/** Test for success */
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
$password = 'password';
|
||||
$name = 'username';
|
||||
$userId = ID::unique();
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'name' => $name,
|
||||
'userId' => $userId
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
$userId = ID::unique();
|
||||
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge($this->getHeaders(), [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
'x-appwrite-mode' => 'admin',
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'name' => $name,
|
||||
'userId' => $userId,
|
||||
'phone' => $phone
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
|
||||
/**
|
||||
* Reset
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/personal-data', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(false, $response['body']['authPersonalDataCheck']);
|
||||
}
|
||||
|
||||
|
||||
public function testUpdateProjectServiceStatusAdmin(): array
|
||||
{
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Auth\Validator;
|
||||
|
||||
use Appwrite\Auth\Validator\PersonalData;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PersonalDataTest extends TestCase
|
||||
{
|
||||
protected ?PersonalData $object = null;
|
||||
|
||||
public function testStrict(): void
|
||||
{
|
||||
$this->object = new PersonalData('userId', 'email@example.com', 'name', '+129492323', true);
|
||||
|
||||
$this->assertEquals($this->object->isValid('userId'), false);
|
||||
$this->assertEquals($this->object->isValid('something.userId'), false);
|
||||
$this->assertEquals($this->object->isValid('userId.something'), false);
|
||||
$this->assertEquals($this->object->isValid('something.userId.something'), false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('email@example.com'), false);
|
||||
$this->assertEquals($this->object->isValid('something.email@example.com'), false);
|
||||
$this->assertEquals($this->object->isValid('email@example.com.something'), false);
|
||||
$this->assertEquals($this->object->isValid('something.email@example.com.something'), false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('name'), false);
|
||||
$this->assertEquals($this->object->isValid('something.name'), false);
|
||||
$this->assertEquals($this->object->isValid('name.something'), false);
|
||||
$this->assertEquals($this->object->isValid('something.name.something'), false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('+129492323'), false);
|
||||
$this->assertEquals($this->object->isValid('something.+129492323'), false);
|
||||
$this->assertEquals($this->object->isValid('+129492323.something'), false);
|
||||
$this->assertEquals($this->object->isValid('something.+129492323.something'), false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('129492323'), false);
|
||||
$this->assertEquals($this->object->isValid('something.129492323'), false);
|
||||
$this->assertEquals($this->object->isValid('129492323.something'), false);
|
||||
$this->assertEquals($this->object->isValid('something.129492323.something'), false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('email'), false);
|
||||
$this->assertEquals($this->object->isValid('something.email'), false);
|
||||
$this->assertEquals($this->object->isValid('email.something'), false);
|
||||
$this->assertEquals($this->object->isValid('something.email.something'), false);
|
||||
|
||||
/** Test for success */
|
||||
$this->assertEquals($this->object->isValid('893pu5egerfsv3rgersvd'), true);
|
||||
}
|
||||
|
||||
public function testNotStrict(): void
|
||||
{
|
||||
$this->object = new PersonalData('userId', 'email@example.com', 'name', '+129492323', false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('userId'), false);
|
||||
$this->assertEquals($this->object->isValid('USERID'), false);
|
||||
$this->assertEquals($this->object->isValid('something.USERID'), false);
|
||||
$this->assertEquals($this->object->isValid('USERID.something'), false);
|
||||
$this->assertEquals($this->object->isValid('something.USERID.something'), false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('email@example.com'), false);
|
||||
$this->assertEquals($this->object->isValid('EMAIL@EXAMPLE.COM'), false);
|
||||
$this->assertEquals($this->object->isValid('something.EMAIL@EXAMPLE.COM'), false);
|
||||
$this->assertEquals($this->object->isValid('EMAIL@EXAMPLE.COM.something'), false);
|
||||
$this->assertEquals($this->object->isValid('something.EMAIL@EXAMPLE.COM.something'), false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('name'), false);
|
||||
$this->assertEquals($this->object->isValid('NAME'), false);
|
||||
$this->assertEquals($this->object->isValid('something.NAME'), false);
|
||||
$this->assertEquals($this->object->isValid('NAME.something'), false);
|
||||
$this->assertEquals($this->object->isValid('something.NAME.something'), false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('+129492323'), false);
|
||||
$this->assertEquals($this->object->isValid('129492323'), false);
|
||||
|
||||
$this->assertEquals($this->object->isValid('email'), false);
|
||||
$this->assertEquals($this->object->isValid('EMAIL'), false);
|
||||
$this->assertEquals($this->object->isValid('something.EMAIL'), false);
|
||||
$this->assertEquals($this->object->isValid('EMAIL.something'), false);
|
||||
$this->assertEquals($this->object->isValid('something.EMAIL.something'), false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user