{"record":{"id":"1b8092979528fdfa","repo":"we-promise/sure","slug":"access-forbidden-1b8092","errorCode":"access_forbidden","errorMessage":"Access forbidden - check your permissions","messagePattern":"Access forbidden - check your permissions","errorType":"exception","errorClass":"AuthenticationError","httpStatus":403,"severity":"error","filePath":"app/models/provider/indexa_capital.rb","lineNumber":206,"sourceCode":"\n      jwt\n    end\n\n    def handle_response(response)\n      case response.code\n      when 200, 201\n        begin\n          JSON.parse(response.body, symbolize_names: true)\n        rescue JSON::ParserError => e\n          raise Error.new(\"Invalid JSON in response: #{e.message}\", :bad_response)\n        end\n      when 400\n        Rails.logger.error \"IndexaCapital API: Bad request - #{response.body}\"\n        raise Error.new(\"Bad request: #{response.body}\", :bad_request)\n      when 401\n        raise AuthenticationError.new(\"Invalid credentials\", :unauthorized)\n      when 403\n        raise AuthenticationError.new(\"Access forbidden - check your permissions\", :access_forbidden)\n      when 404\n        raise Error.new(\"Resource not found\", :not_found)\n      when 429\n        raise Error.new(\"Rate limit exceeded. Please try again later.\", :rate_limited)\n      when 500..599\n        raise Error.new(\"IndexaCapital server error (#{response.code}). Please try again later.\", :server_error)\n      else\n        Rails.logger.error \"IndexaCapital API: Unexpected response - Code: #{response.code}, Body: #{response.body}\"\n        raise Error.new(\"Unexpected error: #{response.code} - #{response.body}\", :unknown)\n      end\n    end\n\n    # Extract accounts array from /users/me response\n    # API returns: { accounts: [{ account_number: \"ABC12345\", type: \"mutual\", status: \"active\", ... }] }\n    def extract_accounts(user_data)\n      accounts = user_data[:accounts] || []\n      accounts.map do |acct|\n        {","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/indexa_capital.rb#L188-L224","documentation":"The API answered HTTP 403: the credential is valid (it passed auth) but is not entitled to the resource. Mapped to AuthenticationError(:access_forbidden), though it is an authorization problem, not an authentication one - the token works, the target does not.","triggerScenarios":"Querying /accounts/{n}/... with an account_number that belongs to a different Indexa user; a token whose plan/permissions exclude the endpoint; regional or account-state restrictions (e.g. closed or transferred pension plan).","commonSituations":"Typo'd or transposed account numbers that are well-formed but foreign, tokens scoped to a subset of accounts, account ownership changed (divorce/inheritance transfers) while the number stayed in your sync list.","solutions":["Only ever call account endpoints with account_number values freshly returned by list_accounts for the same credential","Drop or disable the offending account from the sync set and re-list to confirm it disappeared","Check the Indexa dashboard for the token's permitted accounts/scopes","If every call 403s with a token that used to work, the token's permissions changed - regenerate it"],"exampleFix":"# before\nprovider.get_portfolio(account_number: params[:account_number]) # user-supplied\n\n# after\nowned = provider.list_accounts.map { |a| a[:account_number] }\nraise ArgumentError, \"account not owned by this credential\" unless owned.include?(params[:account_number])\nprovider.get_portfolio(account_number: params[:account_number])","handlingStrategy":"validation","validationCode":"owned = provider.list_accounts.map { |a| a[:account_number] }.to_set\nraise ArgumentError, \"account not owned by this credential\" unless owned.include?(account_number)\nprovider.get_portfolio(account_number: account_number)","typeGuard":"def indexa_forbidden?(error)\n  error.is_a?(Provider::IndexaCapital::AuthenticationError) && error.error_type == :access_forbidden\nend","tryCatchPattern":"begin\n  provider.get_portfolio(account_number: num)\nrescue Provider::IndexaCapital::AuthenticationError => e\n  raise unless e.error_type == :access_forbidden\n  account.update!(sync_disabled: true, disable_reason: \"not_authorized_by_provider\")\nend","preventionTips":["Always cross-check the account number against a fresh list_accounts for the same credential","Never accept account numbers from user input into provider calls","Remember :access_forbidden means the token WORKS - don't touch credentials when you see it","When an ownership change happens at Indexa, expect this on the affected account only"],"tags":["indexa-capital","http-403","forbidden","authorization","permissions"],"backgroundTag":"http-403-forbidden","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}