User Spammer process

Moved from www spam user process
Upgraded to python 3.x

Change-Id: I38231566b30f293dd0214ee7782be213b9a11eee
Signed-off-by: smarcet <smarcet@gmail.com>
This commit is contained in:
smarcet 2020-03-06 18:12:15 -03:00
parent 8493cee023
commit 163238e6aa
42 changed files with 1209 additions and 194 deletions

View File

@ -1,5 +1,6 @@
APP_ENV=local
APP_DEBUG=true
DEV_EMAIL_TO=smarcet@gmail.com
APP_KEY=SomeRandomString
APP_URL=http://localhost
APP_OAUTH_2_0_CLIENT_ID=clientid

View File

@ -0,0 +1,4 @@
env/
.idea/
__pycache__/
user_classifier.pickle

View File

@ -0,0 +1,19 @@
## Dependencies
````bas
$ sudo apt update
$ sudo apt install python3-pip python3-dev build-essential libssl-dev libffi-dev python3-setuptools python3-venv
libmysqlclient-dev
````
## Virtual Env
````bash
$ python3.6 -m venv env
$ source env/bin/activate
$ pip install -r requirements.txt
````

View File

@ -0,0 +1,75 @@
<?php namespace App\Console\Commands\SpammerProcess;
/**
* 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\Console\Command;
use Symfony\Component\Process\Process;
use Exception;
/**
* Class RebuildUserSpammerEstimator
* @package App\Console\Commands\SpammerProcess
*/
final class RebuildUserSpammerEstimator extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'user-spam:rebuild';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user-spam:rebuild';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rebuild User spam estimator';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$command = sprintf(
'%s/app/Console/Commands/SpammerProcess/estimator_build.sh "%s" "%s" "%s" "%s" "%s"',
base_path(),
base_path().'/app/Console/Commands/SpammerProcess',
env('DB_HOST','localhost'),
env('DB_USERNAME',''),
env('DB_PASSWORD',''),
env('DB_DATABASE','')
);
$process = new Process($command);
$process->setTimeout(PHP_INT_MAX);
$process->setIdleTimeout(PHP_INT_MAX);
$process->run();
while ($process->isRunning()) {
}
$output = $process->getOutput();
if (!$process->isSuccessful()) {
throw new Exception("Process Error!");
}
}
}

View File

@ -0,0 +1,120 @@
<?php namespace App\Console\Commands\SpammerProcess;
/**
* 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 App\libs\Utils\CSVReader;
use App\Mail\UserSpammerProcessorResultsEmail;
use Auth\Repositories\IUserRepository;
use Auth\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\URL;
use Symfony\Component\Process\Process;
use Exception;
/**
* Class UserSpammerProcessor
* @package App\Console\Commands\SpammerProcess
*/
final class UserSpammerProcessor extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'user-spam:process';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user-spam:process';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process User spam estimator';
/**
* @var IUserRepository
*/
private $user_repository;
/**
* MemberSpammerProcessor constructor.
* @param IUserRepository $user_repository
*/
public function __construct(IUserRepository $user_repository)
{
parent::__construct();
$this->user_repository = $user_repository;
}
/**
* @throws Exception
*/
public function handle()
{
$command = sprintf(
'%s/app/Console/Commands/SpammerProcess/estimator_process.sh "%s" "%s" "%s" "%s" "%s"',
base_path(),
base_path().'/app/Console/Commands/SpammerProcess',
env('DB_HOST','localhost'),
env('DB_USERNAME',''),
env('DB_PASSWORD',''),
env('DB_DATABASE','')
);
$default = Config::get("database.default");
$process = new Process($command);
$process->setTimeout(PHP_INT_MAX);
$process->setIdleTimeout(PHP_INT_MAX);
$process->run();
while ($process->isRunning()) {
}
$csv_content = $process->getOutput();
if (!$process->isSuccessful()) {
throw new Exception("Process Error!");
}
$rows = CSVReader::load($csv_content);
// send email with excerpt
$users = [];
foreach($rows as $row) {
$user_id = intval($row["ID"]);
$type = $row["Type"];
$user = $this->user_repository->getById($user_id);
if(is_null($user) || !$user instanceof User) continue;
$users[] = [
'id' => $user->getId(),
'email' => $user->getEmail(),
'full_name' => $user->getFullName(),
'spam_type' => $type,
'edit_link' => URL::route("edit_user", ["user_id" => $user->getId()], true)
];
}
if(count($users) > 0){
Mail::queue(new UserSpammerProcessorResultsEmail($users));
}
}
}

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# !/usr/bin/env python
#
# Copyright (c) 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.
import sys
from openstack_member_spammer_estimator import EstimatorBuilder
import os
# params
db_host = sys.argv[1]
db_user = sys.argv[2]
db_user_password = sys.argv[3]
db_name = sys.argv[4]
filename = 'user_classifier.pickle'
builder = EstimatorBuilder(filename=filename, db_host=db_host, db_user=db_user, db_user_password=db_user_password,
db_name=db_name)
script_dir = os.path.dirname(__file__)
pickle_file = os.path.join(script_dir, )
if os.path.exists(pickle_file):
os.remove(pickle_file)
builder.build()

