diff --git a/app/Api/V2/Controllers/JsonApi/AccountController.php b/app/Api/V2/Controllers/JsonApi/AccountController.php index cf6943048c..612149e996 100644 --- a/app/Api/V2/Controllers/JsonApi/AccountController.php +++ b/app/Api/V2/Controllers/JsonApi/AccountController.php @@ -24,6 +24,11 @@ declare(strict_types=1); namespace FireflyIII\Api\V2\Controllers\JsonApi; use FireflyIII\Http\Controllers\Controller; +use FireflyIII\JsonApi\V2\Accounts\AccountCollectionQuery; +use FireflyIII\JsonApi\V2\Accounts\AccountSchema; +use FireflyIII\JsonApi\V2\Accounts\Capabilities\AccountQuery; +use Illuminate\Support\Facades\Log; +use LaravelJsonApi\Core\Responses\DataResponse; use LaravelJsonApi\Laravel\Http\Controllers\Actions; /** @@ -38,7 +43,8 @@ class AccountController extends Controller use Actions\AttachRelationship; use Actions\Destroy; use Actions\DetachRelationship; - use Actions\FetchMany; + +// use Actions\FetchMany; use Actions\FetchOne; use Actions\FetchRelated; use Actions\FetchRelationship; @@ -46,6 +52,28 @@ class AccountController extends Controller use Actions\Update; use Actions\UpdateRelationship; + /** + * Fetch zero to many JSON API resources. + * + * @param AccountSchema $schema + * @param AccountQuery $request + * + * @return \Illuminate\Contracts\Support\Responsable|\Illuminate\Http\Response + */ + public function index(AccountSchema $schema, AccountCollectionQuery $request) + { + Log::debug(__METHOD__); + $models = $schema + ->repository() + ->queryAll() + ->withRequest($request) + ->get(); + + // do something custom... + + return new DataResponse($models); + } + // public function readAccountBalances(AnonymousQuery $query, AccountBalanceSchema $schema, Account $account): Responsable // { diff --git a/app/JsonApi/V2/Accounts/AccountCollectionQuery.php b/app/JsonApi/V2/Accounts/AccountCollectionQuery.php new file mode 100644 index 0000000000..99290f88d6 --- /dev/null +++ b/app/JsonApi/V2/Accounts/AccountCollectionQuery.php @@ -0,0 +1,60 @@ + [ + 'nullable', + 'array', + JsonApiRule::fieldSets(), + ], + 'user_group_id' => [ + 'nullable', + 'integer', + new IsAllowedGroupAction(Account::class, request()->method()), + ], + 'filter' => [ + 'nullable', + 'array', + JsonApiRule::filter(), + ], + 'include' => [ + 'nullable', + 'string', + JsonApiRule::includePaths(), + ], + 'page' => [ + 'nullable', + 'array', + JsonApiRule::page(), + ], + 'sort' => [ + 'nullable', + 'string', + JsonApiRule::sort(), + ], + 'withCount' => [ + 'nullable', + 'string', + JsonApiRule::countable(), + ], + ]; + } +} diff --git a/app/JsonApi/V2/Accounts/AccountQuery.php b/app/JsonApi/V2/Accounts/AccountQuery.php new file mode 100644 index 0000000000..050baa3fc2 --- /dev/null +++ b/app/JsonApi/V2/Accounts/AccountQuery.php @@ -0,0 +1,45 @@ + [ + 'nullable', + 'array', + JsonApiRule::fieldSets(), + ], + 'filter' => [ + 'nullable', + 'array', + JsonApiRule::filter()->forget('id'), + ], + 'include' => [ + 'nullable', + 'string', + JsonApiRule::includePaths(), + ], + 'page' => JsonApiRule::notSupported(), + 'sort' => JsonApiRule::notSupported(), + 'withCount' => [ + 'nullable', + 'string', + JsonApiRule::countable(), + ], + ]; + } +} diff --git a/app/JsonApi/V2/Accounts/AccountResource.php b/app/JsonApi/V2/Accounts/AccountResource.php index bc876802b0..437a7ce20c 100644 --- a/app/JsonApi/V2/Accounts/AccountResource.php +++ b/app/JsonApi/V2/Accounts/AccountResource.php @@ -4,6 +4,7 @@ namespace FireflyIII\JsonApi\V2\Accounts; use FireflyIII\Models\Account; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; use LaravelJsonApi\Core\Resources\JsonApiResource; /** @@ -31,6 +32,7 @@ class AccountResource extends JsonApiResource */ public function attributes($request): iterable { + Log::debug(__METHOD__); return [ 'created_at' => $this->resource->created_at, 'updated_at' => $this->resource->updated_at, diff --git a/app/JsonApi/V2/Accounts/AccountSchema.php b/app/JsonApi/V2/Accounts/AccountSchema.php index adac8309b1..2713400f3d 100644 --- a/app/JsonApi/V2/Accounts/AccountSchema.php +++ b/app/JsonApi/V2/Accounts/AccountSchema.php @@ -4,6 +4,7 @@ namespace FireflyIII\JsonApi\V2\Accounts; use FireflyIII\Models\Account; use FireflyIII\Support\JsonApi\Concerns\UsergroupAware; +use Illuminate\Support\Facades\Log; use LaravelJsonApi\Core\Schema\Schema; use LaravelJsonApi\Eloquent\Fields\Relations\HasOne; use LaravelJsonApi\NonEloquent\Fields\Attribute; @@ -30,6 +31,7 @@ class AccountSchema extends Schema */ public function fields(): array { + Log::debug(__METHOD__);; return [ ID::make(), Attribute::make('name'), @@ -44,6 +46,7 @@ class AccountSchema extends Schema */ public function filters(): array { + Log::debug(__METHOD__);; return [ // Filter::make('id'), ]; @@ -51,6 +54,10 @@ class AccountSchema extends Schema public function repository(): AccountRepository { + Log::debug(__METHOD__);; + // to access the repository, you need to have the necessary rights. + + $this->setUserGroup($this->server->getUsergroup()); return AccountRepository::make() ->withServer($this->server) diff --git a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php index d78712978d..7afcca2a7e 100644 --- a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php +++ b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php @@ -29,6 +29,7 @@ use FireflyIII\Support\JsonApi\ExpandsQuery; use FireflyIII\Support\JsonApi\FiltersPagination; use FireflyIII\Support\JsonApi\SortsCollection; use FireflyIII\Support\JsonApi\ValidateSortParameters; +use Illuminate\Support\Facades\Log; use LaravelJsonApi\Contracts\Store\HasPagination; use LaravelJsonApi\NonEloquent\Capabilities\QueryAll; use LaravelJsonApi\NonEloquent\Concerns\PaginatesEnumerables; @@ -48,6 +49,7 @@ class AccountQuery extends QueryAll implements HasPagination */ public function get(): iterable { + Log::debug(__METHOD__); // collect filters $filters = $this->queryParameters->filter(); // collect sort options diff --git a/app/JsonApi/V2/Server.php b/app/JsonApi/V2/Server.php index 171f858f7f..e59c629ac2 100644 --- a/app/JsonApi/V2/Server.php +++ b/app/JsonApi/V2/Server.php @@ -30,6 +30,7 @@ class Server extends BaseServer */ public function serving(): void { + // at this point the user may not actually have access to this user group. $res = $this->detectUserGroup(); $this->setUserGroup($res); } diff --git a/app/Rules/IsAllowedGroupAction.php b/app/Rules/IsAllowedGroupAction.php new file mode 100644 index 0000000000..5e5c3036f2 --- /dev/null +++ b/app/Rules/IsAllowedGroupAction.php @@ -0,0 +1,127 @@ +className = $className; + $this->methodName = $methodName; + // you need these roles to do anything with any endpoint. + $this->acceptedRoles = [UserRoleEnum::OWNER, UserRoleEnum::FULL]; + $this->repository = app(UserGroupRepositoryInterface::class); + } + + /** + * @inheritDoc + * @throws AuthorizationException + */ + #[\Override] public function validate(string $attribute, mixed $value, Closure $fail): void + { + if('GET' === $this->methodName) { + // need at least "read only rights". + $this->acceptedRoles[] = UserRoleEnum::READ_ONLY; + } + if('GET' !== $this->methodName) { + // either post, put or delete or something else.. you need more access rights. + switch ($this->className) { + default: + throw new AuthorizationException(sprintf('Cannot handle class "%s"', $this->className)); + case Account::class: + $this->acceptedRoles[] = UserRoleEnum::MANAGE_TRANSACTIONS; + break; + } + } + $this->validateUserGroup((int)$value, $fail); + } + + private function validateUserGroup(int $userGroupId, Closure $fail): void { + Log::debug(sprintf('validateUserGroup: %s', static::class)); + if (!auth()->check()) { + Log::debug('validateUserGroup: user is not logged in, return NULL.'); + $fail('validation.no_auth_user_group')->translate(); + return; + } + + /** @var User $user */ + $user = auth()->user(); + if(0 !== $userGroupId) { + Log::debug(sprintf('validateUserGroup: user group submitted, search for memberships in group #%d.', $userGroupId)); + } + if (0 === $userGroupId) { + $userGroupId = $user->user_group_id; + Log::debug(sprintf('validateUserGroup: no user group submitted, use default group #%d.', $userGroupId)); + } + + $this->repository->setUser($user); + $memberships = $this->repository->getMembershipsFromGroupId($userGroupId); + + if (0 === $memberships->count()) { + Log::debug(sprintf('validateUserGroup: user has no access to group #%d.', $userGroupId)); + $fail('validation.no_access_user_group')->translate(); + return; + } + + // need to get the group from the membership: + $userGroup = $this->repository->getById($userGroupId); + if (null === $userGroup) { + Log::debug(sprintf('validateUserGroup: group #%d does not exist.', $userGroupId)); + $fail('validation.belongs_user_or_user_group')->translate(); + return; + } + Log::debug(sprintf('validateUserGroup: validate access of user to group #%d ("%s").', $userGroupId, $userGroup->title)); + Log::debug(sprintf('validateUserGroup: have %d roles to check.', count($this->acceptedRoles)), $this->acceptedRoles); + + /** @var UserRoleEnum $role */ + foreach ($this->acceptedRoles as $role) { + if ($user->hasRoleInGroupOrOwner($userGroup, $role)) { + Log::debug(sprintf('validateUserGroup: User has role "%s" in group #%d, return.', $role->value, $userGroupId)); + + return; + } + Log::debug(sprintf('validateUserGroup: User does NOT have role "%s" in group #%d, continue searching.', $role->value, $userGroupId)); + } + + Log::debug('validateUserGroup: User does NOT have enough rights to access endpoint.'); + $fail('validation.belongs_user_or_user_group')->translate(); + } +} diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 330072cd2e..fc92cecb81 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -268,6 +268,7 @@ return [ 'auto_budget_period_mandatory' => 'The auto budget period is a mandatory field.', // no access to administration: + 'no_auth_user_group' => 'You have to be logged in to access this administration.', 'no_access_user_group' => 'You do not have the correct access rights for this administration.', 'administration_owner_rename' => 'You can\'t rename your standard administration.', ];