Repository navigation
Conversation
The admin routes need each user's email and name from Auth0, read when an admin asks and never stored. These tests describe a second, optional settings group (AUTH0_USER_PROFILES_*), separate from the production-only deletion settings, and the Management API client's two reads: profiles for a set of user IDs, one search per 50 IDs, and user IDs found by part of an email, with every query character escaped and paging stopped at Auth0's 1,000-result limit. Every failure is one "profiles unavailable" error whose message carries no email, user ID, token or secret. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AUTH0_USER_PROFILES_DOMAIN, _CLIENT_ID and _CLIENT_SECRET or
_CLIENT_SECRET_RESOURCE configure a client for reading users, validated
like the deletion settings by the same loader, and built through the same
shared-client token cache. The AUTH0_MANAGEMENT_* deletion settings and
their messages are unchanged.
read_user_profiles searches Auth0 for each group of at most 50 user IDs
(q=user_id:("..." OR "..."), search engine v3), quoting each ID with its
quotes and backslashes escaped. search_user_ids_by_email escapes every
Lucene special character and whitespace in the text, uses a leading
wildcard only from three characters on, and pages 50 at a time up to
Auth0's 1,000-result limit, saying whether it reached it. Any failure,
the token request included, raises Auth0UserProfilesUnavailableError,
whose message names at most an HTTP status or an error type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The token request parses its answer with response.json(), which raises ValueError for a body that isn't JSON. A profile read now reports that as Auth0UserProfilesUnavailableError too, so callers handle one error type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a profile reader configured, the admin user list, a single user, a type change's response and the type history take each user's email and names from Auth0, looking up only identities from the users' issuer: the most recently seen identity first, a verified email before an unverified one, stored values when Auth0 has none. An email search finds users through Auth0 as well as by their stored emails, with the same paging. When Auth0 is unavailable, reads keep stored values and record a WARNING without personal data, a type change still commits, and an email search answers 503. Callers below admin never reach Auth0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The admin user list, a single user, a type change's response and the type history now take each user's email and names from Auth0 through the user profile reader, read after the database work and never stored. Only identities whose issuer is the users' issuer are looked up, the most recently seen first, one profile read per page; a verified email comes before an unverified one, and stored values stay where Auth0 has none. With profiles configured, an email search also lists the users with an identity among the subjects Auth0 found for the text, together with the users whose stored email matches, newest first, with the same offset, limit and has_more. Without them, the search reads stored emails only, as before. When Auth0 can't answer, reads answer with stored values and record the admin_user_profiles_unavailable WARNING with the operation and error type only; a type change commits before its response reads Auth0; and an email search answers 503 (AdminUserProfilesUnavailableError). The admin check runs before any Auth0 search. Response shapes are unchanged; the OpenAPI descriptions say where emails and names come from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A partial AUTH0_USER_PROFILES_* group, or one with a URL for its domain, now fails readiness, which the deploy waits on, as the deletion settings already do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both deploy jobs in push.yml now pass AUTH0_USER_PROFILES_DOMAIN and AUTH0_USER_PROFILES_CLIENT_ID from their GitHub environment's variables, and AUTH0_USER_PROFILES_CLIENT_SECRET_RESOURCE from its secrets, as the production job passes the AUTH0_MANAGEMENT_* settings. validate_cloud_run_deploy_env.sh requires the three together or none, and allows them in staging: the client only reads users, so it can't delete the staging CI user as the deletion settings could. deploy_cloud_run_candidate.sh writes them to the revision only when set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stage 12A design doc says where the admin routes' emails and names come from, how the email search queries Auth0 and its limits, what each route does when Auth0 can't answer, the AUTH0_USER_PROFILES_* settings and the operator steps for the read-only client. The testing skill lists the new service tests, and a changelog fragment describes the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The email search now lists users with an identity among the subjects Auth0 found. This runs that read as the Public API's runtime role on PostgreSQL, finding a user with no stored email through a stand-in for Auth0, and checks that nothing read from Auth0 is stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…turns Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e reads One admin request can make several Management API requests in a row, and on Auth0's Free plan (2 requests a second) the later ones get 429. These tests describe a profile read or search that waits the time Auth0 gives (Retry-After, or X-RateLimit-Reset less the current time) and retries once when that is at most 2 seconds, sends all of one operation's requests through one HTTP client, sorts every search page by user ID so pages don't shift, and gives every failure a cause without personal data (the HTTP status, timeout, connection_error, unreadable_answer, or the token request's status), so a missing read:users grant can be told from rate limiting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A profile read or email search now sends its token request and all its searches through one httpx.Client, so one connection serves them, and sorts every page by user ID (sort=user_id:1). A 429 is retried once after the wait Auth0 gives, from Retry-After or X-RateLimit-Reset less the current time, when that wait is at most 2 seconds; otherwise, or when the retry is rate limited too, profiles are unavailable as before. Every Auth0ManagementError now carries a cause without personal data: http_<status>, token_http_<status>, token_unreadable_answer, timeout, connection_error, unreadable_answer or secret_unavailable. A token answer that isn't JSON raises Auth0ManagementError rather than ValueError. Account deletion's requests and messages are otherwise unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A common text such as gmail matches many Auth0 users, and paging through up to 1,000 of them takes 20 Management API requests in a row. These tests describe a search that stops after 200 matching users (four pages of 50, and a fifth to see whether more match), counts each user once across pages, and is refused with 422 when more than 200 match; exactly 200 are not more. Text with < or >, which Auth0's query syntax reads as a range and can't escape, and which no email address holds unquoted, is refused with 422 before any Auth0 call, with or without profile settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or > search_user_ids_by_email takes a match limit and reads pages of 50 until a page is short or more users than the limit have matched, counting each user once; it then returns no IDs and says more matched. The admin service passes MAXIMUM_EMAIL_SEARCH_MATCHES (200), so a search reads at most five pages, and answers 422 with "More than 200 users match that email text; enter more of it". Exactly 200 matches are served. With the 1,000-result limit out of reach, its handling and the admin_user_email_search_limit_reached record are gone. Search text with < or > is refused with 422 before any Auth0 call, whether or not profiles are configured: Auth0's query syntax reads them as a range and can't escape them, and no email address holds them unquoted. The client refuses such text too (ValueError). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ord a cause The unusable-settings test now covers the single read, the type change's response and the history, not only the list; a new test checks that the history never looks up the admin's identities from another issuer. The admin_user_profiles_unavailable record is expected to carry the failure's safe cause (http_403, http_429, timeout, invalid_settings, or unknown for an error without one), so a missing read:users grant can be told from rate limiting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The admin_user_profiles_unavailable WARNING now carries a cause beside the operation and error type: the profile reader's cause (http_403 for a client without read:users, http_429, http_5xx, timeout, connection_error, unreadable_answer and so on), invalid_settings for unusable settings, or unknown. Only a short lowercase identifier is recorded, so no email address or subject can pass as a cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only identities from PUBLIC_API_AUTH_ISSUER are looked up, so profile settings without it would quietly read profiles for nobody. These tests expect the settings loader, readiness, the default profile source and the deploy validation to refuse them instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
load_auth0_user_profile_settings now raises when the AUTH0_USER_PROFILES_* settings are set without PUBLIC_API_AUTH_ISSUER, so readiness fails, and validate_cloud_run_deploy_env.sh refuses them too, instead of the profiles being quietly read for nobody. The default profile source no longer returns None for a missing issuer; an admin read then keeps stored values and records invalid_settings, and an email search answers 503. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_auth0_user_profile_reader now returns the UserProfileReader protocol, which has only the profile read and the email search, rather than the whole Management API client with delete_user, so mypy refuses a deletion through it. The protocol moves to the Auth0 module beside the types it returns; the admin service refers to it from there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arches The admin list route's OpenAPI description and its email parameter now say that text with < or > is refused (422), and so is text that more than 200 Auth0 users match. The Stage 12A design doc describes the limit, the refusal, the sort, the one HTTP client per operation, the 429 retry, the cause the warning record carries, and that the profile settings need PUBLIC_API_AUTH_ISSUER. It drops the paid-plan rate-limit figures, which weren't confirmed, and says that the release-tests job never receives the Auth0 settings rather than that it unsets them. The changelog fragment mentions the refusals and the issuer requirement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tails Rate-limit waits added up across every Auth0 request one admin request made. These tests describe a budget of 2 seconds of waiting in total, shared by an admin request's email search and profile read and spent by each wait, so a short wait still retries but several waits stop at 2 seconds. They also pin the retry's details: after a 401 refresh, the retry of a rate-limited search sends the fresh token; Retry-After takes precedence over X-RateLimit-Reset; and a wait of nan or infinity is not waited for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RateLimitWaitBudget holds the seconds an operation may still wait for Auth0's rate limit. A rate-limited search waits and retries once only when its whole wait fits in what is left, and spends it. read_user_profiles and search_user_ids_by_email take a budget, or start one of 2 seconds of their own, and every admin service call that reads Auth0 starts one budget and passes it to its email search and its profile read, so their waits together stay within 2 seconds instead of adding up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A search's fifth page can be full only through duplicate or ID-less entries, and it may hide more users, so it is expected to count as more than the limit; 220 entries naming exactly 200 users are expected to be served. Text with < or > is expected to be refused before the profile settings are read, so unusable settings give 422, not 503. A reader error whose cause is not a short identifier is expected to be recorded as unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
search_user_ids_by_email reads at most match_limit // 50 + 1 pages. When the last of them is full without more than match_limit distinct users, which only duplicate or ID-less entries can cause, more users may match, so the search now says more than the limit matched instead of serving what it read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests of the 200-match limit, its refusal message and its OpenAPI descriptions now read the limit from the constant, and derive their pages from it, so they follow it if it changes. With the constant set to 150, only the OpenAPI test fails, because the descriptions still write the number as text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The admin routes' 422 description, the user list's description and the email parameter's description now take the number from MAXIMUM_EMAIL_SEARCH_MATCHES rather than writing 200 as text, so the OpenAPI document and the refusal can't disagree. The Stage 12A design doc keeps the number in prose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The admin service now passes one rate-limit wait budget to its email search and profile read; the stand-in takes it and checks it is one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uest build_auth0_user_profile_reader returns one client per process, so a fallback budget kept on the client would make every admin request share one budget. A test now checks that two calls without a budget on one client each get 2 seconds, and another that a type change and the type history each pass a budget of their own, as the list and the single read do. Both pass, and each fails against the version it guards against. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The admin service should always create a budget, which it can't without importing the Auth0 module while the budget lives there. These tests describe policyengine_api.libs.rate_limit_wait_budget: importing it loads no other application module, the admin service module loads it but not the Auth0 module, and a budget of 2 seconds spends only the waits that fit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RateLimitWaitBudget and its 2-second default now live in policyengine_api.libs.rate_limit_wait_budget, which imports no other application module. The Auth0 module and the admin service both import it at module level, so the service always starts a budget for each admin request: its private methods take a RateLimitWaitBudget, never None, and the import inside _wait_budget_for is gone with that function. The Auth0 module still stays out of the service's module-level imports. The new module is added to mypy's files and to the paths of rule api.quality.mypy-v2, whose statement now names the Auth0 Management libraries with their wait budget; docs/generated/rules.md is re-rendered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
take(nan) succeeded and left nan seconds, which every later wait fits, and take(-5.0) added 5 seconds. The test expects such waits to be refused without spending anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RateLimitWaitBudget.take now returns False, spending nothing, for a wait that is negative or not a finite number, as for one longer than what is left, so a rate-limited request with such a wait counts as unavailable. The client already clamps and filters Auth0's headers; this keeps the budget sound for any caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in the design doc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Draft
4 tasks
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4283 +/- ##
==========================================
+ Coverage 92.69% 92.82% +0.13%
==========================================
Files 329 330 +1
Lines 18783 19154 +371
Branches 3114 3096 -18
==========================================
+ Hits 17410 17780 +370
- Misses 818 821 +3
+ Partials 555 553 -2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The staging deploy job's comment on the read-only Auth0 client named AUTH0_MANAGEMENT_*, which the existing guard against those settings reaching staging (test_push_workflow_wires_user_auth_settings_and_staging_test_user) refuses wherever it appears in the job. The comment now calls them the production-only account-deletion settings, and the guard stays as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4282
Summary
AUTH0_USER_PROFILES_DOMAIN,AUTH0_USER_PROFILES_CLIENT_ID, andAUTH0_USER_PROFILES_CLIENT_SECRET_RESOURCEorAUTH0_USER_PROFILES_CLIENT_SECRET.PUBLIC_API_AUTH_ISSUER.build_auth0_user_profile_readerreturns a read-onlyUserProfileReader, with no deletion.AUTH0_MANAGEMENT_*deletion settings are unchanged and stay production-only: staging's live tests delete their own account throughDELETE /v2/me.read_user_profiles: oneGET /api/v2/userssearch per 50 user IDs (q=user_id:("…" OR "…"),search_engine=v3,sort=user_id:1, only the profile fields), with quotes and backslashes escaped.search_user_ids_by_email: escapes every Lucene special character and whitespace, and usesemail:*text*(oremail:text*below three characters). It reads sorted pages of 50, counting each user once, and stops once more than the match limit match. A full last page counts as more than the limit.Retry-Afterfirst, elseX-RateLimit-Resetminus now), when that wait fits in what's left of aRateLimitWaitBudget(policyengine_api.libs.rate_limit_wait_budget). Each admin request starts one 2-second budget, shared by all its Auth0 calls. A call given no budget gets its own, and a negative or non-finite wait is never taken.http_<status>,timeout,connection_error,unreadable_answer,token_http_<status>,token_unreadable_answerorsecret_unavailable.email,first_nameandlast_name(and the history'schanged_by_email) from Auth0, for identities from the users' issuer. The most recently seen identity comes first, a verified email is preferred, and stored values fill any gaps. Nothing read from Auth0 is stored.offset,limitandhas_more. It answers 422 for:<or>, before the profile settings are read or Auth0 is called;MAXIMUM_EMAIL_SEARCH_MATCHES(200) Auth0 users match ("More than 200 users match that email text; enter more of it"). Exactly 200 are served.admin_user_profiles_unavailable, with the operation, the error type and a safe cause (unknownunless it is a short identifier). A type change still commits, and an email search answers 503.validate_cloud_run_deploy_env.sh,deploy_cloud_run_candidate.sh, and both deploy jobs inpush.ymlcarry the new settings: variables for the domain and client ID, a secret for the secret resource. The Stage 12A design doc, the OpenAPI descriptions (whose limit comes from the constant), the testing skill and a changelog fragment are updated.api.quality.mypy-v2's paths inrules/registry.toml, withdocs/generated/rules.mdre-rendered. This change to the rule machinery needs approval from a core developer other than the author.Operator steps, not in this PR:
read:usersin each environment's users' tenant (policyengine.uk.auth0.comfor production,policyengine-dev.uk.auth0.comfor staging), on the canonical*.auth0.comdomain.AUTH0_USER_PROFILES_DOMAINandAUTH0_USER_PROFILES_CLIENT_IDas GitHub environment variables, andAUTH0_USER_PROFILES_CLIENT_SECRET_RESOURCEas a GitHub environment secret, instagingandproduction.roles/secretmanager.secretAccessoron it.Until then, admins see stored values as today. Unverified until then: the GitHub variables and secret, the Secret Manager secrets and their access grants, and each client's
read:usersgrant.Shares files with #4269 and #4272 (the admin routes, the user-type service and, for #4272, the Auth0 Management module); whichever merges later resolves the conflicts.
Testing
httpx.MockTransportand a recorded wait instead of sleeping; no network calls (tests/unit/libs/test_auth0_management.py). They cover:<and>;tests/unit/libs/test_rate_limit_wait_budget.py): its module loads no other application module, the admin service loads it but not the Auth0 module, it spends waits that fit, and it refuses negative and non-finite ones.tests/unit/v2/test_admin_user_profile_service.py). They cover:tests/unit/v2/test_admin_user_routes.py). Readiness, deploy validation, candidate deploy and push workflow tests cover the new settings.tests/integration/test_v2_user_type_persistence.py::test_the_runtime_role_finds_users_by_their_auth0_email).GET /api/v2/users/{id}, a search by several user IDs joined withOR, prefix, suffix and text-anywhere email searches, andsort=user_id:1) were tried by hand, read-only, on the production users' tenant.lint-imports,python -m scripts.rules validate,python scripts/run_quality_guards.py,ruff check,ruff format --check,mypy,scripts/export_migration_contracts.py(no diff) and the changelog fragment check.</>turning a short search into a range query), fixed test-first; rounds 2 and 3 found none, and their quality findings are fixed.Inline suppressions added: none.
🤖 Generated with Claude Code