View File

@ -0,0 +1,31 @@
#!/bin/bash
# Copyright (c) 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.
WORK_DIR=$1
DB_HOST=$2
DB_USER=$3
DB_PASSWORD=$4
DB_NAME=$5
export PYTHONPATH="$PYTHONPATH:$WORK_DIR";
cd $WORK_DIR;
source env/bin/activate;
python estimator_build.py $DB_HOST $DB_USER $DB_PASSWORD $DB_NAME;
deactivate;

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
#
# Copyright (c) 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.
import sys
from openstack_member_spammer_estimator import EstimatorClassifier
import os
# params
db_host = sys.argv[1]
db_user = sys.argv[2]
db_user_password = sys.argv[3]
db_name = sys.argv[4]
filename = 'user_classifier.pickle'
classifier = EstimatorClassifier(db_host=db_host, db_user=db_user, db_user_password=db_user_password, db_name=db_name)
script_dir = os.path.dirname(__file__)
pickle_file = os.path.join(script_dir, filename)
if not os.path.exists(pickle_file):
raise Exception('File %s does not exists!' % pickle_file)
res = classifier.classify(pickle_file)
# output CSV file
print("ID,Type")
for row in res:
print("%s,%s" % row)

View File

@ -0,0 +1,31 @@
#!/bin/bash
# Copyright (c) 2017 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.
WORK_DIR=$1
DB_HOST=$2
DB_USER=$3
DB_PASSWORD=$4
DB_NAME=$5
export PYTHONPATH="$PYTHONPATH:$WORK_DIR";
cd $WORK_DIR;
source env/bin/activate;
python estimator_process.py $DB_HOST $DB_USER $DB_PASSWORD $DB_NAME;
deactivate;

View File

@ -0,0 +1,26 @@
openstack-member-spammer-estimator==1.0.2
pkg-resources==0.0.0
attrs==19.3.0
configparser==4.0.2
HTMLParser==0.0.2
importlib-metadata==1.5.0
joblib==0.14.1
more-itertools==8.2.0
mysqlclient==1.4.6
nltk==3.4.5
numpy==1.18.1
packaging==20.3
pandas==1.0.1
pluggy==0.13.1
py==1.8.1
pyparsing==2.4.6
pytest==5.3.5
python-dateutil==2.8.1
pytz==2019.3
scikit-learn==0.22.2.post1
scipy==1.4.1
six==1.14.0
sklearn==0.0
wcwidth==0.1.8
zipp==3.1.0

View File

@ -29,6 +29,8 @@ class Kernel extends ConsoleKernel
Commands\CleanOAuth2StaleData::class,
Commands\CleanOpenIdStaleData::class,
Commands\CreateSuperAdmin::class,
Commands\SpammerProcess\RebuildUserSpammerEstimator::class,
Commands\SpammerProcess\UserSpammerProcessor::class,
];
/**
@ -41,5 +43,8 @@ class Kernel extends ConsoleKernel
{
$schedule->command('idp:oauth2-clean')->dailyAt("02:30")->withoutOverlapping();
$schedule->command('idp:openid-clean')->dailyAt("03:30")->withoutOverlapping();
// user spammer
$schedule->command('user-spam:rebuild')->dailyAt("02:30")->withoutOverlapping();
$schedule->command('user-spam:process')->dailyAt("03:30")->withoutOverlapping();
}
}

View File

@ -0,0 +1,19 @@
<?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.
**/
/**
* Class UserActivated
* @package App\Events
*/
class UserActivated extends UserEvent {}

