Merge "Security fixes"

This commit is contained in:
Zuul 2020-02-29 04:19:12 +00:00 committed by Gerrit Code Review
commit 661199b43f
11 changed files with 298 additions and 137 deletions

View File

@ -0,0 +1,44 @@
<?php namespace App\Events;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Illuminate\Queue\SerializesModels;
/**
* Class UserEmailUpdated
* @package App\Events
*/
class UserEmailUpdated
{
use SerializesModels;
/**
* @var int
*/
private $user_id;
/**
* UserEmailVerified constructor.
* @param int $user_id
*/
public function __construct(int $user_id)
{
$this->user_id = $user_id;
}
/**
* @return int
*/
public function getUserId(): int
{
return $this->user_id;
}
}

View File

@ -11,13 +11,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Http\Controllers\Controller;
use App\Services\Auth\IUserService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\Request as LaravelRequest;
use models\exceptions\ValidationException;
use OAuth2\Repositories\IClientRepository;
/**
* Class ForgotPasswordController
* @package App\Http\Controllers\Auth
@ -29,24 +32,58 @@ final class ForgotPasswordController extends Controller
*/
private $user_service;
/**
* @var IClientRepository
*/
private $client_repository;
/**
* ForgotPasswordController constructor.
* @param IClientRepository $client_repository
* @param IUserService $user_service
*/
public function __construct(IUserService $user_service)
public function __construct
(
IClientRepository $client_repository,
IUserService $user_service
)
{
$this->middleware('guest');
$this->user_service = $user_service;
$this->client_repository = $client_repository;
}
/**
* Display the form to request a password reset link.
*
* @return \Illuminate\Http\Response
* @param LaravelRequest $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showLinkRequestForm()
public function showLinkRequestForm(LaravelRequest $request)
{
return view('auth.passwords.email');
try {
$params = [
"redirect_uri" => '',
"client_id" => '',
];
// check if we have explicit params at query string
if ($request->has("redirect_uri") && $request->has("client_id")) {
$redirect_uri = $request->get("redirect_uri");
$client_id = $request->get("client_id");
$client = $this->client_repository->getClientById($client_id);
if (is_null($client))
throw new ValidationException("client does not exists");
if (!$client->isUriAllowed($redirect_uri))
throw new ValidationException(sprintf("redirect_uri %s is not allowed on associated client", $redirect_uri));
$params['redirect_uri'] = $redirect_uri;
$params['client_id'] = $client_id;
}
return view('auth.passwords.email', $params);
} catch (\Exception $ex) {
Log::warning($ex);
}
return view("auth.passwords.email_error");
}
/**
@ -63,24 +100,44 @@ final class ForgotPasswordController extends Controller
if (!$validator->passes()) {
return back()
->withInput($request->only('email'))
->withInput($request->only('email', 'client_id', 'redirect_uri'))
->withErrors($validator);
}
$this->user_service->requestPasswordReset($payload);
return $this->sendResetLinkResponse("Reset link sent");
$params = [
'client_id' => '',
'redirect_uri' => '',
];
// check redirect uri with associated client
if($request->has("redirect_uri") && $request->has("client_id")){
$redirect_uri = $request->get("redirect_uri");
$client_id = $request->get("client_id");
$client = $this->client_repository->getClientById($client_id);
if(is_null($client))
throw new ValidationException("client does not exists");
if(!$client->isUriAllowed($redirect_uri))
throw new ValidationException(sprintf("redirect_uri %s is not allowed on associated client", $redirect_uri));
$params['client_id'] = $client_id;
$params['redirect_uri'] = $redirect_uri;
}
catch (ValidationException $ex){
$params['status'] = 'Reset link sent';
return back()->with($params);
} catch (ValidationException $ex) {
Log::warning($ex);
foreach ($ex->getMessages() as $message) {
$validator->getMessageBag()->add('validation', $message);
}
return back()
->withInput($request->only('email'))
->withInput($request->only(['email', 'client_id', 'redirect_uri']))
->withErrors($validator);
}
catch(\Exception $ex){
} catch (\Exception $ex) {
Log::warning($ex);
}
return view("auth.passwords.email_error");
@ -107,20 +164,7 @@ final class ForgotPasswordController extends Controller
*/
protected function sendResetLinkResponse($response)
{
return back()->with('status', trans($response));
}
/**
* Get the response for a failed password reset link.
*
* @param \Illuminate\Http\Request $request
* @param string $response
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
protected function sendResetLinkFailedResponse(LaravelRequest $request, $response)
{
return back()
->withInput($request->only('email'))
->withErrors(['email' => trans($response)]);
}
}

View File

@ -93,7 +93,6 @@ final class RegisterController extends Controller
if ($oauth_auth_request->isValid()) {
$redirect_uri = $oauth_auth_request->getRedirectUri();
$client_id = $oauth_auth_request->getClientId();
@ -189,7 +188,6 @@ final class RegisterController extends Controller
'redirect_uri' => '',
];
// check if we have a former oauth2 request
if ($this->memento_service->exists()) {

View File

@ -12,6 +12,7 @@
* limitations under the License.
**/
use App\Events\OAuth2ClientLocked;
use App\Events\UserEmailUpdated;
use App\Events\UserLocked;
use App\Events\UserPasswordResetRequestCreated;
use App\Events\UserPasswordResetSuccessful;
@ -78,14 +79,22 @@ final class EventServiceProvider extends ServiceProvider
Mail::queue(new WelcomeNewUserEmail($user));
if(!$user->isEmailVerified() && !$user->hasCreator())
$user_service->sendVerificationEmail($user);
});
Event::listen(UserEmailUpdated::class, function($event)
{
$repository = App::make(IUserRepository::class);
$user = $repository->getById($event->getUserId());
if(is_null($user)) return;
if(! $user instanceof User) return;
$user_service = App::make(IUserService::class);
$user_service->sendVerificationEmail($user);
});
Event::listen(UserPasswordResetRequestCreated::class, function($event){
$repository = App::make(IUserPasswordResetRequestRepository::class);
$request = $repository->find($event->getId());
if(is_null($request)) return;
});
Event::listen(UserLocked::class, function($event){

View File

@ -230,11 +230,12 @@ final class UserService extends AbstractService implements IUserService
{
return $this->tx_service->transaction(function() use($payload) {
$user = $this->user_repository->getByEmailOrName(trim($payload['email']));
if(is_null($user))
throw new EntityNotFoundException("user not found");
if(is_null($user) || !$user->isEmailVerified())
throw new EntityNotFoundException("User not found.");
$request = new UserPasswordResetRequest();
$request->setOwner($user);
do{
$token = $request->generateToken();
$former_request = $this->request_reset_password_repository->getByToken($token);

View File

@ -11,17 +11,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Events\UserEmailUpdated;
use App\libs\Auth\Factories\UserFactory;
use App\libs\Auth\Repositories\IGroupRepository;
use App\Services\AbstractService;
use Auth\IUserNameGeneratorService;
use Auth\Repositories\IUserRepository;
use Auth\User;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use models\exceptions\EntityNotFoundException;
use models\exceptions\ValidationException;
use models\utils\IEntity;
use OpenId\Services\IUserService;
use phpDocumentor\Reflection\Types\Parent_;
use Utils\Db\ITransactionService;
use Utils\Services\ILogService;
use Utils\Services\IServerConfigurationService;
@ -225,6 +227,7 @@ final class UserService extends AbstractService implements IUserService
if(is_null($user) || !$user instanceof User)
throw new EntityNotFoundException("user not found");
$former_email = $user->getEmail();
if(isset($payload["email"])){
$former_user = $this->repository->getByEmailOrName(trim($payload["email"]));
if(!is_null($former_user) && $former_user->getId() != $id)
@ -249,6 +252,12 @@ final class UserService extends AbstractService implements IUserService
}
}
if($former_email != $user->getEmail()){
Log::debug(sprintf("UserService::update use id %s - email changed old %s - email new %s", $id, $former_email , $user->getEmail()));
$user->clearEmailVerification();
Event::fire(new UserEmailUpdated($user->getId()));
}
return $user;
});

View File

@ -151,6 +151,7 @@ class CustomAuthProvider implements UserProvider
$user->setLastLoginDate(new \DateTime('now', new \DateTimeZone('UTC')));
$user->setLoginFailedAttempt(0);
$user->setActive(true);
$user->clearResetPasswordRequests();
$auth_extensions = $this->auth_extension_service->getExtensions();

View File

@ -45,8 +45,10 @@ final class UserFactory
$user->setFirstName(trim($payload['first_name']));
if(isset($payload['last_name']))
$user->setLastName(trim($payload['last_name']));
if(isset($payload['email']))
$user->setEmail(strtolower(trim($payload['email'])));
if(isset($payload['second_email']))
$user->setSecondEmail(strtolower(trim($payload['second_email'])));
if(isset($payload['third_email']))

View File

@ -11,6 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Events\UserCreated;
use App\Events\UserLocked;
use App\libs\Auth\Models\IGroupSlugs;
@ -33,6 +34,7 @@ use Illuminate\Auth\Passwords\CanResetPassword as CanResetPasswordTrait;
use Doctrine\Common\Collections\ArrayCollection;
use App\Models\Utils\BaseEntity;
use Doctrine\ORM\Mapping AS ORM;
/**
* @ORM\Entity(repositoryClass="App\Repositories\DoctrineUserRepository")
* @ORM\Table(name="users")
@ -557,7 +559,8 @@ class User extends BaseEntity
/**
* @return bool
*/
public function isSuperAdmin():bool{
public function isSuperAdmin(): bool
{
return $this->belongToGroup(IGroupSlugs::SuperAdminGroup);
}
@ -565,7 +568,8 @@ class User extends BaseEntity
* @param string $slug
* @return bool
*/
public function belongToGroup(string $slug):bool{
public function belongToGroup(string $slug): bool
{
$criteria = new Criteria();
$criteria->where(Criteria::expr()->eq('slug', $slug));
return $this->groups->matching($criteria)->count() > 0;
@ -574,7 +578,8 @@ class User extends BaseEntity
/**
* @param Group $group
*/
public function addToGroup(Group $group){
public function addToGroup(Group $group)
{
if ($this->groups->contains($group)) return;
$this->groups->add($group);
$group->addUser($this);
@ -583,13 +588,15 @@ class User extends BaseEntity
/**
* @param Group $group
*/
public function removeFromGroup(Group $group){
public function removeFromGroup(Group $group)
{
if (!$this->groups->contains($group)) return;
$this->groups->removeElement($group);
$group->removeUser($this);
}
public function clearGroups():void{
public function clearGroups(): void
{
$this->groups->clear();
}
@ -622,7 +629,8 @@ class User extends BaseEntity
/**
* @param OpenIdTrustedSite $site
*/
public function addTrustedSite(OpenIdTrustedSite $site) {
public function addTrustedSite(OpenIdTrustedSite $site)
{
if ($this->trusted_sites->contains($site)) return;
$this->trusted_sites->add($site);
$site->setOwner($this);
@ -700,8 +708,7 @@ class User extends BaseEntity
$active_scope_groups = $this->scope_groups->matching($criteria);
foreach ($active_scope_groups as $group) {
foreach($group->getScopes() as $scope)
{
foreach ($group->getScopes() as $scope) {
if (!isset($map[$scope->getId()]))
$scopes[] = $scope;
}
@ -715,7 +722,8 @@ class User extends BaseEntity
* @return bool
* @throws ValidationException
*/
public function isGroupScopeAllowed(ApiScope $scope):bool{
public function isGroupScopeAllowed(ApiScope $scope): bool
{
if (!$scope->isAssignedByGroups()) throw new ValidationException("scope is not assigned by groups!");
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('active', true));
@ -726,6 +734,10 @@ class User extends BaseEntity
return false;
}
public function clearEmailVerification(){
$this->email_verified = false;
$this->email_verified_date = null;
}
/**
* @return bool
@ -735,7 +747,8 @@ class User extends BaseEntity
return $this->email_verified;
}
public function clearTrustedSites():void{
public function clearTrustedSites(): void
{
$this->trusted_sites->clear();
}
@ -760,11 +773,13 @@ class User extends BaseEntity
/**
* Get either a Gravatar URL or complete image tag for a specified email address.
*/
private function getGravatarUrl( ):string {
private function getGravatarUrl(): string
{
$url = 'https://www.gravatar.com/avatar/';
$url .= md5(strtolower(trim($this->email)));
return $url;
}
/**
* @param string $password
* @return bool
@ -1236,7 +1251,8 @@ class User extends BaseEntity
/**
* @param UserAction $action
*/
public function addUserAction(UserAction $action){
public function addUserAction(UserAction $action)
{
if ($this->actions->contains($action)) return;
$this->actions->add($action);
$action->setOwner($this);
@ -1304,20 +1320,23 @@ SQL;
/**
* @param UserConsent $consent
*/
public function addConsent(UserConsent $consent){
public function addConsent(UserConsent $consent)
{
if ($this->consents->contains($consent)) return;
$this->consents->add($consent);
$consent->setOwner($this);
}
public function updateLastLoginDate():void{
public function updateLastLoginDate(): void
{
$this->last_login_date = new \DateTime('now', new \DateTimeZone('UTC'));
}
/**
* @return int
*/
public function updateLoginFailedAttempt():int {
public function updateLoginFailedAttempt(): int
{
$this->login_failed_attempt = $this->login_failed_attempt + 1;
return $this->login_failed_attempt;
}
@ -1343,6 +1362,10 @@ SQL;
*/
public function setEmail(string $email): void
{
if (!empty($this->email) && $email != $this->email) {
//we are setting a new email
$this->clearResetPasswordRequests();
}
$this->email = $email;
}
@ -1365,7 +1388,8 @@ SQL;
/**
* @return $this
*/
public function verifyEmail(){
public function verifyEmail()
{
if (!$this->email_verified) {
$this->email_verified = true;
$this->active = true;
@ -1419,14 +1443,16 @@ SQL;
/**
* @param string $identifier
*/
public function setIdentifier(string $identifier){
public function setIdentifier(string $identifier)
{
$this->identifier = $identifier;
}
/**
* @ORM\PostPersist
*/
public function inserted($args){
public function inserted($args)
{
Event::fire(new UserCreated($this->getId(), $args));
}
@ -1434,7 +1460,8 @@ SQL;
* @param $name
* @return mixed
*/
public function __get($name) {
public function __get($name)
{
if ($name == "fullname")
return $this->getFullName();
@ -1464,7 +1491,8 @@ SQL;
/**
* @param UserPasswordResetRequest $request
*/
public function addPasswordResetRequest(UserPasswordResetRequest $request){
public function addPasswordResetRequest(UserPasswordResetRequest $request)
{
if ($this->reset_password_requests->contains($request)) return;
$this->reset_password_requests->add($request);
}
@ -1488,14 +1516,16 @@ SQL;
/**
* @return bool
*/
public function hasCreator():bool{
public function hasCreator(): bool
{
return $this->getCreatedById() > 0;
}
/**
* @return int
*/
public function getCreatedById():int{
public function getCreatedById(): int
{
try {
return !is_null($this->created_by) ? $this->created_by->getId() : 0;
} catch (\Exception $ex) {
@ -1519,4 +1549,9 @@ SQL;
$this->twitter_name = $twitter_name;
}
public function clearResetPasswordRequests(): void
{
$this->reset_password_requests->clear();
}
}

View File

@ -26,6 +26,15 @@
return true;
});
$(document).ready(function($){
var redirect = $('#redirect_url');
if(redirect.length > 0){
var href = $(redirect).attr('href');
setTimeout(function(){ window.location = href; }, 3000);
}
});
});
// End of closure.

View File

@ -20,6 +20,9 @@
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{{ session('status') }}
</div>
@if($redirect_uri)
<p>Now you will be redirected to <a id="redirect_url" name="redirect_url" href="{{$redirect_uri}}">{{$redirect_uri}}</a></p>
@endif
@endif
@if ($errors->any())
<div class="alert alert-danger alert-dismissible" role="alert">
@ -48,6 +51,12 @@
<div class="form-group">
<button type="submit" class="btn btn-primary btn-lg btn-block">{{ __('Send Password Reset Link') }}</button>
</div>
@if($redirect_uri)
<input type="hidden" id="redirect_uri" name="redirect_uri" value="{{$redirect_uri}}"/>
@endif
@if($client_id)
<input type="hidden" id="client_id" name="client_id" value="{{$client_id}}"/>
@endif
</form>
</div>
</div>