Expand API with better user group management.

This commit is contained in:
James Cole 2023-09-20 06:17:56 +02:00
parent 549f3c038a
commit 1c41b6753d
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
31 changed files with 853 additions and 146 deletions

View File

@ -32,6 +32,10 @@ use Illuminate\Http\JsonResponse;
class StoreController extends Controller
{
/**
* TODO this method is practically the same as the V1 method and borrows as much code as possible.
* TODO still it duplicates a lot.
* TODO the v1 endpoints will never support separate administrations, this is an important distinction.
*
* @return JsonResponse
*/
public function post(): JsonResponse

View File

@ -2,7 +2,7 @@
/*
* NetWorthController.php
* Copyright (c) 2022 james@firefly-iii.org
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@ -22,8 +22,9 @@
declare(strict_types=1);
namespace FireflyIII\Api\V2\Controllers;
namespace FireflyIII\Api\V2\Controllers\Summary;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Generic\SingleDateRequest;
use FireflyIII\Helpers\Report\NetWorthInterface;
use Illuminate\Http\JsonResponse;

View File

@ -2,7 +2,7 @@
/*
* VersionUpdateController.php
* Copyright (c) 2022 james@firefly-iii.org
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@ -22,7 +22,9 @@
declare(strict_types=1);
namespace FireflyIII\Api\V2\Controllers;
namespace FireflyIII\Api\V2\Controllers\System;
use FireflyIII\Api\V2\Controllers\Controller;
/**
* Class VersionUpdateController

View File

@ -2,7 +2,7 @@
/*
* AccountController.php
* Copyright (c) 2022 james@firefly-iii.org
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*

View File

@ -1,6 +1,4 @@
<?php
declare(strict_types=1);
/*
* TransactionController.php
* Copyright (c) 2023 james@firefly-iii.org
@ -21,6 +19,8 @@ declare(strict_types=1);
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V2\Controllers\Transaction\List;
use FireflyIII\Api\V2\Controllers\Controller;

View File

@ -2,7 +2,7 @@
/*
* BillController.php
* Copyright (c) 2022 james@firefly-iii.org
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*

View File

@ -0,0 +1,32 @@
<?php
/*
* DestroyController.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V2\Controllers\UserGroup;
use FireflyIII\Api\V2\Controllers\Controller;
/**
* Class DestroyController
*/
class DestroyController extends Controller
{
}

View File

@ -0,0 +1,97 @@
<?php
/*
* ShowController.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V2\Controllers\UserGroup;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Models\UserGroup;
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
use FireflyIII\Transformers\V2\UserGroupTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
/**
* Class ShowController
*/
class ShowController extends Controller
{
private UserGroupRepositoryInterface $repository;
/**
*
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(UserGroupRepositoryInterface::class);
return $next($request);
}
);
}
/**
* @param Request $request
*
* @return JsonResponse
*/
public function index(Request $request): JsonResponse
{
$collection = new Collection();
// if the user has the system owner role, get all. Otherwise, get only the users' groups.
if (!auth()->user()->hasRole('owner')) {
$collection = $this->repository->get();
}
if (auth()->user()->hasRole('owner')) {
$collection = $this->repository->getAll();
}
$count = $collection->count();
$userGroups = $collection->slice(($this->parameters->get('page') - 1) * $this->pageSize, $this->pageSize);
$paginator = new LengthAwarePaginator($userGroups, $count, $this->pageSize, $this->parameters->get('page'));
$transformer = new UserGroupTransformer();
$transformer->setParameters($this->parameters); // give params to transformer
return response()
->json($this->jsonApiList('user-groups', $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE);
}
/**
* @param Request $request
* @param UserGroup $userGroup
*
* @return JsonResponse
*/
public function show(Request $request, UserGroup $userGroup): JsonResponse
{
$transformer = new UserGroupTransformer();
$transformer->setParameters($this->parameters);
return response()
->api($this->jsonApiObject('user-groups', $userGroup, $transformer))
->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@ -0,0 +1,67 @@
<?php
/*
* StoreController.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V2\Controllers\UserGroup;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\UserGroup\StoreRequest;
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
use FireflyIII\Transformers\V2\UserGroupTransformer;
use Illuminate\Http\JsonResponse;
/**
* Class StoreController
*/
class StoreController extends Controller
{
private UserGroupRepositoryInterface $repository;
/**
*
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(UserGroupRepositoryInterface::class);
return $next($request);
}
);
}
/**
* @return JsonResponse
*/
public function store(StoreRequest $request): JsonResponse
{
$all = $request->getAll();
$userGroup = $this->repository->store($all);
$transformer = new UserGroupTransformer();
$transformer->setParameters($this->parameters);
return response()
->api($this->jsonApiObject('user-groups', $userGroup, $transformer))
->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
* UpdateController.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V2\Controllers\UserGroup;
use FireflyIII\Api\V2\Controllers\Controller;
class UpdateController extends Controller
{
// basic edit van group
// add user, add rights, remove user, remove rights.
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V2\Request\Autocomplete;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\UserRole;
@ -92,7 +93,7 @@ class AutocompleteRequest extends FormRequest
$validator->after(
function (Validator $validator) {
// validate if the account can access this administration
$this->validateAdministration($validator, [UserRole::CHANGE_TRANSACTIONS]);
$this->validateAdministration($validator, [UserRoleEnum::MANAGE_TRANSACTIONS]);
}
);
}

View File

@ -0,0 +1,52 @@
<?php
/*
* StoreRequest.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Api\V2\Request\UserGroup;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest extends FormRequest
{
use ChecksLogin;
use ConvertsDataTypes;
/**
* @return array
*/
public function getAll(): array
{
return [
'title' => $this->convertString('title'),
];
}
/**
* @return array
*/
public function rules(): array
{
return [
'title' => 'unique:user_groups,title|required|min:2|max:255',
];
}
}

View File

@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands\Integrity;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\GroupMembership;
use FireflyIII\Models\UserGroup;
@ -85,7 +86,7 @@ class CreateGroupMemberships extends Command
$userGroup = UserGroup::create(['title' => $user->email]);
}
$userRole = UserRole::where('title', UserRole::OWNER)->first();
$userRole = UserRole::where('title', UserRoleEnum::OWNER->value)->first();
if (null === $userRole) {
throw new FireflyException('Firefly III could not find a user role. Please make sure all migrations have run.');

View File

@ -29,12 +29,34 @@ namespace FireflyIII\Enums;
*/
enum UserRoleEnum: string
{
case CHANGE_PIGGY_BANKS = 'change_piggies';
case CHANGE_REPETITIONS = 'change_reps';
case CHANGE_RULES = 'change_rules';
case CHANGE_TRANSACTIONS = 'change_tx';
case FULL = 'full';
case OWNER = 'owner';
case READ_ONLY = 'ro';
case VIEW_REPORTS = 'view_reports';
// most basic rights, cannot see other members, can see everything else.
case READ_ONLY = 'ro';
// required to even USE the group properly (in this order)
case MANAGE_TRANSACTIONS = 'mng_trx';
// required to edit, add or change categories/tags/object-groups
case MANAGE_META = 'mng_meta';
// manage other financial objects:
case MANAGE_BUDGETS = 'mng_budgets';
case MANAGE_PIGGY_BANKS = 'mng_piggies';
case MANAGE_REPETITIONS = 'mng_reps';
case MANAGE_SUBSCRIPTIONS = 'mng_subscriptions';
case MANAGE_RULES = 'mng_rules';
case MANAGE_RECURRING = 'mng_recurring';
case MANAGE_WEBHOOKS = 'mng_webhooks';
case MANAGE_CURRENCIES = 'mng_currencies';
// view and generate reports
case VIEW_REPORTS = 'view_reports';
// view memberships. needs FULL to manage them.
case VIEW_MEMBERSHIPS = 'view_memberships';
// everything the creator can, except remove/change original creator and delete group
case FULL = 'full';
// reserved for original creator
case OWNER = 'owner';
}

View File

@ -0,0 +1,62 @@
<?php
/*
* UserGroupFactory.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Factory;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\GroupMembership;
use FireflyIII\Models\UserGroup;
use FireflyIII\Models\UserRole;
/**
* Class UserGroupFactory
*/
class UserGroupFactory
{
/**
* @param array $data
*
* @return UserGroup
* @throws FireflyException
*/
public function create(array $data): UserGroup
{
$userGroup = new UserGroup();
$userGroup->title = $data['title'];
$userGroup->save();
// grab the OWNER role:
$role = UserRole::whereTitle(UserRoleEnum::OWNER->value)->first();
if (null === $role) {
throw new FireflyException('Role "owner" does not exist.');
}
// make user member:
$groupMembership = new GroupMembership();
$groupMembership->user_group_id = $userGroup->id;
$groupMembership->user_id = $data['user']->id;
$groupMembership->user_role_id = $role->id;
$groupMembership->save();
return $userGroup;
}
}

View File

@ -48,18 +48,23 @@ class AutomationHandler
public function reportJournals(RequestedReportOnJournals $event): void
{
Log::debug('In reportJournals.');
$sendReport = config('firefly.send_report_journals');
if (false === $sendReport) {
return;
}
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);
$user = $repository->find($event->userId);
if (null === $user || 0 === $event->groups->count()) {
$sendReport = app('preferences')->getForUser($user, 'notification_transaction_creation', false)->data;
if (false === $sendReport) {
Log::debug('Not sending report, because config says so.');
return;
}
if (null === $user || 0 === $event->groups->count()) {
Log::debug('No transaction groups in event, nothing to email about.');
return;
}
Log::debug('Continue with message!');
// transform groups into array:
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
@ -83,5 +88,6 @@ class AutomationHandler
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
Log::debug('If there is no error above this line, message was sent.');
}
}

View File

@ -26,6 +26,7 @@ namespace FireflyIII\Handlers\Events;
use Carbon\Carbon;
use Database\Seeders\ExchangeRateSeeder;
use Exception;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Events\ActuallyLoggedIn;
use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\DetectedNewIPAddress;
@ -144,7 +145,7 @@ class UserEventHandler
}
}
/** @var UserRole|null $role */
$role = UserRole::where('title', UserRole::OWNER)->first();
$role = UserRole::where('title', UserRoleEnum::OWNER->value)->first();
if (null === $role) {
throw new FireflyException('The user role is unexpectedly empty. Did you run all migrations?');
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Requests;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Account;
use FireflyIII\Models\Location;
use FireflyIII\Models\UserRole;
@ -152,7 +153,7 @@ class AccountFormRequest extends FormRequest
$validator->after(
function (Validator $validator) {
// validate if the account can access this administration
$this->validateAdministration($validator, [UserRole::CHANGE_TRANSACTIONS]);
$this->validateAdministration($validator, [UserRoleEnum::MANAGE_TRANSACTIONS->value]);
}
);
}

View File

@ -460,16 +460,18 @@ class CreateRecurringTransactions implements ShouldQueue
$total = $this->repository->totalTransactions($recurrence, $repetition);
$count = $this->repository->getJournalCount($recurrence) + 1;
$transactions = $recurrence->recurrenceTransactions()->get();
$return = [];
/** @var RecurrenceTransaction $first */
$first = $transactions->first();
$return = [];
/** @var RecurrenceTransaction $transaction */
foreach ($transactions as $index => $transaction) {
$single = [
'type' => strtolower($recurrence->transactionType->type),
'type' => null === $first->transactionType ? strtolower($recurrence->transactionType->type) : strtolower($first->transactionType->type),
'date' => $date,
'user' => $recurrence->user_id,
'currency_id' => (int)$transaction->transaction_currency_id,
'currency_code' => null,
'description' => $transactions->first()->description,
'description' => $first->description,
'amount' => $transaction->amount,
'budget_id' => $this->repository->getBudget($transaction),
'budget_name' => null,

View File

@ -25,23 +25,26 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Eloquent;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class UserGroup
*
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property string|null $deleted_at
* @property string $title
* @property-read Collection|GroupMembership[] $groupMemberships
* @property-read int|null $group_memberships_count
* @property int $id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property string|null $deleted_at
* @property string $title
* @property-read Collection|GroupMembership[] $groupMemberships
* @property-read int|null $group_memberships_count
* @method static Builder|UserGroup newModelQuery()
* @method static Builder|UserGroup newQuery()
* @method static Builder|UserGroup query()
@ -50,24 +53,52 @@ use Illuminate\Support\Carbon;
* @method static Builder|UserGroup whereId($value)
* @method static Builder|UserGroup whereTitle($value)
* @method static Builder|UserGroup whereUpdatedAt($value)
* @property-read Collection<int, Account> $accounts
* @property-read int|null $accounts_count
* @property-read Collection<int, \FireflyIII\Models\AvailableBudget> $availableBudgets
* @property-read int|null $available_budgets_count
* @property-read Collection<int, \FireflyIII\Models\Bill> $bills
* @property-read int|null $bills_count
* @property-read Collection<int, \FireflyIII\Models\Budget> $budgets
* @property-read int|null $budgets_count
* @property-read Collection<int, \FireflyIII\Models\PiggyBank> $piggyBanks
* @property-read int|null $piggy_banks_count
* @property-read Collection<int, \FireflyIII\Models\TransactionJournal> $transactionJournals
* @property-read int|null $transaction_journals_count
* @property-read Collection<int, Account> $accounts
* @property-read int|null $accounts_count
* @property-read Collection<int, AvailableBudget> $availableBudgets
* @property-read int|null $available_budgets_count
* @property-read Collection<int, Bill> $bills
* @property-read int|null $bills_count
* @property-read Collection<int, Budget> $budgets
* @property-read int|null $budgets_count
* @property-read Collection<int, PiggyBank> $piggyBanks
* @property-read int|null $piggy_banks_count
* @property-read Collection<int, TransactionJournal> $transactionJournals
* @property-read int|null $transaction_journals_count
* @mixin Eloquent
*/
class UserGroup extends Model
{
protected $fillable = ['title'];
/**
* Route binder. Converts the key in the URL to the specified object (or throw 404).
*
* @param string $value
*
* @return UserGroup
* @throws NotFoundHttpException
*/
public static function routeBinder(string $value): UserGroup
{
if (auth()->check()) {
$userGroupId = (int)$value;
/** @var User $user */
$user = auth()->user();
/** @var UserGroup $userGroup */
$userGroup = UserGroup::find($userGroupId);
if (null === $userGroup) {
throw new NotFoundHttpException();
}
// need at least ready only to be aware of the user group's existence,
// but owner/full role (in the group) or global owner role may overrule this.
if ($user->hasRoleInGroup($userGroup, UserRoleEnum::READ_ONLY, true, true)) {
return $userGroup;
}
}
throw new NotFoundHttpException();
}
/**
* Link to accounts.
*

View File

@ -53,18 +53,6 @@ use Illuminate\Support\Carbon;
*/
class UserRole extends Model
{
public const CHANGE_PIGGY_BANKS = 'change_piggies';
public const CHANGE_REPETITIONS = 'change_reps';
public const CHANGE_RULES = 'change_rules';
public const CHANGE_TRANSACTIONS = 'change_tx';
public const FULL = 'full';
public const MANAGE_CURRENCIES = 'manage_currencies';
public const MANAGE_WEBHOOKS = 'manage_webhooks';
public const OWNER = 'owner';
public const READ_ONLY = 'ro';
public const VIEW_REPORTS = 'view_reports';
protected $fillable = ['title'];
/**

View File

@ -47,6 +47,8 @@ use FireflyIII\Repositories\TransactionType\TransactionTypeRepository;
use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface;
use FireflyIII\Repositories\User\UserRepository;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Repositories\UserGroup\UserGroupRepository;
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
use FireflyIII\Repositories\Webhook\WebhookRepository;
use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface;
use FireflyIII\Services\FireflyIIIOrg\Update\UpdateRequest;
@ -212,6 +214,19 @@ class FireflyServiceProvider extends ServiceProvider
}
);
$this->app->bind(
UserGroupRepositoryInterface::class,
static function (Application $app) {
/** @var UserGroupRepository $repository */
$repository = app(UserGroupRepository::class);
if ($app->auth->check()) { // @phpstan-ignore-line (phpstan does not understand the reference to auth)
$repository->setUser(auth()->user());
}
return $repository;
}
);
// more generators:
$this->app->bind(PopupReportInterface::class, PopupReport::class);
$this->app->bind(ReportHelperInterface::class, ReportHelper::class);

View File

@ -0,0 +1,91 @@
<?php
/*
* UserGroupRepository.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Repositories\UserGroup;
use FireflyIII\Factory\UserGroupFactory;
use FireflyIII\Models\GroupMembership;
use FireflyIII\Models\UserGroup;
use FireflyIII\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Collection;
/**
* Class UserGroupRepository
*/
class UserGroupRepository implements UserGroupRepositoryInterface
{
private User $user;
/**
* Returns all groups the user is member in.
*
* @inheritDoc
*/
public function get(): Collection
{
$collection = new Collection();
$memberships = $this->user->groupMemberships()->get();
/** @var GroupMembership $membership */
foreach ($memberships as $membership) {
/** @var UserGroup $group */
$group = $membership->userGroup()->first();
if (null !== $group) {
$collection->push($group);
}
}
return $collection;
}
/**
* Returns all groups.
*
* @inheritDoc
*/
public function getAll(): Collection
{
return UserGroup::all();
}
/**
* @inheritDoc
*/
public function setUser(Authenticatable | User | null $user): void
{
app('log')->debug(sprintf('Now in %s', __METHOD__));
if (null !== $user) {
$this->user = $user;
}
}
/**
* @param array $data
*
* @return UserGroup
*/
public function store(array $data): UserGroup
{
$data['user'] = $this->user;
/** @var UserGroupFactory $factory */
$factory = app(UserGroupFactory::class);
return $factory->create($data);
}
}

View File

@ -0,0 +1,49 @@
<?php
/*
* UserGroupRepositoryInterface.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Repositories\UserGroup;
use FireflyIII\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Collection;
/**
* Interface UserGroupRepositoryInterface
*/
interface UserGroupRepositoryInterface
{
/**
* @return Collection
*/
public function get(): Collection;
/**
* @return Collection
*/
public function getAll(): Collection;
/**
* @param User|Authenticatable|null $user
*
* @return void
*/
public function setUser(User | Authenticatable | null $user): void;
}

View File

@ -0,0 +1,93 @@
<?php
/*
* UserGroupTransformer.php
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Transformers\V2;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\GroupMembership;
use FireflyIII\Models\UserGroup;
use FireflyIII\User;
use Illuminate\Support\Collection;
/**
* Class UserGroupTransformer
*/
class UserGroupTransformer extends AbstractTransformer
{
private array $memberships;
/**
*
*/
public function __construct()
{
$this->memberships = [];
}
/**
* @inheritDoc
*/
public function collectMetaData(Collection $objects): void
{
if (auth()->check()) {
// collect memberships so they can be listed in the group.
/** @var User $user */
$user = auth()->user();
/** @var UserGroup $userGroup */
foreach ($objects as $userGroup) {
$userGroupId = (int)$userGroup->id;
$access = $user->hasRoleInGroup($userGroup, UserRoleEnum::VIEW_MEMBERSHIPS, true, true);
if ($access) {
$groupMemberships = $userGroup->groupMemberships()->get();
/** @var GroupMembership $groupMembership */
foreach ($groupMemberships as $groupMembership) {
$this->memberships[$userGroupId][] = [
'user_id' => (string)$groupMembership->user_id,
'user_email' => $groupMembership->user->email,
'role' => $groupMembership->userRole->title,
];
}
}
}
}
}
/**
* Transform the user group.
*
* @param UserGroup $userGroup
*
* @return array
*/
public function transform(UserGroup $userGroup): array
{
$return = [
'id' => (int)$userGroup->id,
'created_at' => $userGroup->created_at->toAtomString(),
'updated_at' => $userGroup->updated_at->toAtomString(),
'title' => $userGroup->title,
'members' => $this->memberships[(int)$userGroup->id] ?? [],
];
// if the user has a specific role in this group, then collect the memberships.
return $return;
}
}

View File

@ -26,6 +26,7 @@ namespace FireflyIII;
use Eloquent;
use Exception;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Events\RequestedNewPassword;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
@ -48,6 +49,7 @@ use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\UserGroup;
use FireflyIII\Models\UserRole;
use FireflyIII\Models\Webhook;
use FireflyIII\Notifications\Admin\TestNotification;
use FireflyIII\Notifications\Admin\UserInvitation;
@ -365,6 +367,91 @@ class User extends Authenticatable
return 'objectguid';
}
/**
* Does the user have role X in group Y?
*
* If $allowOverride is set to true, then the roles FULL or OWNER will also be checked,
* which means that in most cases the user DOES have access, regardless of the original role submitted in $role.
*
* @param UserGroup $userGroup
* @param UserRoleEnum $role
* @param bool $allowOverride
*
* @return bool
*/
public function hasRoleInGroup(UserGroup $userGroup, UserRoleEnum $role, bool $allowGroupOverride = false, bool $allowSystemOverride = false): bool
{
if ($allowSystemOverride && $this->hasRole('owner')) {
app('log')->debug(sprintf('hasRoleInGroup: user "#%d %s" is system owner and allowSystemOverride = true, return true', $this->id, $this->email));
return true;
}
$roles = [$role->value];
if ($allowGroupOverride) {
$roles[] = UserRoleEnum::OWNER->value;
$roles[] = UserRoleEnum::FULL->value;
}
app('log')->debug(sprintf('in hasRoleInGroup(%s)', join(', ', $roles)));
/** @var Collection $dbRoles */
$dbRoles = UserRole::whereIn('title', $roles)->get();
if (0 === $dbRoles->count()) {
app('log')->error(sprintf('Could not find role(s): %s. Probably migration mishap.', join(', ', $roles)));
return false;
}
$dbRolesIds = $dbRoles->pluck('id')->toArray();
$dbRolesTitles = $dbRoles->pluck('title')->toArray();
/** @var Collection $groupMemberships */
$groupMemberships = $this->groupMemberships()
->whereIn('user_role_id', $dbRolesIds)
->where('user_group_id', $userGroup->id)->get();
if (0 === $groupMemberships->count()) {
app('log')->error(sprintf('User #%d "%s" does not have roles %s in user group #%d "%s"',
$this->id, $this->email,
join(', ', $roles), $userGroup->id, $userGroup->title));
return false;
}
foreach ($groupMemberships as $membership) {
app('log')->debug(sprintf('User #%d "%s" has role "%s" in user group #%d "%s"',
$this->id, $this->email,
$membership->userRole->title, $userGroup->id, $userGroup->title));
if (in_array($membership->userRole->title, $dbRolesTitles)) {
app('log')->debug(sprintf('Return true, found role "%s"', $membership->userRole->title));
return true;
}
}
app('log')->error(sprintf('User #%d "%s" does not have roles %s in user group #%d "%s"',
$this->id, $this->email,
join(', ', $roles), $userGroup->id, $userGroup->title));
return false;
// // not necessary, should always return true:
// $result = $groupMembership->userRole->title === $role->value;
// app('log')->error(sprintf('Does user #%d "%s" have role "%s" in user group #%d "%s"? %s',
// $this->id, $this->email,
// $role->value, $userGroup->id, $userGroup->title, var_export($result, true)));
// return $result;
}
/**
* @param string $role
*
* @return bool
*/
public function hasRole(string $role): bool
{
return $this->roles()->where('name', $role)->count() === 1;
}
/**
* Link to roles.
*
* @return BelongsToMany
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
/**
*
* @return HasMany
@ -445,26 +532,6 @@ class User extends Authenticatable
};
}
/**
* @param string $role
*
* @return bool
*/
public function hasRole(string $role): bool
{
return $this->roles()->where('name', $role)->count() === 1;
}
/**
* Link to roles.
*
* @return BelongsToMany
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
/**
* Route notifications for the Slack channel.
*
@ -513,6 +580,8 @@ class User extends Authenticatable
return $this->hasMany(Rule::class);
}
// start LDAP related code
/**
* Send the password reset notification.
*
@ -525,8 +594,6 @@ class User extends Authenticatable
event(new RequestedNewPassword($this, $token, $ipAddress));
}
// start LDAP related code
/**
* Set the models LDAP domain.
*

View File

@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Validation\Administration;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\UserRole;
use FireflyIII\Repositories\User\UserRepositoryInterface;
@ -48,6 +49,7 @@ trait ValidatesAdministrationAccess
*/
protected function validateAdministration(Validator $validator, array $allowedRoles): void
{
die('deprecated method, must be done through user.');
Log::debug('Now in validateAdministration()');
if (!auth()->check()) {
Log::error('User is not authenticated.');
@ -74,11 +76,11 @@ trait ValidatesAdministrationAccess
$validator->errors()->add('administration', (string)trans('validation.no_access_user_group'));
return;
}
if (in_array(UserRole::OWNER, $array, true)) {
if (in_array(UserRoleEnum::OWNER->value, $array, true)) {
Log::debug('User is owner of this administration.');
return;
}
if (in_array(UserRole::FULL, $array, true)) {
if (in_array(UserRoleEnum::OWNER->value, $array, true)) {
Log::debug('User has full access to this administration.');
return;
}

74
composer.lock generated
View File

@ -4384,16 +4384,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.21",
"version": "3.0.23",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1"
"reference": "866cc78fbd82462ffd880e3f65692afe928bed50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/4580645d3fc05c189024eb3b834c6c1e4f0f30a1",
"reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/866cc78fbd82462ffd880e3f65692afe928bed50",
"reference": "866cc78fbd82462ffd880e3f65692afe928bed50",
"shasum": ""
},
"require": {
@ -4474,7 +4474,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.21"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.23"
},
"funding": [
{
@ -4490,7 +4490,7 @@
"type": "tidelift"
}
],
"time": "2023-07-09T15:24:48+00:00"
"time": "2023-09-18T17:22:01+00:00"
},
{
"name": "pragmarx/google2fa",
@ -9919,16 +9919,16 @@
},
{
"name": "phpmyadmin/sql-parser",
"version": "5.8.0",
"version": "5.8.1",
"source": {
"type": "git",
"url": "https://github.com/phpmyadmin/sql-parser.git",
"reference": "db1b3069b5dbc220d393d67ff911e0ae76732755"
"reference": "b877ee6262a00f0f498da5e01335e8a5dc01d203"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/db1b3069b5dbc220d393d67ff911e0ae76732755",
"reference": "db1b3069b5dbc220d393d67ff911e0ae76732755",
"url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/b877ee6262a00f0f498da5e01335e8a5dc01d203",
"reference": "b877ee6262a00f0f498da5e01335e8a5dc01d203",
"shasum": ""
},
"require": {
@ -9950,7 +9950,7 @@
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"psalm/plugin-phpunit": "^0.16.1",
"vimeo/psalm": "^4.11",
"zumba/json-serializer": "^3.0"
"zumba/json-serializer": "~3.0.2"
},
"suggest": {
"ext-mbstring": "For best performance",
@ -10002,20 +10002,20 @@
"type": "other"
}
],
"time": "2023-06-05T18:19:38+00:00"
"time": "2023-09-15T18:21:22+00:00"
},
{
"name": "phpstan/phpdoc-parser",
"version": "1.24.0",
"version": "1.24.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "3510b0a6274cc42f7219367cb3abfc123ffa09d6"
"reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/3510b0a6274cc42f7219367cb3abfc123ffa09d6",
"reference": "3510b0a6274cc42f7219367cb3abfc123ffa09d6",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01",
"reference": "9f854d275c2dbf84915a5c0ec9a2d17d2cd86b01",
"shasum": ""
},
"require": {
@ -10047,9 +10047,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.0"
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.1"
},
"time": "2023-09-07T20:46:32+00:00"
"time": "2023-09-18T12:18:02+00:00"
},
{
"name": "phpstan/phpstan",
@ -10212,16 +10212,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "10.1.5",
"version": "10.1.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "1df504e42a88044c27a90136910f0b3fe9e91939"
"reference": "56f33548fe522c8d82da7ff3824b42829d324364"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1df504e42a88044c27a90136910f0b3fe9e91939",
"reference": "1df504e42a88044c27a90136910f0b3fe9e91939",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/56f33548fe522c8d82da7ff3824b42829d324364",
"reference": "56f33548fe522c8d82da7ff3824b42829d324364",
"shasum": ""
},
"require": {
@ -10278,7 +10278,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.5"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.6"
},
"funding": [
{
@ -10286,7 +10286,7 @@
"type": "github"
}
],
"time": "2023-09-12T14:37:22+00:00"
"time": "2023-09-19T04:59:03+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -10533,16 +10533,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.3.4",
"version": "10.3.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "b8d59476f19115c9774b3b447f78131781c6c32b"
"reference": "747c3b2038f1139e3dcd9886a3f5a948648b7503"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b8d59476f19115c9774b3b447f78131781c6c32b",
"reference": "b8d59476f19115c9774b3b447f78131781c6c32b",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/747c3b2038f1139e3dcd9886a3f5a948648b7503",
"reference": "747c3b2038f1139e3dcd9886a3f5a948648b7503",
"shasum": ""
},
"require": {
@ -10566,7 +10566,7 @@
"sebastian/comparator": "^5.0",
"sebastian/diff": "^5.0",
"sebastian/environment": "^6.0",
"sebastian/exporter": "^5.0",
"sebastian/exporter": "^5.1",
"sebastian/global-state": "^6.0.1",
"sebastian/object-enumerator": "^5.0",
"sebastian/recursion-context": "^5.0",
@ -10614,7 +10614,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.3.4"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.3.5"
},
"funding": [
{
@ -10630,7 +10630,7 @@
"type": "tidelift"
}
],
"time": "2023-09-12T14:42:28+00:00"
"time": "2023-09-19T05:42:37+00:00"
},
{
"name": "sebastian/cli-parser",
@ -11067,16 +11067,16 @@
},
{
"name": "sebastian/exporter",
"version": "5.0.1",
"version": "5.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "32ff03d078fed1279c4ec9a407d08c5e9febb480"
"reference": "c3fa8483f9539b190f7cd4bfc4a07631dd1df344"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/32ff03d078fed1279c4ec9a407d08c5e9febb480",
"reference": "32ff03d078fed1279c4ec9a407d08c5e9febb480",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c3fa8483f9539b190f7cd4bfc4a07631dd1df344",
"reference": "c3fa8483f9539b190f7cd4bfc4a07631dd1df344",
"shasum": ""
},
"require": {
@ -11133,7 +11133,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy",
"source": "https://github.com/sebastianbergmann/exporter/tree/5.0.1"
"source": "https://github.com/sebastianbergmann/exporter/tree/5.1.0"
},
"funding": [
{
@ -11141,7 +11141,7 @@
"type": "github"
}
],
"time": "2023-09-08T04:46:58+00:00"
"time": "2023-09-18T07:15:37+00:00"
},
{
"name": "sebastian/global-state",

View File

@ -46,6 +46,7 @@ use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalLink;
use FireflyIII\Models\TransactionType as TransactionTypeModel;
use FireflyIII\Models\UserGroup;
use FireflyIII\Models\Webhook;
use FireflyIII\Models\WebhookAttempt;
use FireflyIII\Models\WebhookMessage;
@ -482,6 +483,7 @@ return [
// V2 API endpoints:
'userGroupAccount' => UserGroupAccount::class,
'userGroupBill' => UserGroupBill::class,
'userGroup' => UserGroup::class,
],

View File

@ -23,18 +23,12 @@
declare(strict_types=1);
use FireflyIII\Models\UserRole;
use FireflyIII\Enums\UserRoleEnum;
return [
$result = [];
'roles' => [
UserRole::READ_ONLY => [],
UserRole::CHANGE_TRANSACTIONS => [],
UserRole::CHANGE_RULES => [],
UserRole::CHANGE_PIGGY_BANKS => [],
UserRole::CHANGE_REPETITIONS => [],
UserRole::VIEW_REPORTS => [],
UserRole::FULL => [],
UserRole::OWNER => [],
],
];
foreach (UserRoleEnum::cases() as $role) {
$result[$role->value] = [];
}
return $result;

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace Database\Seeders;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\UserRole;
use Illuminate\Database\Seeder;
use PDOException;
@ -40,18 +41,10 @@ class UserRoleSeeder extends Seeder
*/
public function run()
{
$roles = [
UserRole::READ_ONLY,
UserRole::CHANGE_TRANSACTIONS,
UserRole::CHANGE_RULES,
UserRole::CHANGE_PIGGY_BANKS,
UserRole::CHANGE_REPETITIONS,
UserRole::VIEW_REPORTS,
UserRole::MANAGE_WEBHOOKS,
UserRole::MANAGE_CURRENCIES,
UserRole::FULL,
UserRole::OWNER,
];
$roles = [];
foreach (UserRoleEnum::cases() as $role) {
$roles[] = $role->value;
}
/** @var string $role */
foreach ($roles as $role) {