Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block install process if and administrator user is available in the database along with his preset + Make password GDPR compliant #223

Merged
merged 6 commits into from
Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions hivelvet-backend/app/config/access-install.ini
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ allow @set_locale = *
; logs routes
allow POST @logs_collect = *

; users routes
allow POST @admin_exists = *

; settings routes
allow GET @presets_collect = *
allow GET @settings_collect = *
Expand Down
3 changes: 3 additions & 0 deletions hivelvet-backend/app/config/routes-install.ini
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ PUT @set_locale : /set-locale/@locale [ajax] = Actions\Account\SetLocale-
; logs routes
POST @logs_collect : /logs = Actions\Logs\Collect->execute

; users routes
POST @admin_exists : /users/collect-admin = Actions\Users\Collect->execute

; settings routes
GET @presets_collect : /collect-presets = Actions\Presets\Collect->execute
GET @settings_collect : /collect-settings = Actions\Settings\Collect->execute
Expand Down
517 changes: 517 additions & 0 deletions hivelvet-backend/app/security/dictionary/en-US.json

Large diffs are not rendered by default.

73 changes: 57 additions & 16 deletions hivelvet-backend/app/src/Actions/Account/ChangePassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@
use Actions\Base as BaseAction;
use Enum\ResetTokenStatus;
use Enum\ResponseCode;
use Enum\UserStatus;
use Models\ResetPasswordToken;
use Models\User;
use Respect\Validation\Validator;
use Utils\SecurityUtils;
use Validation\DataChecker;

/**
* Class ChangePassword.
Expand All @@ -40,28 +44,65 @@ public function execute($f3): void
$password = $form['password'];
$resetToken = new ResetPasswordToken();

$dataChecker = new DataChecker();
$dataChecker->verify($password, Validator::length(8)->setName('password'));

/** @todo : move to locales */
$error_message = 'Password could not be changed';
$response_code = ResponseCode::HTTP_BAD_REQUEST;
if ($resetToken->getByToken($form['token'])) {
if (!$resetToken->dry()) {
$user = new User();
$user = $user->getById($resetToken->user_id);
$user->password = $password;
$resetToken->status = ResetTokenStatus::CONSUMED;

try {
$resetToken->save();
$user->save();
} catch (\Exception $e) {
$message = 'password could not be changed';
$this->logger->error('reset password error : password could not be changed', ['error' => $e->getMessage()]);
$this->renderJson(['message' => $message], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);
if ($dataChecker->allValid()) {
$user = new User();
$user = $user->getById($resetToken->user_id);
$resetToken->status = ResetTokenStatus::CONSUMED;

return;
if (SecurityUtils::isGdprCompliant($password)) {
$this->logger->error($error_message, ['error' => 'Only use letters, numbers, and common punctuation characters']);
$this->renderJson(['message' => 'Only use letters, numbers, and common punctuation characters'], $response_code);
} else {
$this->changePassword($user, $password, $resetToken, $error_message, $response_code);
}
} else {
$this->logger->error($error_message, ['errors' => $dataChecker->getErrors()]);
$this->renderJson(['errors' => $dataChecker->getErrors()], ResponseCode::HTTP_UNPROCESSABLE_ENTITY);
}
} else {
$this->logger->error($error_message);
}
}
}