View File

@ -17,35 +17,4 @@ use Doctrine\ORM\Event\LifecycleEventArgs;
* Class UserCreated
* @package App\Events
*/
final class UserCreated
{
use SerializesModels;
/**
* @var int
*/
private $user_id;
/**
* @var LifecycleEventArgs
*/
protected $args;
/**
* UserEmailVerified constructor.
* @param int $user_id
*/
public function __construct(int $user_id, LifecycleEventArgs $args)
{
$this->user_id = $user_id;
$this->args = $args;
}
/**
* @return int
*/
public function getUserId(): int
{
return $this->user_id;
}
}
final class UserCreated extends UserEvent {}

View File

@ -0,0 +1,19 @@
<?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.
**/
/**
* Class UserDeactivated
* @package App\Events
*/
class UserDeactivated extends UserEvent {}

View File

@ -11,34 +11,8 @@
* 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;
}
}
class UserEmailUpdated extends UserEvent{}

View File

@ -11,35 +11,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Illuminate\Queue\SerializesModels;
/**
* Class UserEmailVerified
* @package App\Events
*/
final class UserEmailVerified
{
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;
}
}
final class UserEmailVerified extends UserEvent {}

44
app/Events/UserEvent.php Normal file
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 UserEvent
* @package App\Events
*/
abstract class UserEvent
{
use SerializesModels;
/**
* @var int
*/
protected $user_id;
/**
* UserEvent 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,35 +11,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Illuminate\Queue\SerializesModels;
/**
* Class UserLocked
* @package App\Events
*/
final class UserLocked
{
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;
}
}
final class UserLocked extends UserEvent{}

View File

@ -11,34 +11,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Illuminate\Queue\SerializesModels;
/**
* Class UserPasswordResetRequestCreated
* @package App\Events
*/
final class UserPasswordResetRequestCreated
{
use SerializesModels;
/**
* @var int
*/
private $id;
/**
* UserEmailVerified constructor.
* @param int $user_id
*/
public function __construct(int $id)
{
$this->id = $id;
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
}
final class UserPasswordResetRequestCreated extends UserEvent{}

View File

@ -11,35 +11,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Illuminate\Queue\SerializesModels;
/**
* Class UserPasswordResetSuccessful
* @package App\Events
*/
final class UserPasswordResetSuccessful
{
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;
}
}
final class UserPasswordResetSuccessful extends UserEvent{}

View File

@ -0,0 +1,22 @@
<?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.
**/
/**
* Class UserSpamStateUpdated
* @package App\Events
*/
class UserSpamStateUpdated extends UserEvent
{
}

View File

