Expand API administration validation

This commit is contained in:
James Cole
2024-04-07 06:06:40 +02:00
parent 2c4f2082fe
commit 74291b3870
12 changed files with 380 additions and 291 deletions

View File

@@ -27,6 +27,7 @@ namespace FireflyIII\Api\V2\Controllers\Chart;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Chart\DashboardChartRequest; use FireflyIII\Api\V2\Request\Chart\DashboardChartRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
@@ -46,6 +47,7 @@ class AccountController extends Controller
use ValidatesUserGroupTrait; use ValidatesUserGroupTrait;
private AccountRepositoryInterface $repository; private AccountRepositoryInterface $repository;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
public function __construct() public function __construct()
{ {
@@ -54,9 +56,7 @@ class AccountController extends Controller
function ($request, $next) { function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class); $this->repository = app(AccountRepositoryInterface::class);
$userGroup = $this->validateUserGroup($request); $userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) {
$this->repository->setUserGroup($userGroup); $this->repository->setUserGroup($userGroup);
}
return $next($request); return $next($request);
} }

View File

@@ -27,6 +27,7 @@ namespace FireflyIII\Api\V2\Controllers;
use Carbon\Carbon; use Carbon\Carbon;
use Carbon\Exceptions\InvalidDateException; use Carbon\Exceptions\InvalidDateException;
use Carbon\Exceptions\InvalidFormatException; use Carbon\Exceptions\InvalidFormatException;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Transformers\V2\AbstractTransformer; use FireflyIII\Transformers\V2\AbstractTransformer;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@@ -55,6 +56,7 @@ class Controller extends BaseController
protected const string CONTENT_TYPE = 'application/vnd.api+json'; protected const string CONTENT_TYPE = 'application/vnd.api+json';
protected ParameterBag $parameters; protected ParameterBag $parameters;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
public function __construct() public function __construct()
{ {

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Api\V2\Controllers\Model\Account;
use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\Account\IndexRequest; use FireflyIII\Api\V2\Request\Model\Account\IndexRequest;
use FireflyIII\Api\V2\Request\Model\Transaction\InfiniteListRequest; use FireflyIII\Api\V2\Request\Model\Transaction\InfiniteListRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface; use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Transformers\V2\AccountTransformer; use FireflyIII\Transformers\V2\AccountTransformer;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -36,6 +37,7 @@ class IndexController extends Controller
public const string RESOURCE_KEY = 'accounts'; public const string RESOURCE_KEY = 'accounts';
private AccountRepositoryInterface $repository; private AccountRepositoryInterface $repository;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS];
/** /**
* AccountController constructor. * AccountController constructor.
@@ -48,9 +50,7 @@ class IndexController extends Controller
$this->repository = app(AccountRepositoryInterface::class); $this->repository = app(AccountRepositoryInterface::class);
// new way of user group validation // new way of user group validation
$userGroup = $this->validateUserGroup($request); $userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) {
$this->repository->setUserGroup($userGroup); $this->repository->setUserGroup($userGroup);
}
return $next($request); return $next($request);
} }
@@ -77,8 +77,7 @@ class IndexController extends Controller
return response() return response()
->json($this->jsonApiList('accounts', $paginator, $transformer)) ->json($this->jsonApiList('accounts', $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE) ->header('Content-Type', self::CONTENT_TYPE);
;
} }
public function infiniteList(InfiniteListRequest $request): JsonResponse public function infiniteList(InfiniteListRequest $request): JsonResponse
@@ -98,7 +97,6 @@ class IndexController extends Controller
return response() return response()
->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer)) ->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE) ->header('Content-Type', self::CONTENT_TYPE);
;
} }
} }

View File

@@ -25,7 +25,9 @@ declare(strict_types=1);
namespace FireflyIII\Api\V2\Controllers\Model\Account; namespace FireflyIII\Api\V2\Controllers\Model\Account;
use FireflyIII\Api\V2\Controllers\Controller; use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Transformers\V2\AccountTransformer; use FireflyIII\Transformers\V2\AccountTransformer;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -36,6 +38,28 @@ use Illuminate\Http\JsonResponse;
*/ */
class ShowController extends Controller class ShowController extends Controller
{ {
public const string RESOURCE_KEY = 'accounts';
private AccountRepositoryInterface $repository;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS];
/**
* AccountController constructor.
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class);
// new way of user group validation
$userGroup = $this->validateUserGroup($request);
$this->repository->setUserGroup($userGroup);
return $next($request);
}
);
}
/** /**
* TODO this endpoint is not yet reachable. * TODO this endpoint is not yet reachable.
*/ */

View File

@@ -77,13 +77,11 @@ class BasicController extends Controller
$this->opsRepository = app(OperationsRepositoryInterface::class); $this->opsRepository = app(OperationsRepositoryInterface::class);
$userGroup = $this->validateUserGroup($request); $userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) {
$this->abRepository->setUserGroup($userGroup); $this->abRepository->setUserGroup($userGroup);
$this->accountRepository->setUserGroup($userGroup); $this->accountRepository->setUserGroup($userGroup);
$this->billRepository->setUserGroup($userGroup); $this->billRepository->setUserGroup($userGroup);
$this->budgetRepository->setUserGroup($userGroup); $this->budgetRepository->setUserGroup($userGroup);
$this->opsRepository->setUserGroup($userGroup); $this->opsRepository->setUserGroup($userGroup);
}
return $next($request); return $next($request);
} }
@@ -137,8 +135,7 @@ class BasicController extends Controller
->setPage($this->parameters->get('page')) ->setPage($this->parameters->get('page'))
// set types of transactions to return. // set types of transactions to return.
->setTypes([TransactionType::DEPOSIT]) ->setTypes([TransactionType::DEPOSIT])
->setRange($start, $end) ->setRange($start, $end);
;
$set = $collector->getExtractedJournals(); $set = $collector->getExtractedJournals();
$object->groupTransactions('income', $set); $object->groupTransactions('income', $set);
@@ -153,8 +150,7 @@ class BasicController extends Controller
->setPage($this->parameters->get('page')) ->setPage($this->parameters->get('page'))
// set types of transactions to return. // set types of transactions to return.
->setTypes([TransactionType::WITHDRAWAL]) ->setTypes([TransactionType::WITHDRAWAL])
->setRange($start, $end) ->setRange($start, $end);
;
$set = $collector->getExtractedJournals(); $set = $collector->getExtractedJournals();
$object->groupTransactions('expense', $set); $object->groupTransactions('expense', $set);

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V2\Request\Chart; namespace FireflyIII\Api\V2\Request\Chart;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
@@ -39,6 +40,8 @@ class DashboardChartRequest extends FormRequest
use ConvertsDataTypes; use ConvertsDataTypes;
use ValidatesUserGroupTrait; use ValidatesUserGroupTrait;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
/** /**
* Get all data from the request. * Get all data from the request.
*/ */

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Exceptions; namespace FireflyIII\Exceptions;
use FireflyIII\Jobs\MailError; use FireflyIII\Jobs\MailError;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
@@ -98,6 +99,13 @@ class Handler extends ExceptionHandler
return response()->json(['message' => 'Resource not found', 'exception' => 'NotFoundHttpException'], 404); return response()->json(['message' => 'Resource not found', 'exception' => 'NotFoundHttpException'], 404);
} }
if ($e instanceof AuthorizationException && $expectsJson) {
// somehow Laravel handler does not catch this:
app('log')->debug('Return JSON unauthorized error.');
return response()->json(['message' => $e->getMessage(), 'exception' => 'AuthorizationException'], 401);
}
if ($e instanceof AuthenticationException && $expectsJson) { if ($e instanceof AuthenticationException && $expectsJson) {
// somehow Laravel handler does not catch this: // somehow Laravel handler does not catch this:
app('log')->debug('Return JSON unauthenticated error.'); app('log')->debug('Return JSON unauthenticated error.');

View File

@@ -32,6 +32,7 @@ use FireflyIII\Http\Middleware\Installer;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface as UserGroupAccountRepositoryInterface;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -127,6 +128,20 @@ class HomeController extends Controller
if (0 === $count) { if (0 === $count) {
return redirect(route('new-user.index')); return redirect(route('new-user.index'));
} }
if ('v1' === (string)config('view.layout')) {
return $this->indexV1($repository);
}
if ('v2' === (string)config('view.layout')) {
return $this->indexV2();
}
throw new FireflyException('Invalid layout configuration');
}
private function indexV1(AccountRepositoryInterface $repository): mixed
{
$types = config('firefly.accountTypesByIdentifier.asset');
$count = $repository->count($types);
$subTitle = (string)trans('firefly.welcome_back'); $subTitle = (string)trans('firefly.welcome_back');
$transactions = []; $transactions = [];
$frontpage = app('preferences')->getFresh('frontpageAccounts', $repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray()); $frontpage = app('preferences')->getFresh('frontpageAccounts', $repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray());
@@ -136,15 +151,12 @@ class HomeController extends Controller
} }
/** @var Carbon $start */ /** @var Carbon $start */
$start = session('start', today(config('app.timezone'))->startOfMonth());
/** @var Carbon $end */ /** @var Carbon $end */
$start = session('start', today(config('app.timezone'))->startOfMonth());
$end = session('end', today(config('app.timezone'))->endOfMonth()); $end = session('end', today(config('app.timezone'))->endOfMonth());
$accounts = $repository->getAccountsById($frontpageArray); $accounts = $repository->getAccountsById($frontpageArray);
$today = today(config('app.timezone')); $today = today(config('app.timezone'));
$accounts = $accounts->sortBy('order'); // sort frontpage accounts by order
// sort frontpage accounts by order
$accounts = $accounts->sortBy('order');
app('log')->debug('Frontpage accounts are ', $frontpageArray); app('log')->debug('Frontpage accounts are ', $frontpageArray);
@@ -166,4 +178,18 @@ class HomeController extends Controller
return view('index', compact('count', 'subTitle', 'transactions', 'billCount', 'start', 'end', 'today')); return view('index', compact('count', 'subTitle', 'transactions', 'billCount', 'start', 'end', 'today'));
} }
private function indexV2(): mixed
{
$subTitle = (string)trans('firefly.welcome_back');
$start = session('start', today(config('app.timezone'))->startOfMonth());
$end = session('end', today(config('app.timezone'))->endOfMonth());
/** @var User $user */
$user = auth()->user();
event(new RequestedVersionCheckStatus($user));
return view('index', compact( 'subTitle','start','end'));
}
} }

