{"record":{"id":"1dad47e94b6e92fa","repo":"BookStackApp/BookStack","slug":"errors-api-user-no-api-permission","errorCode":null,"errorMessage":"errors.api_user_no_api_permission","messagePattern":"errors\\.api_user_no_api_permission","errorType":"exception","errorClass":"ApiAuthException","httpStatus":403,"severity":"error","filePath":"app/Api/ApiTokenGuard.php","lineNumber":136,"sourceCode":"     * @throws ApiAuthException\n     */\n    protected function validateToken(?ApiToken $token, string $secret): void\n    {\n        if ($token === null) {\n            throw new ApiAuthException(trans('errors.api_user_token_not_found'));\n        }\n\n        if (!Hash::check($secret, $token->secret)) {\n            throw new ApiAuthException(trans('errors.api_incorrect_token_secret'));\n        }\n\n        $now = Carbon::now();\n        if ($token->expires_at <= $now) {\n            throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);\n        }\n\n        if (!$token->user->can(Permission::AccessApi)) {\n            throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);\n        }\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function validate(array $credentials = []): bool\n    {\n        if (empty($credentials['id']) || empty($credentials['secret'])) {\n            return false;\n        }\n\n        $token = ApiToken::query()\n            ->where('token_id', '=', $credentials['id'])\n            ->with(['user'])->first();\n\n        if ($token === null) {\n            return false;","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Api/ApiTokenGuard.php#L118-L154","documentation":"ApiTokenGuard::validateToken throws ApiAuthException('errors.api_user_no_api_permission') with status 403 when $token->user->can(Permission::AccessApi) is false. The token itself is valid (exists, secret matches, not expired), but the owning user account lacks the 'Access the API' system permission, so all its API tokens are refused.","triggerScenarios":"An admin revoked or never granted the 'Access the API' permission from the user's role/profile; the token owner's role was changed to one without API access; API access was disabled for a whole role during a security lockdown.","commonSituations":"Role refactors that silently drop API permission; offboarding processes that strip permissions while integrations still run under that user; new tokens created for a read-only role that lacks AccessApi.","solutions":["In BookStack admin, grant the user (or its role) the 'Access the API' permission","If the user account is intentionally restricted, issue a token under a dedicated integration user with API access and minimal role permissions","Audit role permission changes when API 403s start appearing","Keep a dedicated service account for integrations so human permission changes don't break them"],"exampleFix":"// before\n// user 'report-bot' role has no 'Access the API' permission -> all its tokens 403\n// after\n// Admin UI: Edit role 'bots' -> check 'Access the API' -> save; retry the request","handlingStrategy":"try-catch","validationCode":"// Ensure the service user has API access before deploying the integration\n// Check in admin: the user's role has 'Access the API' permission enabled\n// Optionally verify with a cheap authenticated call at startup:\n$probe = $client->get('/api/books', ['headers' => ['Authorization' => \"Token {$id}:{$secret}\"]]);\nif ($probe->getStatusCode() !== 200) { throw new RuntimeException('API user lacks AccessApi permission'); }","typeGuard":"function apiProbeSucceeds(array $headers): bool {\n    $code = (int) (probeBookstack('/api/books', $headers) ?? 500);\n    return $code === 200;\n}","tryCatchPattern":"try {\n    $res = $client->get($url, ['headers' => ['Authorization' => \"Token {$id}:{$secret}\"]]);\n} catch (ClientException $e) {\n    if ($e->getResponse()->getStatusCode() === 403) {\n        // ask an admin to re-enable 'Access the API' for the token's user/role\n    }\n    throw $e;\n}","preventionTips":["Use a dedicated service account whose role never loses API permission","Re-check role permissions after any BookStack upgrade or role refactor","Include the AccessApi permission in deployment checklists for integrations","Separate human accounts from integration accounts to avoid offboard side effects"],"tags":["api","authorization","permissions"],"backgroundTag":"missing-api-permission","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}