@ -141,7 +141,7 @@ Route::group(['namespace' => 'App\Http\Controllers', 'middleware' => 'web' ], fu
Route::group(array('prefix' => 'users'), function () {
Route::get('', 'AdminController@listUsers');
Route::group(array('prefix' => '{user_id}'), function () {
Route::get('', 'AdminController@editUser');
Route::get('', 'AdminController@editUser')->name("edit_user");
});
});

View File

@ -0,0 +1,52 @@
<?php namespace App\Mail;
/**
* 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\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
/**
* Class UserSpammerProcessorResultsEmail
* @package App\Mail
*/
class UserSpammerProcessorResultsEmail extends Mailable
{
use Queueable, SerializesModels;
/**
* @var array
*/
public $users;
public function __construct(array $users)
{
$this->users = $users;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$subject = sprintf("[%s] User Spammer Process Result", Config::get('app.app_name'));
return $this->from(Config::get("mail.from"))
->to(Config::get("mail.from"))
->subject($subject)
->view('emails.user_spammer_process_result');
}
}

View File

@ -31,6 +31,7 @@ final class PublicUserSerializer extends BaseUserSerializer {
final class PrivateUserSerializer extends BaseUserSerializer {
protected static $array_mappings = [
'Email' => 'email:json_string',
'SpamType' => 'spam_type:json_string',
'Identifier' => 'identifier:json_string',
'LastLoginDate' => 'last_login_date:datetime_epoch',
'Active' => 'active:json_boolean',

View File

@ -12,10 +12,13 @@
* limitations under the License.
**/
use App\Events\OAuth2ClientLocked;
use App\Events\UserActivated;
use App\Events\UserDeactivated;
use App\Events\UserEmailUpdated;
use App\Events\UserLocked;
use App\Events\UserPasswordResetRequestCreated;
use App\Events\UserPasswordResetSuccessful;
use App\Events\UserSpamStateUpdated;
use App\libs\Auth\Repositories\IUserPasswordResetRequestRepository;
use App\Mail\UserLockedEmail;
use App\Mail\UserPasswordResetMail;
@ -81,6 +84,16 @@ final class EventServiceProvider extends ServiceProvider
$user_service->sendVerificationEmail($user);
});
Event::listen(UserSpamStateUpdated::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->recalculateUserSpamType($user);
});
Event::listen(UserEmailUpdated::class, function($event)
{
$repository = App::make(IUserRepository::class);
@ -93,7 +106,7 @@ final class EventServiceProvider extends ServiceProvider
Event::listen(UserPasswordResetRequestCreated::class, function($event){
$repository = App::make(IUserPasswordResetRequestRepository::class);
$request = $repository->find($event->getId());
$request = $repository->find($event->getUserId());
if(is_null($request)) return;
});
@ -123,5 +136,14 @@ final class EventServiceProvider extends ServiceProvider
if(!$client instanceof Client) return;
Mail::queue(new \App\Mail\OAuth2ClientLocked($client));
});
Event::listen(\Illuminate\Mail\Events\MessageSending::class, function($event){
$devEmail = env('DEV_EMAIL_TO', null);
if(in_array(App::environment(), ['local','dev','testing']) && !empty($devEmail)){
$event->message->setTo(explode(",", $devEmail));
}
return true;
});
}
}

View File

@ -0,0 +1,46 @@
<?php namespace App\Repositories;
/**
* 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 App\libs\Auth\Models\SpamEstimatorFeed;
use App\libs\Auth\Repositories\ISpamEstimatorFeedRepository;
use Illuminate\Support\Facades\Log;
/**
* Class DoctrineSpamEstimatorFeedRepository
* @package App\Repositories
*/
final class DoctrineSpamEstimatorFeedRepository
extends ModelDoctrineRepository implements ISpamEstimatorFeedRepository
{
/**
* @inheritDoc
*/
protected function getBaseEntity()
{
return SpamEstimatorFeed::class;
}
public function deleteByEmail(string $email)
{
try {
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->delete(SpamEstimatorFeed::class, 'e');
$qb->where('e.email = :email');
$qb->setParameter('email', trim($email));
$qb->getQuery()->execute();
}
catch(\Exception $ex){
Log::error($ex);
}
}
}

View File

@ -11,10 +11,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\libs\Auth\Models\SpamEstimatorFeed;
use App\libs\Auth\Models\UserRegistrationRequest;
use App\libs\Auth\Repositories\IBannedIPRepository;
use App\libs\Auth\Repositories\IGroupRepository;
use App\libs\Auth\Repositories\ISpamEstimatorFeedRepository;
use App\libs\Auth\Repositories\IUserExceptionTrailRepository;
use App\libs\Auth\Repositories\IUserPasswordResetRequestRepository;
use App\libs\Auth\Repositories\IUserRegistrationRequestRepository;
@ -216,6 +217,13 @@ final class RepositoriesProvider extends ServiceProvider
}
);
App::singleton(
ISpamEstimatorFeedRepository::class,
function(){
return EntityManager::getRepository(SpamEstimatorFeed::class);
}
);
}
public function provides()
@ -237,6 +245,7 @@ final class RepositoriesProvider extends ServiceProvider
IResourceServerRepository::class,
IWhiteListedIPRepository::class,
IUserRegistrationRequestRepository::class,
ISpamEstimatorFeedRepository::class,
];
}
}

View File

@ -92,4 +92,10 @@ interface IUserService
* @throws EntityNotFoundException
*/
public function createRegistrationRequest(string $client_id, array $payload):UserRegistrationRequest;
/**
* @param User $user
* @return void
*/
public function recalculateUserSpamType(User $user):void;
}

View File