/**
* @param $user
* @param $password
* @param $resetToken
* @param $error_message
* @param $response_code
*
* @throws \JsonException
*/
private function changePassword($user, $password, $resetToken, $error_message, $response_code): void
{
$next = SecurityUtils::credentialsAreCommon($user->username, $user->email, $password, $error_message, $response_code);
if ($user->verifyPassword($password) && $next) {
$this->logger->error($error_message, ['error' => 'New password cannot be the same as your old password']);
$this->renderJson(['message' => 'New password cannot be the same as your old password']);
} elseif ($next) {
try {
$user->password = $password;
$user->status = UserStatus::ACTIVE;
$resetToken->save();
$user->save();
} catch (\Exception $e) {
$message = 'Password could not be changed';
$this->logger->error($error_message, ['error' => $e->getMessage()]);
$this->renderJson(['message' => $message], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);

$this->renderJson(['result' => 'success']);
return;
}
} else {
$this->logger->error('reset password error : password could not be changed');

$this->renderJson(['result' => 'success']);
}
}
}
84 changes: 56 additions & 28 deletions hivelvet-backend/app/src/Actions/Account/Login.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,41 +41,69 @@ public function authorise($f3): void
{
$form = $this->getDecodedBody();

$dataChecker = new DataChecker();
$dataChecker = new DataChecker();
$dataChecker->verify($email = $form['email'], Validator::email()->setName('email'));
$dataChecker->verify($form['password'], Validator::length(4)->setName('password'));
$dataChecker->verify($form['password'], Validator::length(8)->setName('password'));

$userInfos = [];
$userInfos = [];
$error_message = 'Could not authenticate user with email';
if ($dataChecker->allValid()) {
$user = new User();
$user = $user->getByEmail($email);
$this->logger->info('Login attempt using email', ['email' => $email]);
// Check if the user exists
if ($user->valid() && UserStatus::ACTIVE === $user->status && $user->verifyPassword($form['password'])) {
// @todo: test UserRole::API !== $user->role->name
// valid credentials
$this->session->authorizeUser($user);
$this->login($form, $email, $userInfos, $dataChecker, $error_message);
} else {
$this->logger->error($error_message, ['errors' => $dataChecker->getErrors()]);
$this->renderJson(['errors' => $dataChecker->getErrors()], ResponseCode::HTTP_UNPROCESSABLE_ENTITY);
}
}

$user->last_login = Time::db();
$user->save();
private function login($form, $email, $userInfos, $dataChecker, $error_message): void
{
$user = new User();
$user = $user->getByEmail($email);
$this->logger->info('Login attempt using email', ['email' => $email]);
// Check if the user exists
if ($user->valid() && UserStatus::ACTIVE === $user->status && $user->verifyPassword($form['password'])) {
// @todo: test UserRole::API !== $user->role->name
// valid credentials
$this->session->authorizeUser($user);

// @todo: store role in redis cache to allow routes
$this->f3->set('role', $user->role->name);
$user->last_login = Time::db();
$user->password_attempts = 3;
$user->save();

// @todo: store locale in user prefs table
// $this->session->set('locale', $user->locale);
$userInfos = [
'username' => $user->username,
'email' => $user->email,
'role' => $user->role->name,
];
$this->logger->info('User successfully logged in', ['email' => $email]);
$this->renderJson($userInfos);
}
}
// @todo: store role in redis cache to allow routes
$this->f3->set('role', $user->role->name);

if (empty($userInfos) || \count($dataChecker->getErrors()) > 0) {
$this->logger->error('Could not authenticate user with email', ['email' => $email]);
// @todo: store locale in user prefs table
// $this->session->set('locale', $user->locale);
$userInfos = [
'username' => $user->username,
'email' => $user->email,
'role' => $user->role->name,
];
$this->logger->info('User successfully logged in', ['email' => $email]);
$this->renderJson($userInfos);
} elseif ($user->valid() && UserStatus::ACTIVE === $user->status && !$user->verifyPassword($form['password']) && $user->password_attempts > 1) {
--$user->password_attempts;
$user->save();
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => "Wrong password. Attempts left : {$user->password_attempts}"], ResponseCode::HTTP_BAD_REQUEST);
} elseif ($user->valid() && 0 === $user->password_attempts || 1 === $user->password_attempts) {
$user->password_attempts = 0;
$user->status = UserStatus::INACTIVE;
$user->save();
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => 'Your account has been locked because you have reached the maximum number of invalid sign-in attempts. You can contact the administrator or click here to receive an email containing instructions on how to unlock your account'], ResponseCode::HTTP_BAD_REQUEST);
} elseif ($user->valid() && UserStatus::PENDING === $user->status) {
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => 'Your account is not active. Please contact your administrator'], ResponseCode::HTTP_BAD_REQUEST);
} elseif ($user->valid() && UserStatus::DELETED === $user->status) {
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => 'Your account has been disabled for violating our terms'], ResponseCode::HTTP_BAD_REQUEST);
} elseif (!$user->valid()) {
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => 'User does not exist with this email'], ResponseCode::HTTP_BAD_REQUEST);
} elseif (empty($userInfos) || \count($dataChecker->getErrors()) > 0) {
$this->logger->error($error_message, ['email' => $email]);
$this->renderJson(['message' => 'Invalid Authentication data'], ResponseCode::HTTP_BAD_REQUEST);
}
}
Expand Down
54 changes: 32 additions & 22 deletions hivelvet-backend/app/src/Actions/Account/Register.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Enum\UserStatus;
use Models\User;
use Respect\Validation\Validator;
use Utils\SecurityUtils;
use Validation\DataChecker;

