Expand API administration validation

This commit is contained in:
James Cole 2024-04-07 06:06:40 +02:00
parent 2c4f2082fe
commit 74291b3870
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
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 FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Chart\DashboardChartRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
@ -46,6 +47,7 @@ class AccountController extends Controller
use ValidatesUserGroupTrait;
private AccountRepositoryInterface $repository;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
public function __construct()
{
@ -54,9 +56,7 @@ class AccountController extends Controller
function ($request, $next) {
$this->repository = app(AccountRepositoryInterface::class);
$userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) {
$this->repository->setUserGroup($userGroup);
}
return $next($request);
}

View File

@ -27,6 +27,7 @@ namespace FireflyIII\Api\V2\Controllers;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidDateException;
use Carbon\Exceptions\InvalidFormatException;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait;
use FireflyIII\Transformers\V2\AbstractTransformer;
use Illuminate\Database\Eloquent\Model;
@ -55,6 +56,7 @@ class Controller extends BaseController
protected const string CONTENT_TYPE = 'application/vnd.api+json';
protected ParameterBag $parameters;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY];
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\Request\Model\Account\IndexRequest;
use FireflyIII\Api\V2\Request\Model\Transaction\InfiniteListRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Transformers\V2\AccountTransformer;
use Illuminate\Http\JsonResponse;
@ -36,6 +37,7 @@ class IndexController extends Controller
public const string RESOURCE_KEY = 'accounts';
private AccountRepositoryInterface $repository;
protected array $acceptedRoles = [UserRoleEnum::READ_ONLY, UserRoleEnum::MANAGE_TRANSACTIONS];
/**
* AccountController constructor.
@ -48,9 +50,7 @@ class IndexController extends Controller
$this->repository = app(AccountRepositoryInterface::class);
// new way of user group validation
$userGroup = $this->validateUserGroup($request);
if (null !== $userGroup) {
$this->repository->setUserGroup($userGroup);
}
return $next($request);
}
@ -77,8 +77,7 @@ class IndexController extends Controller
return response()
->json($this->jsonApiList('accounts', $paginator, $transformer))
->header('Content-Type', self::CONTENT_TYPE)
;
->header('Content-Type', self::CONTENT_TYPE);
}
public function infiniteList(InfiniteListRequest $request): JsonResponse
@ -98,7 +97,6 @@ class IndexController extends Controller
return response()
->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;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface;
use FireflyIII\Transformers\V2\AccountTransformer;
use Illuminate\Http\JsonResponse;
@ -36,6 +38,28 @@ use Illuminate\Http\JsonResponse;
*/
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.
*/

View File

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

View File

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

View File

@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Exceptions;
use FireflyIII\Jobs\MailError;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\QueryException;
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);
}
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) {
// somehow Laravel handler does not catch this:
app('log')->debug('Return JSON unauthenticated error.');

View File

@ -32,6 +32,7 @@ use FireflyIII\Http\Middleware\Installer;
use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\UserGroups\Account\AccountRepositoryInterface as UserGroupAccountRepositoryInterface;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -127,6 +128,20 @@ class HomeController extends Controller
if (0 === $count) {
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');
$transactions = [];
$frontpage = app('preferences')->getFresh('frontpageAccounts', $repository->getAccountsByType([AccountType::ASSET])->pluck('id')->toArray());
@ -136,15 +151,12 @@ class HomeController extends Controller
}
/** @var Carbon $start */
$start = session('start', today(config('app.timezone'))->startOfMonth());
/** @var Carbon $end */
$start = session('start', today(config('app.timezone'))->startOfMonth());
$end = session('end', today(config('app.timezone'))->endOfMonth());
$accounts = $repository->getAccountsById($frontpageArray);
$today = today(config('app.timezone'));
// sort frontpage accounts by order
$accounts = $accounts->sortBy('order');
$accounts = $accounts->sortBy('order'); // sort frontpage accounts by order
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'));
}
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;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\GroupMembership;
use FireflyIII\Models\UserGroup;
use FireflyIII\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
/**
* Trait ValidatesUserGroupTrait
@ -35,37 +38,63 @@ use Illuminate\Http\Request;
trait ValidatesUserGroupTrait
{
/**
* This check does not validate which rights the user has, that comes later.
*
* @throws FireflyException
* @throws AuthorizationException
* @throws AuthenticationException
*/
protected function validateUserGroup(Request $request): ?UserGroup
protected function validateUserGroup(Request $request): UserGroup
{
app('log')->debug(sprintf('validateUserGroup: %s', get_class($this)));
if (!auth()->check()) {
app('log')->debug('validateUserGroup: user is not logged in, return NULL.');
return null;
throw new AuthenticationException();
}
/** @var User $user */
$user = auth()->user();
$groupId = 0;
if (!$request->has('user_group_id')) {
$group = $user->userGroup;
app('log')->debug(sprintf('validateUserGroup: no user group submitted, return default group #%d.', $group?->id));
$groupId = $user->user_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;
}
$groupId = (int)$request->get('user_group_id');
/** @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 does NOT have role "%s" in group #%d, continue searching.', $role->value, $groupId));
}
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.
|
*/
'layout' => env('FIREFLY_III_LAYOUT', 'v1'),
'paths' => $paths,
/*

View File

@ -59,6 +59,8 @@ return [
'invalid_selection' => 'Your selection is invalid.',
'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.',
'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.',
'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.',

View File

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