@ -14,8 +14,10 @@
use App\Events\UserPasswordResetSuccessful;
use App\libs\Auth\Factories\UserFactory;
use App\libs\Auth\Factories\UserRegistrationRequestFactory;
use App\libs\Auth\Models\SpamEstimatorFeed;
use App\libs\Auth\Models\UserRegistrationRequest;
use App\libs\Auth\Repositories\IGroupRepository;
use App\libs\Auth\Repositories\ISpamEstimatorFeedRepository;
use App\libs\Auth\Repositories\IUserPasswordResetRequestRepository;
use App\libs\Auth\Repositories\IUserRegistrationRequestRepository;
use App\Mail\UserEmailVerificationRequest;
@ -69,6 +71,11 @@ final class UserService extends AbstractService implements IUserService
*/
private $client_repository;
/**
* @var ISpamEstimatorFeedRepository
*/
private $spam_estimator_feed_repository;
/**
* UserService constructor.
* @param IUserRepository $user_repository
@ -76,6 +83,7 @@ final class UserService extends AbstractService implements IUserService
* @param IUserPasswordResetRequestRepository $request_reset_password_repository
* @param IUserRegistrationRequestRepository $user_registration_request_repository
* @param IClientRepository $client_repository
* @param ISpamEstimatorFeedRepository $spam_estimator_feed_repository
* @param IUserNameGeneratorService $name_generator_service
* @param ITransactionService $tx_service
*/
@ -86,6 +94,7 @@ final class UserService extends AbstractService implements IUserService
IUserPasswordResetRequestRepository $request_reset_password_repository,
IUserRegistrationRequestRepository $user_registration_request_repository,
IClientRepository $client_repository,
ISpamEstimatorFeedRepository $spam_estimator_feed_repository,
IUserNameGeneratorService $name_generator_service,
ITransactionService $tx_service
)
@ -96,6 +105,7 @@ final class UserService extends AbstractService implements IUserService
$this->name_generator_service = $name_generator_service;
$this->request_reset_password_repository = $request_reset_password_repository;
$this->user_registration_request_repository = $user_registration_request_repository;
$this->spam_estimator_feed_repository = $spam_estimator_feed_repository;
$this->client_repository = $client_repository;
}
@ -369,4 +379,24 @@ final class UserService extends AbstractService implements IUserService
return $request;
});
}
/**
* @inheritDoc
*/
public function recalculateUserSpamType(User $user): void
{
$this->tx_service->transaction(function() use($user) {
$this->spam_estimator_feed_repository->deleteByEmail($user->getEmail());
switch($user->getSpamType()){
case User::SpamTypeSpam:
$feed = SpamEstimatorFeed::buildFromUser($user, User::SpamTypeSpam);
$this->spam_estimator_feed_repository->add($feed);
break;
case User::SpamTypeHam:
$feed = SpamEstimatorFeed::buildFromUser($user, User::SpamTypeHam);
$this->spam_estimator_feed_repository->add($feed);
break;
}
});
}
}

View File

@ -274,7 +274,6 @@ final class UserService extends AbstractService implements IUserService
$user = $this->repository->getById($id);
if(is_null($user) || !$user instanceof User)
throw new EntityNotFoundException("user not found");
$this->repository->delete($user);
});
}

View File

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

View File

@ -134,8 +134,13 @@ final class UserFactory
}
}
if(isset($payload['active']))
$user->setActive(boolval($payload['active']));
if(isset($payload['active'])) {
$active = boolval($payload['active']);
if($active)
$user->activate();
else
$user->deActivate();
}
if(isset($payload['public_profile_show_photo']))
$user->setPublicProfileShowPhoto(boolval($payload['public_profile_show_photo']));

View File

