Rewrite pack — keystone — branch stable/2026.2
Branch
stable/2026.2@e6dc5ae079e6— repository https://opendev.org/openstack/keystone — analysed on 2026-10-28 Client references: keystonemiddlewarestable/2026.2@3a8a835bb573(keystonemiddleware:), keystoneauthstable/2026.2@1cd5400202c6(keystoneauth:), tempest @9831863ccfea(tempest:)
This file is the entry point for re-implementing Keystone in another language (OpenKeel, in Go) as a drop-in replacement:
- every OpenStack service (keystonemiddleware), every client (keystoneauth, openstacksdk, Horizon) and the deployment tools must keep working unchanged;
- ideally, the Python and Go versions can run side by side on the same database and the same keys during the migration.
1. Contents of _rewrite/
| File | Use in a rewrite |
|---|---|
[routes_stable-2026.2.json](_rewrite/routes_stable-2026.2.json) | the 259 operations, extracted from the **Flask url_map** of the real application: path, method, unwrapped handler, file:line, policy rule(s) (joined through the operations of the policy rules), HEAD support |
[openapi_stable-2026.2.json](_rewrite/openapi_stable-2026.2.json) | OpenAPI 3.0.3, 125 paths, 259 operations. Request bodies, query strings and response bodies come from the schemas declared in the code (@validation.request_body_schema, request_query_schema, response_body_schema decorators) when they exist, otherwise from the api-ref |
[request-schemas_stable-2026.2.json](_rewrite/request-schemas_stable-2026.2.json) | the 164 JSON schemas of the keystone/*/schema.py modules: requests, query strings, and responses (*_response_body) |
[schema-selection_stable-2026.2.json](_rewrite/schema-selection_stable-2026.2.json) | which schema each handler applies (body, query, response) |
[api-ref_stable-2026.2.json](_rewrite/api-ref_stable-2026.2.json) | the 249 documented operations (api-ref/source/v3, v3-ext), parameters and examples |
[schema-mysql_stable-2026.2.sql](_rewrite/schema-mysql_stable-2026.2.sql), [schema-postgresql_stable-2026.2.sql](_rewrite/schema-postgresql_stable-2026.2.sql) | DDL of the 47 model tables |
[schema-sqlite-migrated_stable-2026.2.sql](_rewrite/schema-sqlite-migrated_stable-2026.2.sql), [schema-diff_stable-2026.2.md](_rewrite/schema-diff_stable-2026.2.md) | the schema produced by the real Alembic migrations, and its differences from the models (§ 5): here the model DDL is not a faithful contract |
[policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json), [policies_stable-2026.2.yaml](_rewrite/policies_stable-2026.2.yaml) | the 204 default rules: check strings, scope_types, deprecated rules, operations |
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf) | every option with its default (oslo-config-generator) |
[cases-fernet-tokens_stable-2026.2.json](_rewrite/cases-fernet-tokens_stable-2026.2.json) | Fernet token test vectors (§ 3.1) |
[cases-federation-mapping_stable-2026.2.json](_rewrite/cases-federation-mapping_stable-2026.2.json) | cases of the federation mapping engine (§ 3.4) |
[exchanges_stable-2026.2.json](_rewrite/exchanges_stable-2026.2.json) | HTTP exchanges recorded from the keystone/tests/unit/test_v3_*.py API tests (§ 7) |
[response-schemas_stable-2026.2.json](_rewrite/response-schemas_stable-2026.2.json), [response-schemas_stable-2026.2.md](_rewrite/response-schemas_stable-2026.2.md) | response schemas observed in those exchanges, to cross-check against the schemas declared in the code |
[observed-errors_stable-2026.2.md](_rewrite/observed-errors_stable-2026.2.md) | (operation, status, title, message) combinations observed |
[fixture-seeds_stable-2026.2.json](_rewrite/fixture-seeds_stable-2026.2.json) | per-test database seeds (rows, configuration and policy overrides) for the exchanges (§ 7) |
[replay-report_stable-2026.2.json](_rewrite/replay-report_stable-2026.2.json) | replay results of the cases against the Python implementation |
Production: rewrite_pack.py (Flask extraction, --flask-app, --schemas-modules, --import-packages), then conformance_capture.py with the Keystone adapter (http, calls, replay) of the generate-reverse-spec skill, in a venv with Keystone, its test requirements and python-ldap installed.
2. HTTP behaviour that the contracts do not show
- Pipeline order (
keystone/server/flask/core.py:39-142): ProxyFix → CORS → body size limit → HTTPProxyToWSGI → osprofiler → RequestId → URL normalisation (trailing/removed) →AuthContextMiddleware→ Flask (before_request: logging, JSON body check) → handler.X-Auth-Tokenis therefore decoded before routing: an invalid token sent to a non-existent route gives 401, not 404 [inferred:AuthContextMiddlewareorder]. - No microversions: version discovery is
GET /(300, the version list) andGET /v3, in JSON or JSON Home depending onAccept(keystone/api/discovery.py:44-65). keystoneauth reads them to find the v3 endpoint. - Error body:
{"error": {"code", "title", "message"}}, pluserror.identityfor MFA.WWW-Authenticate: Keystone uri="…"on every 401. Messages are masked unlessinsecure_debug(see errors). OAuth2 uses the RFC 6749 format. - Fixed headers:
X-Subject-Tokenon the response ofPOST/GET /v3/auth/tokens;Vary: X-Auth-Tokenon every response; noContent-Typeon a 204. - Lists: attribute filters with comparators (
__contains…);limit/markerpagination bounded bymax_db_limit;truncatedandlinks.next;links.selfon every entity (see REST API § 1). - Member methods on a collection (
DELETE /v3/policies…): 400, coming from aTypeError. A rewrite should answer 405 and list this difference among its known deviations.
3. Algorithms and formats a rewrite must reproduce exactly
3.1 Fernet token format (inter-version and inter-node contract)
A Go rewrite that shares the key repository with the Python service must produce and read the same bytes. The specification:
- Fernet (standard specification):
0x80 | timestamp (8 bytes, big-endian, seconds) | IV (16) | AES-128-CBC(PKCS7(plaintext)) | HMAC-SHA256 (32), then URL-safe base64. Keystone **removes the trailing=** and restores it before decoding (keystone/token/token_formatters.py:39-120). - Keys: each file of the repository is a 32-byte URL-safe base64 key (16 bytes for signing, 16 for encryption).
- Encryption: with the file of the highest number.
- Decryption: by trying every key (MultiFernet).
0is the staged key (keystone/common/fernet_utils.py:202-266).
- Plaintext =
msgpack.packb((version, *fields))(keystone/token/token_formatters.py:138-191), with the field order of each version in interactions § 2 (keystone/token/token_formatters.py:470-1160). - Field encodings:
- IDs: msgpack pair
[true, bin(16)]when the ID is a UUID (32 hex characters, without dashes, written back without dashes), otherwise[false, str]; methods: the sum of2^iover the indexesiof the methods in[auth]/methods, in the order of that option;expires_at: a float of seconds since the epoch;audit_ids: a list ofbin(16). An audit ID isbase64url(uuid4.bytes)without padding, 22 characters (keystone/token/provider.py:69-75). The last element of the list is the token's ownaudit_id, and the one before it, if any, is the parent's (audit_chain_id);system: the string"all".
- IDs: msgpack pair
- **
issued_at** is the Fernet timestamp, not a payload field. Both dates are rendered as ISO 8601 with microseconds andZ. - Old payloads: msgpack < 1.0 encoded some strings as
raw, so decoding must acceptbinwherestris expected (keystone/token/token_formatters.py:193-210).
Test vectors (cases-fernet-tokens_stable-2026.2.json): 227 tokens produced by the unit and API tests, up to 25 per version. Versions covered: all 11 (0 to 10): 25 vectors per version, except version 6 (20), 8 (12), 9 (19) and 10 (1). Each case gives the key repository, the [auth]/methods order, the version, the token and the decoded fields. Replay: 227/227 decoded to the same fields, on two runs. A rewrite must:
- decode every vector to the same fields;
- produce, with the same keys, tokens that Python decodes to the same fields. To test this, encrypt the expected fields, then decode with
keystone-manageor the replay.
JWS ([token]/provider = jws): ES256-signed JWT whose claims carry the same information (keystone/token/providers/jws/core.py). No vectors were captured; Fernet is the provider to support first.
3.2 Token validation (what keystonemiddleware relies on)
Steps of validate_token (keystone/token/provider.py:139-275):
- decrypt (else 404);
- expiry, plus
allow_expired_windowwhen?allow_expired=1; - revocation (§ 3.3);
- user still enabled (else revocation of the user, side effect);
- access rules of the application credential, if any, which require the
OpenStack-Identity-Access-Rulesheader ≥ 1.0; - rendering: roles recomputed at validation time, from the current assignments and implied roles, so a role removed after issuance disappears from the next validation; catalog filtered (OS-EP-FILTER, endpoint URL substitutions
$(project_id)s… throughkeystone/common/utils.py:418-451, else 500MalformedEndpoint).
Any failure → 404.
3.3 Revocation
- Event: a row of
revocation_eventwith the fieldsuser_id,project_id,domain_id,role_id,trust_id,consumer_id,access_token_id,audit_id,audit_chain_id,expires_at,issued_beforeandrevoked_at(keystone/models/revoke_model.py:24-66). - A token is revoked if an event exists with
issued_before >= token.issued_at, for which every non-null field matches the token. The rules are those ofmatches()(keystone/models/revoke_model.py:160-222): the user is compared with the user, the trustor or the trustee; the domain with the identity domain or the scope domain. - SQL pre-filter: see interactions § 3.
- Purge: on every write, of the events older than the lifetime of a token plus the expiry tolerance.
3.4 Federation mapping engine
- Input: the mapping rules (a JSON list of
{"local": [...], "remote": [...]}) and the assertion. The assertion is the set of attributes the web server put in the WSGI environment, filtered by[federation]/assertion_prefix. - Algorithm:
RuleProcessor.process(keystone/federation/utils.py:495-737), helpers inkeystone/federation/utils.py:738-977. For each rule, theremoteconditions must all hold, with the operatorstype,any_one_of,not_any_of,whitelist,blacklistandregex. The values are then injected intolocal({0},{1}… in the order ofremote). The results of the matching rules are merged into:user: id, name, domain,type(ephemeralorlocal);group_ids,group_names(with their domain);projects, with roles, for automatic project creation (mapping schema 2.0).
- Errors:
ValidationError(assertion without the required attributes, user absent from the mapping);DirectMappingError(500) when{n}refers to aremotethat does not exist. - Variants:
schema_version1.0, 2.0 and 3.0 choose the processor class (RuleProcessor,RuleProcessorToHonorDomainOption,RuleProcessorForProjectJsonAttribute,keystone/federation/utils.py:996-1130). - Cases (
cases-federation-mapping_stable-2026.2.json): 705 distinct cases recorded from 370 tests (test_v3_federation,contrib/federation/test_utils). Replay: 705/705. 13 of them are matching errors. Of the 692 others, about 520 to 530 are identical and about 165 to 172 are identical up to list order: the split changes from one run to the next.- Lesson from the replay:
group_idsandgroup_namescome out in an order that changes between runs, because they are built from sets. Compare them as sets. - These cases only exercise the base
RuleProcessorclass: no recorded test goes through the 2.0 or 3.0 variants. Add cases for them.
- Lesson from the replay:
3.5 Passwords and accounts
- Hashing: the algorithm is recognised from the hash prefix, so a rewrite must verify every format: scrypt, bcrypt, bcrypt_sha256, pbkdf2_sha512 (
keystone/common/password_hashing.py:33-60). It must also produce the format of[identity]/password_hash_algorithm. A password longer than[identity]/max_password_lengthgives 403PasswordVerificationError(keystone/common/password_hashing.py:112). - Account rules:
- lockout after
lockout_failure_attemptsfailures, forlockout_duration; - expiry after
password_expires_days; - history of
unique_last_password_countpasswords; minimum_password_age;change_password_upon_first_use;- inactivity is computed on read (
keystone/identity/backends/sql_model.py:217-238); - user options
ignore_lockout_failure_attempts,ignore_user_inactivity… (keystone/identity/backends/resource_options.py:73-90).
- lockout after
3.6 Effective roles
list_role_assignments(effective=True) (keystone/assignment/core.py:1068, keystone/assignment/core.py:772-1066) expands:
- groups (user memberships);
- inheritance (
OS-INHERIT: from a domain to its projects, from a project to its subtree,keystone/assignment/core.py:445); - implied roles (
add_implied_roles,keystone/assignment/core.py:692, transitively); - domain-specific roles: they are removed from the final result when they only serve as relays (
strip_domain_roles).
The token roles and GET /v3/role_assignments?effective depend on it: compare the rewrite with the exchanges of § 7 for the effective-list endpoints.
4. What the clients expect
| Expectation | Where the client relies on it |
|---|---|
validation through GET /v3/auth/tokens with X-Subject-Token, allow_expired and OpenStack-Identity-Access-Rules: 1 | keystonemiddleware:keystonemiddleware/auth_token/_identity.py:48-58, keystonemiddleware:keystonemiddleware/auth_token/_identity.py:21 |
404 = invalid token (InvalidToken, the user gets 401); 401 = the middleware's service token is rejected: one retry with a new token, then ServiceError (503 to the user) | keystonemiddleware:keystonemiddleware/auth_token/_identity.py:130-170 |
application credential access rules (service, path, method) checked by the middleware against the token's application_credential.access_rules | keystonemiddleware:keystonemiddleware/auth_token/__init__.py:548-580 |
the token ID is read from **X-Subject-Token** in the response, not from the body | keystoneauth:keystoneauth1/identity/v3/base.py:259, keystoneauth:keystoneauth1/access/access.py:45 |
discovery on / and /v3 (version document, links, media-types) | keystoneauth discovery [inferred: keystoneauth1/discover.py] |
token body: token.methods, user, project/domain/system, roles, catalog, expires_at, issued_at, audit_ids, is_domain, application_credential, OS-TRUST:trust | keystone/common/render_token.py:20; schema observed in response-schemas_stable-2026.2.json |
5. Database strategy
Recommendation: share the existing schema at the Alembic expand and contract heads of this branch, and leave the migrations to keystone-manage db_sync (Python) until the end of the migration.
- The model DDL is not a faithful contract here. The real migrations (
schema-sqlite-migrated_stable-2026.2.sql) differ from it (schema-diff_stable-2026.2.md):- a **
token** table is still present (legacy, unused since Fernet: Keystone writes nothing to it); assignment.role_id → role.idis a foreign key in the migrations but not in the models;limit.registered_limit_id,registered_limit.service_idandregistered_limit.region_idare foreign keys in the models but not in the migrations;project_tag (project_id, name)is unique in the migrations;- indexes differ (
access_token.consumer_id,endpoint.service_id,user_group_membership.group_idin the migrations;system_assignment.actor_idin the models). Write the rewrite against the migrated schema.
- a **
- IDs:
uuid4().hex(32 characters, no dashes) for most entities. They are fixed by the client when allowed (domains, regions…). EC2 credentials aresha256(access). LDAP and federated users go throughid_mapping(sha256). - No token table: tokens are stateless. Revocation is the only state, and it must be shared.
- Cascades: Keystone's code does them (05 § 1), not foreign keys. Keep them in the same transaction as the triggering request.
- Cache: optional, but if the Go and Python versions run together they must share the invalidation scheme (
<<<region>>>:keys in memcached, 03 § 4), or neither must cache tokens.
6. Authentication, policies, configuration and CLI
- Keystone is the authority: it does not use keystonemiddleware. Its
AuthContextMiddlewaredecodesX-Auth-Tokenlocally (same validation as § 3.2), and it also accepts tokenless authentication (mTLS) andREMOTE_USER(external). - Policies: 204 rules in the oslo.policy language, with
scope_typesand deprecated rules. Operators override them throughpolicy.yaml: the rewrite must parse that language (same need as Placement). - Configuration: the oslo.config INI format and option names of
config_stable-2026.2.conf. These are critical:[token],[fernet_tokens],[credential],[auth]/methods(encoding order!),[security_compliance],[identity],[federation],[cache]. - Key files: the same repository formats (Fernet tokens, credentials, receipts; JWS PEM). Rotation must stay compatible with
keystone-manage fernet_rotateif both versions coexist. - CLI:
keystone-manage bootstrap,db_sync,*_setup/*_rotate,credential_migrate,trust_flush,mapping_purge… (05 § 4). They can stay in Python during the transition, as long as the rewrite reads the same files and the same tables.
7. Conformance strategy
- Black box, language-independent: Tempest.
tempest:tempest/api/identitycontains 138 tests (admin and v3), run against the rewrite's URL in a devstack or kolla deployment. Addkeystone-tempest-plugin(cloned next to this repository) for federation and RBAC [inferred: not counted]. - Format vectors: the 227 Fernet tokens (§ 3.1) and the 705 mapping cases (§ 3.4). These are pure-function tests, without a database, to run in the rewrite's unit tests.
- Differential testing on HTTP:
exchanges_stable-2026.2.jsonholds 8,908 exchanges recorded from 1,679 Python API tests (16 modulestest_v3_*, 0 failures, 16 skipped), each with its test ID.- Seeds: the capture was re-run with
--seed-per-test(conformance_capture.py http): [fixture-seeds_stable-2026.2.json](_rewrite/fixture-seeds_stable-2026.2.json) gives, for each of the 1,489 tests that send requests, the database rows at its first request, the configuration options that differ from their defaults ([auth]/methodsorder included) and the policy overrides. The tests build their data through the Python API, so 276 exchanges (in 231 tests) also carry a **db_delta**: the rows inserted, updated or deleted outside HTTP since the previous request of the same test. The exchanges are grouped by test id (file= test id): one fresh database per test. - Replay: load the seed by natural key (as for Placement), apply each exchange's
db_deltabefore sending it, and bind the IDs and tokens the service generates (UUIDs in bodies,X-Subject-Token). Tests that freeze or move the clock (time_override) or depend on token expiry are expected to differ.
- Seeds: the capture was re-run with
- Response schemas: the rewrite's responses must validate against the schemas declared in the code (
request-schemas_stable-2026.2.json,*_response_bodykeys, attached to 81 operations of the OpenAPI document). For the others, use the observed schemas (response-schemas_stable-2026.2.json, 472 operation × status pairs, over 284 method + route keys (HEAD included) operations × status). - Errors: the 301 observed combinations (IDs templated, with or without dashes) (
observed-errors_stable-2026.2.md), especially the masked messages of 401 and 403.
8. Gaps found while building the pack
| Gap | Impact |
|---|---|
| model DDL ≠ migrations (§ 5) | use schema-sqlite-migrated_stable-2026.2.sql (and a MySQL migration for the exact types) as the reference |
| no gabbi suite: the tests build their data in Python | solved by the per-test seeds and db_delta of fixture-seeds_stable-2026.2.json; Tempest remains the black-box check |
| version 10 (OAuth2 mTLS) has a single Fernet vector, version 8 (system) has 12 | generate more from keystone/token/token_formatters.py before writing the Go decoder |
| no JWS vectors, no receipt vectors | provider to add after Fernet |
| mapping engine: schemas 2.0 and 3.0 not covered by the cases | add cases from keystone/tests/unit/mapping_fixtures.py |
| member methods registered on collections answer 400 | expected deviation (405) to document for clients |
LDAP: the test harness needs python-ldap; on macOS, the OPT_X_TLS_* options are missing (neutralised in the adapter) | none on the content; check the LDAP behaviour against a real directory |
9. Non-functional requirements
- Validation latency:
GET /v3/auth/tokensis called by every service on every request whose token is not in the middleware cache. It is the hot path, together with the revocation query and the role computation. - Stateless and horizontal: any number of instances, provided they share the database, the keys and (if enabled) the cache.
- Auditability: CADF notifications for the same events and with the same
event_types (03 § 5) if Ceilometer, Designate or a SIEM consume them. - Security: messages masked; lockout and password rules; strip the EC2 delegation fields (06, bug 2165281); constant-time comparison for secrets [inferred: to check in Python].
10. Rewrite checklist
- [ ] Routes and statuses from
routes_stable-2026.2.json; discovery/and/v3with JSON Home - [ ] Middleware order and error body (masking,
WWW-Authenticate,X-Subject-Token,Vary) - [ ] Fernet: decode the 227 vectors; encode tokens that Python decodes; MultiFernet keys and rotation
- [ ] Validation: expiry,
allow_expired, revocation, user enabled, access rules, catalog - [ ] Revocation events: same table, same matching, same purge
- [ ] The 705 mapping cases (as sets); schemas 2.0 and 3.0
- [ ] Passwords: verify every hash format; account rules
- [ ] Effective roles: groups, inheritance, implied roles, domain-specific roles
- [ ] Cascades of 05 § 1 in the same transaction
- [ ] 204 oslo.policy rules and the policy language; oslo.config options
- [ ] Migrated schema (not the model one); no new foreign key
- [ ] CADF notifications
- [ ] Tempest identity (138 tests) green; differential tests on the exchanges; responses valid against the declared schemas
- [ ] EC2/S3 security fix (06)
Source: reverse-spec-keystone/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.