View File

@@ -23,11 +23,14 @@ declare(strict_types=1);
namespace FireflyIII\Support\Http\Api; namespace FireflyIII\Support\Http\Api;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\GroupMembership; use FireflyIII\Models\GroupMembership;
use FireflyIII\Models\UserGroup; use FireflyIII\Models\UserGroup;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Collection;
/** /**
* Trait ValidatesUserGroupTrait * Trait ValidatesUserGroupTrait
@@ -35,37 +38,63 @@ use Illuminate\Http\Request;
trait ValidatesUserGroupTrait trait ValidatesUserGroupTrait
{ {
/** /**
* This check does not validate which rights the user has, that comes later. * @throws AuthorizationException
* * @throws AuthenticationException
* @throws FireflyException
*/ */
protected function validateUserGroup(Request $request): ?UserGroup protected function validateUserGroup(Request $request): UserGroup
{ {
app('log')->debug(sprintf('validateUserGroup: %s', get_class($this)));
if (!auth()->check()) { if (!auth()->check()) {
app('log')->debug('validateUserGroup: user is not logged in, return NULL.'); app('log')->debug('validateUserGroup: user is not logged in, return NULL.');
return null; throw new AuthenticationException();
} }
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
$groupId = 0;
if (!$request->has('user_group_id')) { if (!$request->has('user_group_id')) {
$group = $user->userGroup; $groupId = $user->user_group_id;
app('log')->debug(sprintf('validateUserGroup: no user group submitted, return default group #%d.', $group?->id)); app('log')->debug(sprintf('validateUserGroup: no user group submitted, use default group #%d.', $groupId));
}
if ($request->has('user_group_id')) {
$groupId = (int)$request->get('user_group_id');
app('log')->debug(sprintf('validateUserGroup: user group submitted, search for memberships in group #%d.', $groupId));
}
/** @var GroupMembership|null $membership */
$membership = $user->groupMemberships()->where('user_group_id', $groupId)->first();
if (null === $membership) {
app('log')->debug(sprintf('validateUserGroup: user has no access to group #%d.', $groupId));
throw new AuthorizationException((string)trans('validation.no_access_group'));
}
// need to get the group from the membership:
/** @var UserGroup|null $group */
$group = $membership->userGroup;
if (null === $group) {
app('log')->debug(sprintf('validateUserGroup: group #%d does not exist.', $groupId));
throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group'));
}
app('log')->debug(sprintf('validateUserGroup: validate access of user to group #%d ("%s").', $groupId, $group->title));
$roles = property_exists($this, 'acceptedRoles') ? $this->acceptedRoles : [];
if(0 === count($roles)) {
app('log')->debug('validateUserGroup: no roles defined, so no access.');
throw new AuthorizationException((string)trans('validation.no_accepted_roles_defined'));
}
app('log')->debug(sprintf('validateUserGroup: have %d roles to check.', count($roles)), $roles);
/** @var UserRoleEnum $role */
foreach($roles as $role) {
if($user->hasRoleInGroupOrOwner($group, $role)) {
app('log')->debug(sprintf('validateUserGroup: User has role "%s" in group #%d, return the group.', $role->value, $groupId));
return $group; return $group;
} }
$groupId = (int)$request->get('user_group_id'); app('log')->debug(sprintf('validateUserGroup: User does NOT have role "%s" in group #%d, continue searching.', $role->value, $groupId));
/** @var null|GroupMembership $membership */
$membership = $user->groupMemberships()->where('user_group_id', $groupId)->first();
if (null === $membership) {
app('log')->debug('validateUserGroup: user has no access to this group.');
throw new FireflyException((string)trans('validation.belongs_user_or_user_group'));
} }
app('log')->debug(sprintf('validateUserGroup: user has role "%s" in group #%d.', $membership->userRole->title, $membership->userGroup->id));
return $membership->userGroup; app('log')->debug('validateUserGroup: User does NOT have enough rights to access endpoint.');
throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group'));
} }
} }