@ -0,0 +1,149 @@
<?php namespace App\libs\Auth\Models;
/**
* 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 App\Models\Utils\BaseEntity;
use Auth\User;
use Doctrine\ORM\Mapping AS ORM;
/**
* @ORM\Entity(repositoryClass="App\Repositories\DoctrineSpamEstimatorFeedRepository")
* @ORM\Table(name="users_spam_estimator_feed")
* Class SpamEstimatorFeed
* @package App\libs\Auth\Models
*/
class SpamEstimatorFeed extends BaseEntity
{
/**
* @ORM\Column(name="first_name", type="string")
* @var string
*/
private $first_name;
/**
* @ORM\Column(name="last_name", type="string")
* @var string
*/
private $last_name;
/**
* @ORM\Column(name="email", type="string")
* @var string
*/
private $email;
/**
* @ORM\Column(name="bio", nullable=true, type="string")
* @var string
*/
private $bio;
/**
* @ORM\Column(name="spam_type", nullable=false, type="string")
* @var string
*/
private $spam_type;
/**
* @return string
*/
public function getFirstName(): ?string
{
return $this->first_name;
}
/**
* @param string $first_name
*/
public function setFirstName(string $first_name): void
{
$this->first_name = $first_name;
}
/**
* @return string
*/
public function getLastName(): ?string
{
return $this->last_name;
}
/**
* @param string $last_name
*/
public function setLastName(string $last_name): void
{
$this->last_name = $last_name;
}
/**
* @return string
*/
public function getEmail(): ?string
{
return $this->email;
}
/**
* @param string $email
*/
public function setEmail(string $email): void
{
$this->email = $email;
}
/**
* @return string
*/
public function getBio(): ?string
{
return $this->bio;
}
/**
* @param string $bio
*/
public function setBio(string $bio): void
{
$this->bio = $bio;
}
/**
* @return string
*/
public function getSpamType(): ?string
{
return $this->spam_type;
}
/**
* @param string $spam_type
*/
public function setSpamType(string $spam_type): void
{
$this->spam_type = $spam_type;
}
/**
* @param User $user
* @param string $spam_type
* @return SpamEstimatorFeed
*/
public static function buildFromUser(User $user, string $spam_type){
$feed = new SpamEstimatorFeed;
$feed->spam_type = $spam_type;
$feed->email = $user->getEmail();
$feed->first_name = $user->getFirstName();
$feed->last_name = $user->getLastName();
$feed->bio = $user->getBio();
return $feed;
}
}

View File

@ -11,10 +11,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Events\UserCreated;
use App\Events\UserLocked;
use App\Events\UserSpamStateUpdated;
use App\libs\Auth\Models\IGroupSlugs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Illuminate\Support\Facades\Event;
use App\Events\UserEmailVerified;
use Doctrine\Common\Collections\Criteria;
@ -48,6 +49,16 @@ class User extends BaseEntity
use Authenticatable;
use CanResetPasswordTrait;
const SpamTypeNone = 'None';
const SpamTypeSpam = 'Spam';
const SpamTypeHam = 'Ham';
const ValidSpamTypes = [
self::SpamTypeNone,
self::SpamTypeSpam,
self::SpamTypeHam
];
/**
* @ORM\Column(name="identifier", type="string")
* @var string
@ -263,6 +274,12 @@ class User extends BaseEntity
*/
private $birthday;
/**
* @ORM\Column(name="spam_type", nullable=false, type="string")
* @var string
*/
private $spam_type;
// relations
/**
@ -803,21 +820,12 @@ class User extends BaseEntity
return $this->active;
}
/**
* @param bool $active
*/
public function setActive(bool $active): void
{
$this->active = $active;
}
/**
* @return $this
*/
public function lock()
{
$this->active = false;
$this->deActivate();
Event::fire(new UserLocked($this->getId()));
return $this;
}
@ -827,7 +835,7 @@ class User extends BaseEntity
*/
public function unlock()
{
$this->active = true;
$this->activate();
return $this;
}
@ -1385,8 +1393,31 @@ SQL;
$this->bio = $bio;
}
public function activate():void {
if(!$this->active) {
$this->active = true;
$this->spam_type = self::SpamTypeHam;
Event::fire(new UserSpamStateUpdated(
$this->getId()
)
);
}
}
public function deActivate():void {
if( $this->active) {
$this->active = false;
$this->spam_type = self::SpamTypeSpam;
Event::fire(new UserSpamStateUpdated(
$this->getId()
)
);
}
}
/**
* @return $this
* @throws \Exception
*/
public function verifyEmail()
{
@ -1449,11 +1480,36 @@ SQL;
}
/**
* @ORM\PostPersist
* @ORM\postPersist
*/
public function inserted($args)
public function postPersist($args)
{
Event::fire(new UserCreated($this->getId(), $args));
Event::fire(new UserCreated($this->getId()));
}
/**
* @ORM\preRemove
*/
public function preRemove($args)
{
}
/**
* @ORM\preUpdate
* @param PreUpdateEventArgs $args
*/
public function preUpdate(PreUpdateEventArgs $args)
{
if($this->spam_type != self::SpamTypeNone &&
!$args->hasChangedField("active") &&
($args->hasChangedField("bio") || $args->hasChangedField("email"))) {
// enqueue user for spam re checker
$this->resetSpamTypeClassification();
Event::fire(new UserSpamStateUpdated($this->getId()));
}
if($args->hasChangedField("email")) {
// record email change
}
}
/**
@ -1554,4 +1610,43 @@ SQL;
$this->reset_password_requests->clear();
}
/**
* @return string|null
*/
public function getSpamType(): ?string
{
return $this->spam_type;
}
/**
* @param string $spam_type
* @throws ValidationException
*/
public function setSpamType(string $spam_type): void
{
if(!in_array($spam_type, self::ValidSpamTypes))
throw new ValidationException(sprintf("Not valid %s spam type value.", $spam_type));
$this->spam_type = $spam_type;
}
public function resetSpamTypeClassification():void{
$this->spam_type = self::SpamTypeNone;
}
/**
* @return bool
*/
public function isHam():bool{
return $this->spam_type == self::SpamTypeHam;
}
/**
* @return bool
*/
public function isSpam():bool{
return $this->spam_type == self::SpamTypeSpam;
}
}

