From 28e78d76904234eeb33413dd18ec70f4d722e7a6 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:57:00 -0500 Subject: [PATCH] Document the API token scopes each SDK method needs Add an "API token scopes" section to the README that maps every SDK method to the endpoint it calls and the scopes that endpoint checks, matching the per-endpoint scopes in the public API reference. It also explains how a 401 or 403 surfaces: a missing scope raises APIAccessDenied with status_code 403 and the server's "Insufficient permissions" message, and 403s with more specific messages raise APIInsufficientPermissions, APIOrganizationNotAllowed or APIInsufficientQuota, all subclasses of APIFailure. Methods whose endpoint the reference does not list are marked as such rather than given a guessed scope. --- README.rst | 350 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 349 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0ac5262..cbc4a77 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ Initializing the module **PARAMETERS:** -- **token (str)** - The Socket API Key for your Organization +- **token (str)** - The Socket API Key for your Organization. Each method needs the token to carry particular scopes; see `API token scopes`_ - **timeout (int)** - The number of seconds to wait before failing the connection - **allow_unverified (bool)** - Whether to skip SSL certificate verification (default: False). Set to True for testing with self-signed certificates. - **user_agent (str, optional)** - Custom User-Agent string to use in API requests. If not provided, defaults to "SocketSDKPython/{version}" @@ -1498,3 +1498,351 @@ List historical snapshots for an organization. - **org_slug (str)** - The organization slug - **query_params (dict, optional)** - Optional query parameters for filtering + + +API token scopes +---------------- + +The SDK authenticates every call with one organization API token, and each Socket API +endpoint checks that the token carries particular scopes. The table below lists, for each +SDK method, the endpoint it calls and the scopes that endpoint checks. Where an endpoint +lists more than one scope, the token needs all of them. "none" means any valid token for +the organization works. Paths are relative to ``https://api.socket.dev/v0/``; query strings +are omitted. + +The scopes each endpoint checks are also listed in the +`API reference `_. For a least-privilege token, grant +only the scopes of the methods your integration calls. + +Missing scopes and other 403 responses +"""""""""""""""""""""""""""""""""""""" + +Methods do not return an empty result on a 401 or 403. ``API.do_request()`` raises +before the method sees the response, so the exception reaches the caller, including when +``use_types=True``. + +A missing scope raises ``APIAccessDenied`` with ``status_code`` 403. Its message starts +with the server's ``Insufficient permissions``, followed by the request URL and the +response headers. A 401, from a bad or revoked token, also raises ``APIAccessDenied``, +with ``Unauthorized`` in the message. + +Recent API responses name the required scopes in the 403 body under +``error.details.requiredScopes``, but the SDK does not include them in the exception +message; to see them, look up the endpoint in the table below or the API reference. + +A 403 whose ``error.message`` contains one of these texts raises a more specific class +instead, and logs the server's message at ``ERROR`` on the ``socketdev`` logger: + +- ``Insufficient permissions for API method``: ``APIInsufficientPermissions`` +- ``Organization not allowed``: ``APIOrganizationNotAllowed`` +- ``Insufficient max quota``: ``APIInsufficientQuota`` (a 429 also raises this class) + +All of these classes live in ``socketdev.exceptions``, carry the HTTP status in +``status_code`` and subclass ``APIFailure``, so catching ``APIFailure`` covers every +case. + +.. code-block:: python + + from socketdev import socketdev + from socketdev.exceptions import APIAccessDenied + + socket = socketdev(token="REPLACE_ME") + try: + socket.diffscans.list("org_slug") + except APIAccessDenied as error: + if error.status_code == 403: + print("Access denied; check the token's scopes for this method:", error) + raise + +Scopes by method +"""""""""""""""" + +A few rows need a note: + +- ``fullscans.finalize_tier1()`` and ``uploadmanifests.upload_manifest_files()`` call + endpoints the API reference does not list. Both check ``full-scans:create``. +- Rows marked "not in the API reference" call an endpoint the current reference does not + list, so no scope is documented for it. ``repositories.list()`` and ``sbom.view()`` + belong to the deprecated report-era API. ``settings.integration_events()`` calls + ``settings/integrations/{integration_id}``; the reference lists the integration events + endpoint as ``settings/integrations/{integration_id}/events``, which checks + ``integration:list``. +- Some scopes differ from what the method name suggests: ``analytics.get_org()`` and + ``analytics.get_repo()`` check ``report:write``; ``apitokens.update()`` checks + ``api-tokens:create``; ``labels.setting.delete()`` checks ``repo-label:update``; and + the ``export`` methods check ``report:read`` rather than a full-scans scope. + +.. list-table:: + :header-rows: 1 + :widths: 30 45 25 + + * - Method + - Endpoint + - Scopes + * - ``alertfullscansearch.search()`` + - ``GET orgs/{org_slug}/alert-full-scan-search`` + - ``alerts:list`` + * - ``alerts.get()`` + - ``GET orgs/{org_slug}/alerts`` + - ``alerts:list`` + * - ``alerttypes.get()`` + - ``POST alert-types`` + - none + * - ``analytics.get_org()`` + - ``GET analytics/org/{filter}`` + - ``report:write`` + * - ``analytics.get_repo()`` + - ``GET analytics/repo/{name}/{filter}`` + - ``report:write`` + * - ``apitokens.create()`` + - ``POST orgs/{org_slug}/api-tokens`` + - ``api-tokens:create`` + * - ``apitokens.list()`` + - ``GET orgs/{org_slug}/api-tokens`` + - ``api-tokens:list`` + * - ``apitokens.update()`` + - ``POST orgs/{org_slug}/api-tokens/update`` + - ``api-tokens:create`` + * - ``apitokens.update(token_id=...)`` + - ``PUT orgs/{org_slug}/api-tokens/{token_id}`` + - not in the API reference + * - ``apitokens.rotate()`` + - ``POST orgs/{org_slug}/api-tokens/rotate`` + - ``api-tokens:rotate`` + * - ``apitokens.revoke()`` + - ``POST orgs/{org_slug}/api-tokens/revoke`` + - ``api-tokens:revoke`` + * - ``auditlog.get()`` + - ``GET orgs/{org_slug}/audit-log`` + - ``audit-log:list`` + * - ``basics.get_config()`` + - ``GET orgs/{org_slug}/settings/socket-basics`` + - ``socket-basics:read`` + * - ``dependencies.post()`` + - ``POST dependencies/upload`` + - ``report:write`` + * - ``dependencies.get()`` + - ``POST dependencies/search`` + - none + * - ``dependencies.get(org_slug, ecosystem, package, version)`` + - ``GET orgs/{org_slug}/dependencies/{ecosystem}/{package}/{version}`` + - not in the API reference + * - ``diffscans.list()`` + - ``GET orgs/{org_slug}/diff-scans`` + - ``diff-scans:list`` + * - ``diffscans.get()`` + - ``GET orgs/{org_slug}/diff-scans/{diff_scan_id}`` + - ``diff-scans:list`` + * - ``diffscans.create_from_repo()`` + - ``POST orgs/{org_slug}/diff-scans/from-repo/{repo_slug}`` + - ``diff-scans:create``, ``full-scans:create`` and ``repo:list`` + * - ``diffscans.create_from_ids()`` + - ``POST orgs/{org_slug}/diff-scans/from-ids`` + - ``diff-scans:create`` and ``full-scans:list`` + * - ``diffscans.gfm()`` + - ``GET orgs/{org_slug}/diff-scans/{diff_scan_id}/gfm`` + - ``diff-scans:list`` + * - ``diffscans.delete()`` + - ``DELETE orgs/{org_slug}/diff-scans/{diff_scan_id}`` + - ``diff-scans:delete`` + * - ``export.cdx_bom()`` + - ``GET orgs/{org_slug}/export/cdx/{id}`` + - ``report:read`` + * - ``export.spdx_bom()`` + - ``GET orgs/{org_slug}/export/spdx/{id}`` + - ``report:read`` + * - ``export.openvex_bom()`` + - ``GET orgs/{org_slug}/export/openvex/{id}`` + - ``report:read`` + * - ``fixes.get()`` + - ``GET orgs/{org_slug}/fixes`` + - ``fixes:list`` + * - ``fullscans.get()`` + - ``GET orgs/{org_slug}/full-scans``, or ``GET orgs/{org_slug}/full-scans/{full_scan_id}`` when ``params`` holds only ``id`` + - ``full-scans:list`` + * - ``fullscans.post()`` + - ``POST orgs/{org_slug}/full-scans`` + - ``full-scans:create`` + * - ``fullscans.delete()`` + - ``DELETE orgs/{org_slug}/full-scans/{full_scan_id}`` + - ``full-scans:delete`` + * - ``fullscans.stream_diff()`` + - ``GET orgs/{org_slug}/full-scans/diff`` + - ``full-scans:list`` + * - ``fullscans.stream()`` + - ``GET orgs/{org_slug}/full-scans/{full_scan_id}`` + - ``full-scans:list`` + * - ``fullscans.metadata()`` + - ``GET orgs/{org_slug}/full-scans/{full_scan_id}/metadata`` + - ``full-scans:list`` + * - ``fullscans.gfm()`` + - ``GET orgs/{org_slug}/full-scans/diff/gfm`` + - ``full-scans:list`` + * - ``fullscans.finalize_tier1()`` + - ``POST tier1-reachability-scan/finalize`` + - ``full-scans:create`` (not in the API reference) + * - ``fullscans.archive()`` + - ``POST orgs/{org_slug}/full-scans/archive`` + - ``full-scans:create`` + * - ``fullscans.rescan()`` + - ``POST orgs/{org_slug}/full-scans/{full_scan_id}/rescan`` + - ``full-scans:create`` + * - ``fullscans.get_tar_files()`` + - ``GET orgs/{org_slug}/full-scans/{full_scan_id}/files/tar`` + - ``full-scans:list`` + * - ``historical.list()`` + - ``GET orgs/{org_slug}/historical/alerts`` + - ``historical:alerts-list`` + * - ``historical.trend()`` + - ``GET orgs/{org_slug}/historical/alerts/trend`` + - ``historical:alerts-trend`` + * - ``historical.dependencies_trend()`` + - ``GET orgs/{org_slug}/historical/dependencies/trend`` + - ``historical:dependencies-trend`` + * - ``historical.snapshots.create()`` + - ``POST orgs/{org_slug}/historical/snapshots`` + - ``historical:snapshots-start`` + * - ``historical.snapshots.list()`` + - ``GET orgs/{org_slug}/historical/snapshots`` + - ``historical:snapshots-list`` + * - ``labels.list()`` + - ``GET orgs/{org_slug}/repos/labels`` + - ``repo-label:list`` + * - ``labels.post()`` + - ``POST orgs/{org_slug}/repos/labels`` + - ``repo-label:create`` + * - ``labels.get()`` + - ``GET orgs/{org_slug}/repos/labels/{label_id}`` + - ``repo-label:list`` + * - ``labels.delete()`` + - ``DELETE orgs/{org_slug}/repos/labels/{label_id}`` + - ``repo-label:delete`` + * - ``labels.associate()`` + - ``POST orgs/{org_slug}/repos/labels/{label_id}/associate`` + - ``repo-label:update`` + * - ``labels.disassociate()`` + - ``POST orgs/{org_slug}/repos/labels/{label_id}/disassociate`` + - ``repo-label:update`` + * - ``labels.setting.get()`` + - ``GET orgs/{org_slug}/repos/labels/{label_id}/label-setting`` + - ``repo-label:list`` + * - ``labels.setting.put()`` + - ``PUT orgs/{org_slug}/repos/labels/{label_id}/label-setting`` + - ``repo-label:update`` + * - ``labels.setting.delete()`` + - ``DELETE orgs/{org_slug}/repos/labels/{label_id}/label-setting`` + - ``repo-label:update`` + * - ``licensemetadata.post()`` + - ``POST license-metadata`` + - none + * - ``npm.issues()`` + - ``GET npm/{package}/{version}/issues`` + - none + * - ``npm.score()`` + - ``GET npm/{package}/{version}/score`` + - none + * - ``openapi.get()`` + - ``GET openapi`` + - none + * - ``org.get()`` + - ``GET organizations`` + - none + * - ``purl.post(org_slug=...)`` + - ``POST orgs/{org_slug}/purl`` + - ``packages:list`` + * - ``purl.post()`` + - ``POST purl`` + - ``packages:list`` + * - ``quota.get()`` + - ``GET quota`` + - none + * - ``report.list()`` + - ``GET report/list`` + - ``report:list`` + * - ``report.delete()`` + - ``DELETE report/delete/{id}`` + - ``report:write`` + * - ``report.view()`` + - ``GET report/view/{id}`` + - ``report:read`` + * - ``report.supported()`` + - ``GET report/supported`` + - none + * - ``report.create()`` + - ``PUT report/upload`` + - ``report:write`` + * - ``repos.get()`` + - ``GET orgs/{org_slug}/repos`` + - ``repo:list`` + * - ``repos.repo()`` + - ``GET orgs/{org_slug}/repos/{repo_slug}`` + - ``repo:list`` + * - ``repos.post()`` + - ``POST orgs/{org_slug}/repos`` + - ``repo:create`` + * - ``repos.update()`` + - ``POST orgs/{org_slug}/repos/{repo_slug}`` + - ``repo:update`` + * - ``repos.delete()`` + - ``DELETE orgs/{org_slug}/repos/{repo_slug}`` + - ``repo:delete`` + * - ``repositories.list()`` + - ``GET repos`` + - not in the API reference + * - ``sbom.view()`` + - ``GET sbom/view/{id}`` + - not in the API reference + * - ``settings.get()`` + - ``GET orgs/{org_slug}/settings/security-policy`` + - ``security-policy:read`` + * - ``settings.update_security_policy()`` + - ``POST orgs/{org_slug}/settings/security-policy`` + - ``security-policy:update`` + * - ``settings.get_license_policy()`` + - ``GET orgs/{org_slug}/settings/license-policy`` + - ``license-policy:read`` + * - ``settings.update_license_policy()`` + - ``POST orgs/{org_slug}/settings/license-policy`` + - ``license-policy:update`` + * - ``settings.integration_events()`` + - ``GET orgs/{org_slug}/settings/integrations/{integration_id}`` + - not in the API reference + * - ``supportedfiles.get()`` + - ``GET orgs/{org_slug}/supported-files`` + - none + * - ``telemetry.get_config()`` + - ``GET orgs/{org_slug}/telemetry/config`` + - none + * - ``telemetry.update_config()`` + - ``PUT orgs/{org_slug}/telemetry/config`` + - ``telemetry-policy:update`` + * - ``threatfeed.get(org_slug)`` + - ``GET orgs/{org_slug}/threat-feed`` + - ``threat-feed:list`` + * - ``threatfeed.get()`` + - ``GET threat-feed`` + - ``threat-feed:list`` + * - ``triage.list_alert_triage()`` + - ``GET orgs/{org_slug}/triage/alerts`` + - ``triage:alerts-list`` + * - ``triage.update_alert_triage()`` + - ``POST orgs/{org_slug}/triage/alerts`` + - ``triage:alerts-update`` + * - ``uploadmanifests.upload_manifest_files()`` + - ``POST orgs/{org_slug}/upload-manifest-files`` + - ``full-scans:create`` (not in the API reference) + * - ``webhooks.list()`` + - ``GET orgs/{org_slug}/webhooks`` + - ``webhooks:list`` + * - ``webhooks.create()`` + - ``POST orgs/{org_slug}/webhooks`` + - ``webhooks:create`` + * - ``webhooks.get()`` + - ``GET orgs/{org_slug}/webhooks/{webhook_id}`` + - ``webhooks:list`` + * - ``webhooks.update()`` + - ``PUT orgs/{org_slug}/webhooks/{webhook_id}`` + - ``webhooks:update`` + * - ``webhooks.delete()`` + - ``DELETE orgs/{org_slug}/webhooks/{webhook_id}`` + - ``webhooks:delete``