From 7dbdbf15a5fcfcd11726b4f3fabffa53cf08bb72 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Wed, 27 Dec 2023 02:51:47 -0700 Subject: [PATCH 1/6] Add Roles & Parental Controls --- app/Http/Controllers/Api/ApiV1Controller.php | 10 ++ app/Http/Controllers/UserRolesController.php | 10 ++ app/Models/UserRoles.php | 23 ++++ app/Providers/AuthServiceProvider.php | 2 +- app/Services/UserRoleService.php | 111 ++++++++++++++++++ ...3_12_27_081801_create_user_roles_table.php | 30 +++++ ...27_082024_add_has_roles_to_users_table.php | 28 +++++ 7 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/UserRolesController.php create mode 100644 app/Models/UserRoles.php create mode 100644 app/Services/UserRoleService.php create mode 100644 database/migrations/2023_12_27_081801_create_user_roles_table.php create mode 100644 database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 798d9ee55..6f314e0b3 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -98,6 +98,7 @@ use App\Jobs\MediaPipeline\MediaSyncLicensePipeline; use App\Services\DiscoverService; use App\Services\CustomEmojiService; use App\Services\MarkerService; +use App\Services\UserRoleService; use App\Models\Conversation; use App\Jobs\FollowPipeline\FollowAcceptPipeline; use App\Jobs\FollowPipeline\FollowRejectPipeline; @@ -1623,6 +1624,8 @@ class ApiV1Controller extends Controller ]); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); + AccountService::setLastActive($user->id); if($user->last_active_at == null) { @@ -1792,6 +1795,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); $media = Media::whereUserId($user->id) @@ -1831,6 +1835,7 @@ class ApiV1Controller extends Controller ]); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); if($user->last_active_at == null) { return []; @@ -2419,8 +2424,13 @@ class ApiV1Controller extends Controller $max = $request->input('max_id'); $limit = $request->input('limit') ?? 20; $user = $request->user(); + $remote = $request->has('remote'); $local = $request->has('local'); + $userRoleKey = $remote ? 'can-view-network-feed' : 'can-view-public-feed'; + if($user->has_roles && !UserRoleService::can($userRoleKey, $user->id)) { + return []; + } $filtered = $user ? UserFilterService::filters($user->profile_id) : []; AccountService::setLastActive($user->id); $domainBlocks = UserFilterService::domainBlocks($user->profile_id); diff --git a/app/Http/Controllers/UserRolesController.php b/app/Http/Controllers/UserRolesController.php new file mode 100644 index 000000000..dbd34d0da --- /dev/null +++ b/app/Http/Controllers/UserRolesController.php @@ -0,0 +1,10 @@ + 'array' + ]; + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 0ec8c1895..8e1a6a98d 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -14,7 +14,7 @@ class AuthServiceProvider extends ServiceProvider * @var array */ protected $policies = [ - 'App\Model' => 'App\Policies\ModelPolicy', + // 'App\Model' => 'App\Policies\ModelPolicy', ]; /** diff --git a/app/Services/UserRoleService.php b/app/Services/UserRoleService.php new file mode 100644 index 000000000..fef5356c2 --- /dev/null +++ b/app/Services/UserRoleService.php @@ -0,0 +1,111 @@ +first()) { + return $roles->roles; + } + + return self::defaultRoles(); + } + + public static function roleKeys() + { + return array_keys(self::defaultRoles()); + } + + public static function defaultRoles() + { + return [ + 'account-force-private' => true, + 'account-ignore-follow-requests' => true, + + 'can-view-public-feed' => true, + 'can-view-network-feed' => true, + 'can-view-discover' => true, + + 'can-post' => true, + 'can-comment' => true, + 'can-like' => true, + 'can-share' => true, + + 'can-follow' => false, + 'can-make-public' => false, + ]; + } + + public static function getRoles($id) + { + $myRoles = self::get($id); + $roleData = collect(self::roleData()) + ->map(function($role, $k) use($myRoles) { + $role['value'] = $myRoles[$k]; + return $role; + }) + ->toArray(); + return $roleData; + } + + public static function roleData() + { + return [ + 'account-force-private' => [ + 'title' => 'Force Private Account', + 'action' => 'Prevent changing account from private' + ], + 'account-ignore-follow-requests' => [ + 'title' => 'Ignore Follow Requests', + 'action' => 'Hide follow requests and associated notifications' + ], + 'can-view-public-feed' => [ + 'title' => 'Hide Public Feed', + 'action' => 'Hide the public feed timeline' + ], + 'can-view-network-feed' => [ + 'title' => 'Hide Network Feed', + 'action' => 'Hide the network feed timeline' + ], + 'can-view-discover' => [ + 'title' => 'Hide Discover', + 'action' => 'Hide the discover feature' + ], + 'can-post' => [ + 'title' => 'Can post', + 'action' => 'Allows new posts to be shared' + ], + 'can-comment' => [ + 'title' => 'Can comment', + 'action' => 'Allows new comments to be posted' + ], + 'can-like' => [ + 'title' => 'Can Like', + 'action' => 'Allows the ability to like posts and comments' + ], + 'can-share' => [ + 'title' => 'Can Share', + 'action' => 'Allows the ability to share posts and comments' + ], + 'can-follow' => [ + 'title' => 'Can Follow', + 'action' => 'Allows the ability to follow accounts' + ], + 'can-make-public' => [ + 'title' => 'Can make account public', + 'action' => 'Allows the ability to make account public' + ], + ]; + } +} diff --git a/database/migrations/2023_12_27_081801_create_user_roles_table.php b/database/migrations/2023_12_27_081801_create_user_roles_table.php new file mode 100644 index 000000000..4e6af18d0 --- /dev/null +++ b/database/migrations/2023_12_27_081801_create_user_roles_table.php @@ -0,0 +1,30 @@ +id(); + $table->unsignedBigInteger('profile_id')->unique()->index(); + $table->unsignedInteger('user_id')->unique()->index(); + $table->json('roles')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_roles'); + } +}; diff --git a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php new file mode 100644 index 000000000..c60b81deb --- /dev/null +++ b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php @@ -0,0 +1,28 @@ +boolean('has_roles')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('has_roles'); + }); + } +}; From 7b6c9c7428d704552e38d42314272a1b83967a51 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Mon, 1 Jan 2024 16:19:24 -0700 Subject: [PATCH 2/6] Update migrations --- .../migrations/2023_12_27_081801_create_user_roles_table.php | 1 + .../2023_12_27_082024_add_has_roles_to_users_table.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/database/migrations/2023_12_27_081801_create_user_roles_table.php b/database/migrations/2023_12_27_081801_create_user_roles_table.php index 4e6af18d0..59b8ab390 100644 --- a/database/migrations/2023_12_27_081801_create_user_roles_table.php +++ b/database/migrations/2023_12_27_081801_create_user_roles_table.php @@ -16,6 +16,7 @@ return new class extends Migration $table->unsignedBigInteger('profile_id')->unique()->index(); $table->unsignedInteger('user_id')->unique()->index(); $table->json('roles')->nullable(); + $table->json('meta')->nullable(); $table->timestamps(); }); } diff --git a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php index c60b81deb..09246e37b 100644 --- a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php +++ b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php @@ -13,6 +13,8 @@ return new class extends Migration { Schema::table('users', function (Blueprint $table) { $table->boolean('has_roles')->default(false); + $table->unsignedInteger('parent_id')->nullable(); + $table->tinyInteger('role_id')->unsigned()->nullable()->index(); }); } @@ -23,6 +25,8 @@ return new class extends Migration { Schema::table('users', function (Blueprint $table) { $table->dropColumn('has_roles'); + $table->dropColumn('parent_id'); + $table->dropColumn('role_id'); }); } }; From d39946b045be826ef0079d9a8a06c7312f7ec93a Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 2 Jan 2024 22:04:27 -0700 Subject: [PATCH 3/6] Update ApiV1Controller, add permissions check --- app/Http/Controllers/Api/ApiV1Controller.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 6f314e0b3..c1dd8cbf4 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -1245,6 +1245,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); @@ -1306,6 +1307,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); @@ -3175,6 +3177,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); $status = Status::whereScope('public')->findOrFail($id); @@ -3222,6 +3225,7 @@ class ApiV1Controller extends Controller abort_if(!$request->user(), 403); $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); AccountService::setLastActive($user->id); $status = Status::whereScope('public')->findOrFail($id); @@ -3272,6 +3276,13 @@ class ApiV1Controller extends Controller '_pe' => 'sometimes' ]); + $user = $request->user(); + abort_if( + $user->has_roles && !UserRoleService::can('can-view-hashtag-feed', $user->id), + 403, + 'Invalid permissions for this action' + ); + if(config('database.default') === 'pgsql') { $tag = Hashtag::where('name', 'ilike', $hashtag) ->orWhere('slug', 'ilike', $hashtag) From 75b0f2dda043b26046f40417d9c1440734335bcf Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 2 Jan 2024 22:06:18 -0700 Subject: [PATCH 4/6] Update ComposeController, add permissions check --- app/Http/Controllers/ComposeController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Http/Controllers/ComposeController.php b/app/Http/Controllers/ComposeController.php index 9be50f346..e79625861 100644 --- a/app/Http/Controllers/ComposeController.php +++ b/app/Http/Controllers/ComposeController.php @@ -54,6 +54,7 @@ use App\Util\Lexer\Autolink; use App\Util\Lexer\Extractor; use App\Util\Media\License; use Image; +use App\Services\UserRoleService; class ComposeController extends Controller { @@ -92,6 +93,7 @@ class ComposeController extends Controller $user = Auth::user(); $profile = $user->profile; + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); $limitKey = 'compose:rate-limit:media-upload:' . $user->id; $limitTtl = now()->addMinutes(15); @@ -184,6 +186,7 @@ class ComposeController extends Controller ]); $user = Auth::user(); + abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action'); $limitKey = 'compose:rate-limit:media-updates:' . $user->id; $limitTtl = now()->addMinutes(15); From cbe75ce8712f6d703b67738579e22dfa6226e1c2 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 2 Jan 2024 22:06:54 -0700 Subject: [PATCH 5/6] Update UserRolesController --- app/Http/Controllers/UserRolesController.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/UserRolesController.php b/app/Http/Controllers/UserRolesController.php index dbd34d0da..65a71d19d 100644 --- a/app/Http/Controllers/UserRolesController.php +++ b/app/Http/Controllers/UserRolesController.php @@ -3,8 +3,21 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use App\Services\UserRoleService; class UserRolesController extends Controller { - // + public function __construct() + { + $this->middleware('auth'); + } + + public function getRoles(Request $request) + { + $this->validate($request, [ + 'id' => 'required' + ]); + + return UserRoleService::getRoles($request->user()->id); + } } From 0ef6812709fa36ed8ae79da144ba114fcdc02fe6 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Tue, 2 Jan 2024 22:07:42 -0700 Subject: [PATCH 6/6] Update UserRoleService, add useDefaultFallback parameter --- app/Services/UserRoleService.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/Services/UserRoleService.php b/app/Services/UserRoleService.php index fef5356c2..500a4666e 100644 --- a/app/Services/UserRoleService.php +++ b/app/Services/UserRoleService.php @@ -6,12 +6,19 @@ use App\Models\UserRoles; class UserRoleService { - public static function can($action, $id) + public static function can($action, $id, $useDefaultFallback = true) { + $default = self::defaultRoles(); $roles = self::get($id); - - return in_array($action, $roles) ? $roles[$action] : null; - } + return + in_array($action, array_keys($roles)) ? + $roles[$action] : + ( + $useDefaultFallback ? + $default[$action] : + false + ); + } public static function get($id) { @@ -36,6 +43,7 @@ class UserRoleService 'can-view-public-feed' => true, 'can-view-network-feed' => true, 'can-view-discover' => true, + 'can-view-hashtag-feed' => false, 'can-post' => true, 'can-comment' => true,