View File

@ -0,0 +1,22 @@
<?php namespace App\libs\Auth\Repositories;
/**
* 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 models\utils\IBaseRepository;
/**
* Interface ISpamEstimatorFeedRepository
* @package App\libs\Auth\Repositories
*/
interface ISpamEstimatorFeedRepository extends IBaseRepository
{
public function deleteByEmail(string $email);
}

View File

@ -0,0 +1,76 @@
<?php namespace App\libs\Utils;
/**
* 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 InvalidArgumentException;
/**
* Class CSVReader
*/
final class CSVReader {
/**
* @var resource
*/
private $file_handle;
/**
* @param string $filename
* @throws InvalidArgumentException
*/
public function __construct($filename = null){
if(is_null($filename)) return;
if(!file_exists($filename))
throw new InvalidArgumentException;
$this->file_handle = fopen($filename, "r");
if(!$this->file_handle)
throw new InvalidArgumentException;
}
/**
* @param string $content
* @return array
*/
public static function load($content)
{
$data = str_getcsv($content,"\n" );
$lines = array();
$header = array();
$idx = 0;
foreach($data as $row)
{
$row = str_getcsv($row, ",");
++$idx;
if($idx === 1) { $header = $row; continue;}
$line = array();
for($i=0; $i < count($header); $i++){
$line[$header[$i]] = trim($row[$i]);
}
$lines[] = $line;
} //parse the items in rows
return $lines;
}
function __destruct() {
if($this->file_handle) fclose($this->file_handle);
}
/**
* @return array|bool
*/
function getLine(){
if (!feof($this->file_handle) ) {
return fgetcsv($this->file_handle, 1024);
}
return false;
}
}

View File

@ -0,0 +1,91 @@
<?php namespace Database\Migrations;
/**
* 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 Doctrine\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema as Schema;
use LaravelDoctrine\Migrations\Schema\Table;
use LaravelDoctrine\Migrations\Schema\Builder;
/**
* Class Version20200306133045
* @package Database\Migrations
*/
class Version20200306133045 extends AbstractMigration
{
/**
* @param Schema $schema
* @throws \Doctrine\DBAL\Schema\SchemaException
*/
public function up(Schema $schema)
{
$builder = new Builder($schema);
if($schema->hasTable("users") && !$builder->hasColumn("users","spam_type") ) {
$builder->table('users', function (Table $table) {
$table->string('spam_type')->setNotnull(true)->setDefault('None');
});
}
if(!$schema->hasTable("users_spam_estimator_feed")) {
$builder->create('users_spam_estimator_feed', function (Table $table) {
$table->increments('id');
$table->timestamps();
$table->string("first_name", 100)->setNotnull(false);
$table->string("last_name", 100)->setNotnull(false);
$table->string("email", 255)->setNotnull(false);
$table->unique("email");
$table->text("bio")->setNotnull(false);
$table->string('spam_type')->setNotnull(true)->setDefault('None');
});
}
if(!$schema->hasTable("users_deleted")) {
$builder->create('users_deleted', function (Table $table) {
$table->increments('id');
$table->timestamps();
$table->string("first_name", 100)->setNotnull(false);
$table->string("last_name", 100)->setNotnull(false);
$table->string("email", 255)->setNotnull(false);
$table->unique("email");
$table->bigInteger("performer_id")->setUnsigned(true);
$table->index("performer_id", "performer_id");
$table->foreign("users", "performer_id", "id", ["onDelete" => "CASCADE"]);
});
}
if(!$schema->hasTable("users_email_changed")) {
$builder->create('users_email_changed', function (Table $table) {
$table->increments('id');
$table->timestamps();
$table->string("former_email", 255)->setNotnull(false);
$table->string("new_email", 255)->setNotnull(false);
$table->bigInteger("user_id")->setUnsigned(true);
$table->index("user_id", "user_id");
$table->foreign("users", "user_id", "id", ["onDelete" => "CASCADE"]);
$table->bigInteger("performer_id")->setUnsigned(true);
$table->index("performer_id", "performer_id");
$table->foreign("users", "performer_id", "id", ["onDelete" => "CASCADE"]);
});
}
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
$builder = new Builder($schema);
$builder->dropIfExists("users_email_changed");
$builder->dropIfExists("users_deleted");
$builder->dropIfExists("users_spam_estimator_feed");
}
}