/**
Expand All @@ -41,39 +42,48 @@ public function signup($f3): void

$dataChecker->verify($form['username'], Validator::length(4)->setName('username'));
$dataChecker->verify($form['email'], Validator::email()->setName('email'));
$dataChecker->verify($form['password'], Validator::length(4)->setName('password'));
$dataChecker->verify($form['confirmPassword'], Validator::length(4)->equals($form['password'])->setName('confirmPassword'));
$dataChecker->verify($form['password'], Validator::length(8)->setName('password'));
$dataChecker->verify($form['confirmPassword'], Validator::length(8)->equals($form['password'])->setName('confirmPassword'));
// @fixme: the agreement must be accepted only if there are terms for the website
// otherwise in the login it should look for available terms of they were not previously available and ask to accept them
$dataChecker->verify($form['agreement'], Validator::trueVal()->setName('agreement'));

/** @todo : move to locales */
$error_message = 'User could not be added';
$response_code = ResponseCode::HTTP_BAD_REQUEST;
if ($dataChecker->allValid()) {
$user = new User();
$error = $user->usernameOrEmailExists($form['username'], $form['email']);
if ($error) {
$this->logger->error('Registration error : user could not be added', ['error' => $error]);
$this->renderJson(['message' => $error], ResponseCode::HTTP_PRECONDITION_FAILED);
$user = new User();
if (!SecurityUtils::isGdprCompliant($form['password'])) {
$this->logger->error($error_message, ['error' => 'Only use letters, numbers, and common punctuation characters']);
$this->renderJson(['message' => 'Only use letters, numbers, and common punctuation characters'], $response_code);
} else {
$user->email = $form['email'];
$user->username = $form['username'];
$user->password = $form['password'];
$user->role_id = 2;
$user->status = UserStatus::PENDING;
$next = SecurityUtils::credentialsAreCommon($form['username'], $form['email'], $form['password'], $error_message, $response_code);
$users = $this->getUsersByUsernameOrEmail($form['username'], $form['email']);
$error = $user->usernameOrEmailExists($form['username'], $form['email'], $users);
if ($error && $next) {
$this->logger->error($error_message, ['error' => $error]);
$this->renderJson(['message' => $error], ResponseCode::HTTP_PRECONDITION_FAILED);
} elseif ($next) {
$user->email = $form['email'];
$user->username = $form['username'];
$user->password = $form['password'];
$user->role_id = 2;
$user->status = UserStatus::PENDING;

try {
$user->save();
} catch (\Exception $e) {
$message = 'user could not be added';
$this->logger->error('Registration error : user could not be added', ['user' => $user->toArray(), 'error' => $e->getMessage()]);
$this->renderJson(['message' => $message], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);
try {
$user->save();
} catch (\Exception $e) {
$this->logger->error($error_message, ['user' => $user->toArray(), 'error' => $e->getMessage()]);
$this->renderJson(['message' => $error_message], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);

return;
return;
}
$this->logger->info('User successfully registered', ['user' => $user->toArray()]);
$this->renderJson(['result' => 'success', ResponseCode::HTTP_CREATED]);
}
$this->logger->info('user successfully registered', ['user' => $user->toArray()]);
$this->renderJson(['result' => 'success', ResponseCode::HTTP_CREATED]);
}
} else {
$this->logger->error('Registration error', ['errors' => $dataChecker->getErrors()]);
$this->logger->error($error_message, ['errors' => $dataChecker->getErrors()]);
$this->renderJson(['errors' => $dataChecker->getErrors()], ResponseCode::HTTP_UNPROCESSABLE_ENTITY);
}
}
Expand Down
11 changes: 11 additions & 0 deletions hivelvet-backend/app/src/Actions/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,17 @@ protected function getCredentials(): array
return $credentials;
}