View File

@@ -39,7 +39,7 @@ return [
| the usual Laravel view path has already been registered for you. | the usual Laravel view path has already been registered for you.
| |
*/ */
'layout' => env('FIREFLY_III_LAYOUT', 'v1'),
'paths' => $paths, 'paths' => $paths,
/* /*

View File

@@ -59,6 +59,8 @@ return [
'invalid_selection' => 'Your selection is invalid.', 'invalid_selection' => 'Your selection is invalid.',
'belongs_user' => 'This value is linked to an object that does not seem to exist.', 'belongs_user' => 'This value is linked to an object that does not seem to exist.',
'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.', 'belongs_user_or_user_group' => 'This value is linked to an object that does not seem to exist in your current financial administration.',
'no_access_group' => 'The user has no access to this user group.',
'no_accepted_roles_defined' => 'No access roles have been defined for this endpoint, access denied.',
'at_least_one_transaction' => 'Need at least one transaction.', 'at_least_one_transaction' => 'Need at least one transaction.',
'recurring_transaction_id' => 'Need at least one transaction.', 'recurring_transaction_id' => 'Need at least one transaction.',
'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.', 'need_id_to_match' => 'You need to submit this entry with an ID for the API to be able to match it.',

View File

@@ -0,0 +1 @@
This feature is only available in the v2 layout.