View File

@ -0,0 +1,59 @@
<?php namespace Database\Migrations;
/**
* 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 Doctrine\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema as Schema;
/**
* Class Version20200306135446
* @package Database\Migrations
*/
class Version20200306135446 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
$sql = <<<SQL
ALTER TABLE users MODIFY spam_type
enum(
'None', 'Ham', 'Spam'
) default 'None' null;
SQL;
$this->addSql($sql);
$sql = <<<SQL
ALTER TABLE users_spam_estimator_feed MODIFY spam_type
enum(
'None', 'Ham', 'Spam'
) default 'None' null;
SQL;
$this->addSql($sql);
// reset spam state to Ham
$sql = <<<SQL
UPDATE users set spam_type = 'Ham';
SQL;
$this->addSql($sql);
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
}
}

View File

@ -11,6 +11,7 @@ function UsersCrud(urls, perPage) {
'<td class="user-email"></td>' +
'<td class="user-active"><input type="checkbox" class="user-active-checkbox"></td>' +
'<td class="user-last-login"></td>' +
'<td class="user-spam-type"></td>' +
'<td class="user-actions">&nbsp;' + actions + '</td>' +
'</tr></tbody>');
@ -21,6 +22,7 @@ function UsersCrud(urls, perPage) {
'td.user-fname': 'user.first_name',
'td.user-lname': 'user.last_name',
'td.user-email': 'user.email',
'td.user-spam-type': 'user.spam_type',
'td.user-last-login': function (arg) {
if (arg.item.last_login_date == null) return 'N/A';
return moment.unix(arg.item.last_login_date).format();
@ -80,7 +82,7 @@ UsersCrud.prototype.init = function () {
url: url,
contentType: "application/json; charset=utf-8",
success: function (data, textStatus, jqXHR) {
_this.loadPage();
},
error: function (jqXHR, textStatus, errorThrown) {
ajaxError(jqXHR, textStatus, errorThrown);

View File

@ -178,7 +178,10 @@
/>&nbsp;Email Verified?
</label>
</div>
<div class="col-xs-10 col-sm-4 col-md-12 col-lg-12">
<label for="spam-type">Spam Type</label>
<input type="text" readonly class="form-control" id="spam-type" name="spam-type" data-lpignore="true" value="{!! $user->spam_type !!}">
</div>
<button type="submit" class="btn btn-default btn-lg btn-primary">Save</button>
<input type="hidden" name="id" id="id" value="{!! $user->id !!}"/>
</form>

View File

@ -30,6 +30,7 @@
<th>Email</th>
<th>Active</th>
<th>Last Login Date</th>
<th>Spam Type</th>
<th>&nbsp;</th>
</tr>
</thead>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
</head>
<body>
<p>
<ul>
@foreach($users as $user)
<li>
[{!! $user['spam_type'] !!}] - {!! $user['full_name'] !!} ({!! $user['email'] !!}) <a href="{!! $user['edit_link'] !!}" target="_blank">Edit</a>
</li>
@endforeach
</ul>
</p>
<p>Cheers,<br/>{!! Config::get('app.tenant_name') !!} Support Team</p>
</body>
</html>