protected function getUsersByUsernameOrEmail(string $username, string $email): array
{
$dbname = 'hivelvet';
$user = 'hivelvet';
$secret = 'hivelvet';
$conn = pg_pconnect("host=localhost dbname={$dbname} user={$user} password={$secret}");
$result = pg_query_params($conn, 'SELECT username, email FROM public.users WHERE lower(username) = lower($1) OR lower(email) = lower($2)', [$username, $email]);

return pg_fetch_all($result);
}

private function parseXMLView(string $view = null): string
{
$xmlResponse = new SimpleXMLElement(Template::instance()->render($this->view . '.xml'));
Expand Down
5 changes: 4 additions & 1 deletion hivelvet-backend/app/src/Actions/Core/Install.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function execute($f3, $params): void

$dataChecker->verify($form['username'], Validator::length(4)->setName('username'));
$dataChecker->verify($form['email'], Validator::email()->setName('email'));
$dataChecker->verify($form['password'], Validator::length(4)->setName('password'));
$dataChecker->verify($form['password'], Validator::length(8)->setName('password'));

$dataChecker->verify($form['company_name'], Validator::notEmpty()->setName('company_name'));
$dataChecker->verify($form['company_url'], Validator::url()->setName('company_url'));
Expand Down Expand Up @@ -99,6 +99,7 @@ public function execute($f3, $params): void
$defaultSettings->accent_color = $colors['accent_color'];
$defaultSettings->additional_color = $colors['add_color'];

// @fixme: should not have embedded try/catch here
try {
$defaultSettings->save();
$this->logger->info('Initial application setup : Update settings', ['settings' => $defaultSettings->toArray()]);
Expand All @@ -114,6 +115,7 @@ public function execute($f3, $params): void
$presetSettings->name = $subcategory['name'];
$presetSettings->enabled = $subcategory['status'];

// @fixme: should not have embedded try/catch here
try {
$presetSettings->save();
$this->logger->info('Initial application setup : Add preset settings', ['preset' => $presetSettings->toArray()]);
Expand All @@ -140,6 +142,7 @@ public function execute($f3, $params): void
// assign admin created to role admin
$user->role_id = $roleAdmin->id;

// @fixme: should not have embedded try/catch here
try {
$user->save();
$this->logger->info('Initial application setup : Assign role to administrator user', ['user' => $user->toArray()]);
Expand Down
9 changes: 5 additions & 4 deletions hivelvet-backend/app/src/Actions/Roles/Add.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,24 @@ public function save($f3, $params): void

$dataChecker->verify($form['name'], Validator::notEmpty()->setName('name'));

$error_message = 'Role could not be added';
if ($dataChecker->allValid()) {
$checkRole = new Role();
$role = new Role();
$role->name = $form['name'];
if ($checkRole->nameExists($role->name)) {
$this->logger->error('Role could not be added', ['error' => 'Name already exist']);
$this->renderJson(['errors' => ['name' => 'Name already exist']], ResponseCode::HTTP_PRECONDITION_FAILED);
$this->logger->error($error_message, ['error' => 'Name already exists']);
$this->renderJson(['errors' => ['name' => 'Name already exists']], ResponseCode::HTTP_PRECONDITION_FAILED);
} else {
try {
$result = $role->saveRoleAndPermissions($form['permissions']);
if (!$result) {
$this->renderJson(['errors' => 'role could not be added'], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);
$this->renderJson(['errors' => $error_message], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);

return;
}
} catch (\Exception $e) {
$this->logger->error('Role could not be added', ['error' => $e->getMessage()]);
$this->logger->error($error_message, ['error' => $e->getMessage()]);
$this->renderJson(['errors' => $e->getMessage()], ResponseCode::HTTP_INTERNAL_SERVER_ERROR);

return;
Expand Down
Loading