Merge pull request #5371 from appwrite/disallow-personal-data
Disallow personal data
This commit is contained in:
commit
c1167f7dc7
25 changed files with 426 additions and 25 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
@ -1,4 +1,4 @@
|
||||||
[submodule "app/console"]
|
[submodule "app/console"]
|
||||||
path = app/console
|
path = app/console
|
||||||
url = https://github.com/appwrite/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 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)
|
- 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
|
## Fixes
|
||||||
|
|
||||||
|
|
|
@ -175,6 +175,16 @@ return [
|
||||||
'description' => 'Passwords do not match. Please check the password and confirm password.',
|
'description' => 'Passwords do not match. Please check the password and confirm password.',
|
||||||
'code' => 400,
|
'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 => [
|
Exception::USER_SESSION_NOT_FOUND => [
|
||||||
'name' => Exception::USER_SESSION_NOT_FOUND,
|
'name' => Exception::USER_SESSION_NOT_FOUND,
|
||||||
'description' => 'The current user session could not be found.',
|
'description' => 'The current user session could not be found.',
|
||||||
|
|
1
app/config/specs/open-api3-1.4.x-client.json
Normal file
1
app/config/specs/open-api3-1.4.x-client.json
Normal file
File diff suppressed because one or more lines are too long
1
app/config/specs/open-api3-1.4.x-console.json
Normal file
1
app/config/specs/open-api3-1.4.x-console.json
Normal file
File diff suppressed because one or more lines are too long
1
app/config/specs/open-api3-1.4.x-server.json
Normal file
1
app/config/specs/open-api3-1.4.x-server.json
Normal file
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
1
app/config/specs/swagger2-1.4.x-client.json
Normal file
1
app/config/specs/swagger2-1.4.x-client.json
Normal file
File diff suppressed because one or more lines are too long
1
app/config/specs/swagger2-1.4.x-console.json
Normal file
1
app/config/specs/swagger2-1.4.x-console.json
Normal file
File diff suppressed because one or more lines are too long
1
app/config/specs/swagger2-1.4.x-server.json
Normal file
1
app/config/specs/swagger2-1.4.x-server.json
Normal file
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
|
@ -1 +1 @@
|
||||||
Subproject commit 9174d8f8cb584744dd7a53f69d324f490ee82ee3
|
Subproject commit 61ed63c5094bb675c1b2715cb2fa7e17389e1152
|
|
@ -42,6 +42,7 @@ use Utopia\Validator\Text;
|
||||||
use Utopia\Validator\WhiteList;
|
use Utopia\Validator\WhiteList;
|
||||||
use Appwrite\Auth\Validator\PasswordHistory;
|
use Appwrite\Auth\Validator\PasswordHistory;
|
||||||
use Appwrite\Auth\Validator\PasswordDictionary;
|
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||||
|
use Appwrite\Auth\Validator\PersonalData;
|
||||||
|
|
||||||
$oauthDefaultSuccess = '/auth/oauth2/success';
|
$oauthDefaultSuccess = '/auth/oauth2/success';
|
||||||
$oauthDefaultFailure = '/auth/oauth2/failure';
|
$oauthDefaultFailure = '/auth/oauth2/failure';
|
||||||
|
@ -193,6 +194,13 @@ App::post('/v1/account')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||||
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||||
try {
|
try {
|
||||||
|
@ -785,7 +793,7 @@ App::post('/v1/account/sessions/magic-url')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$userId = $userId == 'unique()' ? ID::unique() : $userId;
|
$userId = $userId === 'unique()' ? ID::unique() : $userId;
|
||||||
|
|
||||||
$user->setAttributes([
|
$user->setAttributes([
|
||||||
'$id' => $userId,
|
'$id' => $userId,
|
||||||
|
@ -1703,16 +1711,22 @@ App::patch('/v1/account/password')
|
||||||
|
|
||||||
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||||
$history = [];
|
|
||||||
if ($historyLimit > 0) {
|
|
||||||
$history = $user->getAttribute('passwordHistory', []);
|
$history = $user->getAttribute('passwordHistory', []);
|
||||||
|
if ($historyLimit > 0) {
|
||||||
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
||||||
if (!$validator->isValid($password)) {
|
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;
|
$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
|
$user
|
||||||
|
@ -2307,8 +2321,9 @@ App::put('/v1/account/recovery')
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('user')
|
->inject('user')
|
||||||
->inject('dbForProject')
|
->inject('dbForProject')
|
||||||
|
->inject('project')
|
||||||
->inject('events')
|
->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) {
|
if ($password !== $passwordAgain) {
|
||||||
throw new Exception(Exception::USER_PASSWORD_MISMATCH);
|
throw new Exception(Exception::USER_PASSWORD_MISMATCH);
|
||||||
}
|
}
|
||||||
|
@ -2328,8 +2343,23 @@ App::put('/v1/account/recovery')
|
||||||
|
|
||||||
Authorization::setRole(Role::user($profile->getId())->toString());
|
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
|
$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('passwordUpdate', DateTime::now())
|
||||||
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
||||||
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
|
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
|
||||||
|
|
|
@ -88,7 +88,7 @@ App::post('/v1/projects')
|
||||||
}
|
}
|
||||||
|
|
||||||
$auth = Config::getParam('auth', []);
|
$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) {
|
foreach ($auth as $index => $method) {
|
||||||
$auths[$method['key'] ?? ''] = true;
|
$auths[$method['key'] ?? ''] = true;
|
||||||
}
|
}
|
||||||
|
@ -615,7 +615,7 @@ App::patch('/v1/projects/:projectId/auth/password-history')
|
||||||
});
|
});
|
||||||
|
|
||||||
App::patch('/v1/projects/:projectId/auth/password-dictionary')
|
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'])
|
->groups(['api', 'projects'])
|
||||||
->label('scope', 'projects.write')
|
->label('scope', 'projects.write')
|
||||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
->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);
|
$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')
|
App::patch('/v1/projects/:projectId/auth/max-sessions')
|
||||||
->desc('Update Project user sessions limit')
|
->desc('Update Project user sessions limit')
|
||||||
->groups(['api', 'projects'])
|
->groups(['api', 'projects'])
|
||||||
|
|
|
@ -37,6 +37,7 @@ use MaxMind\Db\Reader;
|
||||||
use Utopia\Validator\Integer;
|
use Utopia\Validator\Integer;
|
||||||
use Appwrite\Auth\Validator\PasswordHistory;
|
use Appwrite\Auth\Validator\PasswordHistory;
|
||||||
use Appwrite\Auth\Validator\PasswordDictionary;
|
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||||
|
use Appwrite\Auth\Validator\PersonalData;
|
||||||
|
|
||||||
/** TODO: Remove function when we move to using utopia/platform */
|
/** 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
|
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $events): Document
|
||||||
|
@ -53,6 +54,13 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||||
? ID::unique()
|
? ID::unique()
|
||||||
: ID::custom($userId);
|
: 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;
|
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
|
||||||
$user = $dbForProject->createDocument('users', new Document([
|
$user = $dbForProject->createDocument('users', new Document([
|
||||||
'$id' => $userId,
|
'$id' => $userId,
|
||||||
|
@ -825,19 +833,25 @@ App::patch('/v1/users/:userId/password')
|
||||||
throw new Exception(Exception::USER_NOT_FOUND);
|
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);
|
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||||
|
|
||||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||||
$history = [];
|
|
||||||
if ($historyLimit > 0) {
|
|
||||||
$history = $user->getAttribute('passwordHistory', []);
|
$history = $user->getAttribute('passwordHistory', []);
|
||||||
|
if ($historyLimit > 0) {
|
||||||
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
||||||
if (!$validator->isValid($password)) {
|
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;
|
$history[] = $newPassword;
|
||||||
array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
$history = array_slice($history, (count($history) - $historyLimit), $historyLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
$user
|
$user
|
||||||
|
|
|
@ -36,7 +36,6 @@ use Utopia\App;
|
||||||
use Utopia\Logger\Logger;
|
use Utopia\Logger\Logger;
|
||||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||||
use Utopia\Cache\Cache;
|
use Utopia\Cache\Cache;
|
||||||
use Utopia\CLI\Console;
|
|
||||||
use Utopia\Config\Config;
|
use Utopia\Config\Config;
|
||||||
use Utopia\Database\Helpers\ID;
|
use Utopia\Database\Helpers\ID;
|
||||||
use Utopia\Database\Database;
|
use Utopia\Database\Database;
|
||||||
|
@ -817,7 +816,6 @@ foreach ($locales as $locale) {
|
||||||
if (!\file_exists($path)) {
|
if (!\file_exists($path)) {
|
||||||
$path = __DIR__ . '/config/locale/translations/' . \substr($code, 0, 2) . '.json'; // if `ar-ae` doesn't exist, look for `ar`
|
$path = __DIR__ . '/config/locale/translations/' . \substr($code, 0, 2) . '.json'; // if `ar-ae` doesn't exist, look for `ar`
|
||||||
if (!\file_exists($path)) {
|
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`
|
$path = __DIR__ . '/config/locale/translations/en.json'; // if none translation exists, use default from `en.json`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
102
src/Appwrite/Auth/Validator/PersonalData.php
Normal file
102
src/Appwrite/Auth/Validator/PersonalData.php
Normal file
|
@ -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_SESSION_ALREADY_EXISTS = 'user_session_already_exists';
|
||||||
public const USER_NOT_FOUND = 'user_not_found';
|
public const USER_NOT_FOUND = 'user_not_found';
|
||||||
public const USER_PASSWORD_RECENTLY_USED = 'password_recently_used';
|
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_EMAIL_ALREADY_EXISTS = 'user_email_already_exists';
|
||||||
public const USER_PASSWORD_MISMATCH = 'user_password_mismatch';
|
public const USER_PASSWORD_MISMATCH = 'user_password_mismatch';
|
||||||
public const USER_SESSION_NOT_FOUND = 'user_session_not_found';
|
public const USER_SESSION_NOT_FOUND = 'user_session_not_found';
|
||||||
|
|
|
@ -132,6 +132,12 @@ class Project extends Model
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'example' => true,
|
'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', [
|
->addRule('providers', [
|
||||||
'type' => Response::MODEL_PROVIDER,
|
'type' => Response::MODEL_PROVIDER,
|
||||||
'description' => 'List of Providers.',
|
'description' => 'List of Providers.',
|
||||||
|
@ -307,6 +313,7 @@ class Project extends Model
|
||||||
$document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT);
|
$document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT);
|
||||||
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
|
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
|
||||||
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
|
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
|
||||||
|
$document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false);
|
||||||
|
|
||||||
foreach ($auth as $index => $method) {
|
foreach ($auth as $index => $method) {
|
||||||
$key = $method['key'];
|
$key = $method['key'];
|
||||||
|
|
|
@ -1336,7 +1336,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
'password' => $password,
|
'password' => $password,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals(409, $response['headers']['status-code']);
|
$this->assertEquals(400, $response['headers']['status-code']);
|
||||||
|
|
||||||
$headers = array_merge($this->getHeaders(), [
|
$headers = array_merge($this->getHeaders(), [
|
||||||
'x-appwrite-mode' => 'admin',
|
'x-appwrite-mode' => 'admin',
|
||||||
|
@ -1348,7 +1348,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
'password' => $password,
|
'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;
|
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
|
public function testUpdateProjectServiceStatusAdmin(): array
|
||||||
{
|
{
|
||||||
|
|
81
tests/unit/Auth/Validator/PersonalDataTest.php
Normal file
81
tests/unit/Auth/Validator/PersonalDataTest.php
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue