@stigmer/server 3.14.1 → 3.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot/compose.d.ts +8 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +98 -12
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/apikey/verifier.d.ts +8 -1
- package/dist/domain/apikey/verifier.d.ts.map +1 -1
- package/dist/domain/apikey/verifier.js +14 -2
- package/dist/domain/apikey/verifier.js.map +1 -1
- package/dist/domain/identityaccount/constants.d.ts +66 -0
- package/dist/domain/identityaccount/constants.d.ts.map +1 -0
- package/dist/domain/identityaccount/constants.js +153 -0
- package/dist/domain/identityaccount/constants.js.map +1 -0
- package/dist/domain/identityaccount/controller.d.ts +34 -0
- package/dist/domain/identityaccount/controller.d.ts.map +1 -0
- package/dist/domain/identityaccount/controller.js +342 -0
- package/dist/domain/identityaccount/controller.js.map +1 -0
- package/dist/domain/identityaccount/operator.d.ts +14 -0
- package/dist/domain/identityaccount/operator.d.ts.map +1 -0
- package/dist/domain/identityaccount/operator.js +67 -0
- package/dist/domain/identityaccount/operator.js.map +1 -0
- package/dist/domain/identityaccount/provisioning.d.ts +52 -0
- package/dist/domain/identityaccount/provisioning.d.ts.map +1 -0
- package/dist/domain/identityaccount/provisioning.js +114 -0
- package/dist/domain/identityaccount/provisioning.js.map +1 -0
- package/dist/domain/identityaccount/resolve.d.ts +36 -0
- package/dist/domain/identityaccount/resolve.d.ts.map +1 -0
- package/dist/domain/identityaccount/resolve.js +12 -0
- package/dist/domain/identityaccount/resolve.js.map +1 -0
- package/dist/domain/identityaccount/resource-store.d.ts +4 -0
- package/dist/domain/identityaccount/resource-store.d.ts.map +1 -0
- package/dist/domain/identityaccount/resource-store.js +131 -0
- package/dist/domain/identityaccount/resource-store.js.map +1 -0
- package/dist/domain/identityaccount/steps.d.ts +79 -0
- package/dist/domain/identityaccount/steps.d.ts.map +1 -0
- package/dist/domain/identityaccount/steps.js +291 -0
- package/dist/domain/identityaccount/steps.js.map +1 -0
- package/dist/domain/identityaccount/store-contract.d.ts +22 -0
- package/dist/domain/identityaccount/store-contract.d.ts.map +1 -0
- package/dist/domain/identityaccount/store-contract.js +307 -0
- package/dist/domain/identityaccount/store-contract.js.map +1 -0
- package/dist/domain/identityaccount/store.d.ts +66 -0
- package/dist/domain/identityaccount/store.d.ts.map +1 -0
- package/dist/domain/identityaccount/store.js +13 -0
- package/dist/domain/identityaccount/store.js.map +1 -0
- package/dist/extensions/drivers.d.ts +26 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/gate-slots.d.ts +16 -1
- package/dist/extensions/gate-slots.d.ts.map +1 -1
- package/dist/extensions/gate-slots.js +1 -0
- package/dist/extensions/gate-slots.js.map +1 -1
- package/dist/extensions/identity-federation.d.ts +36 -0
- package/dist/extensions/identity-federation.d.ts.map +1 -0
- package/dist/extensions/identity-federation.js +2 -0
- package/dist/extensions/identity-federation.js.map +1 -0
- package/dist/extensions/registry.d.ts +18 -1
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +20 -0
- package/dist/extensions/registry.js.map +1 -1
- package/dist/identity/oidc-discovery.d.ts +36 -0
- package/dist/identity/oidc-discovery.d.ts.map +1 -0
- package/dist/identity/oidc-discovery.js +60 -0
- package/dist/identity/oidc-discovery.js.map +1 -0
- package/dist/identity/oidc-userinfo.d.ts +30 -0
- package/dist/identity/oidc-userinfo.d.ts.map +1 -0
- package/dist/identity/oidc-userinfo.js +28 -0
- package/dist/identity/oidc-userinfo.js.map +1 -0
- package/dist/identity/oidc-verifier.d.ts +6 -0
- package/dist/identity/oidc-verifier.d.ts.map +1 -1
- package/dist/identity/oidc-verifier.js +35 -34
- package/dist/identity/oidc-verifier.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/interceptors/auth.d.ts +10 -0
- package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
- package/dist/pipeline/interceptors/auth.js +9 -1
- package/dist/pipeline/interceptors/auth.js.map +1 -1
- package/package.json +4 -4
- package/src/boot/__tests__/auth-enabled-composition.test.ts +226 -2
- package/src/boot/compose.ts +118 -13
- package/src/domain/apikey/__tests__/verifier.test.ts +146 -22
- package/src/domain/apikey/verifier.ts +24 -2
- package/src/domain/identityaccount/__tests__/constants.test.ts +155 -0
- package/src/domain/identityaccount/__tests__/identityaccount.test.ts +484 -0
- package/src/domain/identityaccount/__tests__/operator.test.ts +180 -0
- package/src/domain/identityaccount/__tests__/provisioning.test.ts +281 -0
- package/src/domain/identityaccount/__tests__/resolve.test.ts +114 -0
- package/src/domain/identityaccount/__tests__/resource-store.test.ts +216 -0
- package/src/domain/identityaccount/__tests__/support.ts +58 -0
- package/src/domain/identityaccount/constants.ts +178 -0
- package/src/domain/identityaccount/controller.ts +667 -0
- package/src/domain/identityaccount/operator.ts +94 -0
- package/src/domain/identityaccount/provisioning.ts +182 -0
- package/src/domain/identityaccount/resolve.ts +52 -0
- package/src/domain/identityaccount/resource-store.ts +157 -0
- package/src/domain/identityaccount/steps.ts +377 -0
- package/src/domain/identityaccount/store-contract.ts +471 -0
- package/src/domain/identityaccount/store.ts +70 -0
- package/src/extensions/__tests__/identity-account-composed.test.ts +803 -0
- package/src/extensions/__tests__/identity-account-points.test.ts +129 -0
- package/src/extensions/__tests__/registry.test.ts +4 -0
- package/src/extensions/__tests__/tier-truthfulness.test.ts +299 -0
- package/src/extensions/drivers.ts +26 -0
- package/src/extensions/gate-slots.ts +16 -0
- package/src/extensions/identity-federation.ts +58 -0
- package/src/extensions/registry.ts +44 -1
- package/src/identity/__tests__/oidc-discovery.test.ts +122 -0
- package/src/identity/__tests__/oidc-userinfo.test.ts +103 -0
- package/src/identity/__tests__/oidc-verifier.test.ts +173 -3
- package/src/identity/oidc-discovery.ts +84 -0
- package/src/identity/oidc-userinfo.ts +64 -0
- package/src/identity/oidc-verifier.ts +42 -45
- package/src/index.ts +19 -0
- package/src/pipeline/interceptors/auth.ts +13 -1
- package/src/pipeline/steps/__tests__/authorize-annotation-completeness.test.ts +184 -0
|
@@ -18,27 +18,43 @@
|
|
|
18
18
|
* - everything else → "invalid token"
|
|
19
19
|
*
|
|
20
20
|
* Discovery and key handling: the issuer's /.well-known/openid-configuration
|
|
21
|
-
* is
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* is read through the shared oidc-discovery module the composition root
|
|
22
|
+
* builds once for every lane (20260911.11 A8 — the userinfo client reads
|
|
23
|
+
* the same document, so one issuer is discovered once and validated the
|
|
24
|
+
* same way wherever it is consumed: `issuer` must match exactly, RFC 8414
|
|
25
|
+
* §3.3; `jwks_uri` required). The module memoizes on success only, so a
|
|
26
|
+
* flaky IdP at boot never permanently bricks the lane. The JWKS client
|
|
27
|
+
* (jose's createRemoteJWKSet — cached, rotation-aware) is built ONCE per
|
|
28
|
+
* verifier from the discovered location and kept: discovery hands back a
|
|
29
|
+
* URI, and a client rebuilt per request would refetch the keys per
|
|
30
|
+
* request. Discovery/JWKS OUTAGES are infrastructure faults — thrown as
|
|
26
31
|
* plain errors so the chassis maps them to INTERNAL, never a credential
|
|
27
32
|
* rejection (the DD-007 unavailable doctrine).
|
|
28
33
|
*
|
|
29
|
-
* Identity
|
|
30
|
-
* the
|
|
31
|
-
*
|
|
32
|
-
*
|
|
34
|
+
* Identity (20260911.11 Q-IA-2, A1 — the cloud's direct-login posture):
|
|
35
|
+
* after the token verifies, `sub` (rejected as invalid when absent) is
|
|
36
|
+
* resolved through the identity-account domain (identityIdForSubject):
|
|
37
|
+
* a subject with a direct account stamps the ACCOUNT id; a subject
|
|
38
|
+
* without one is admitted idp-shaped (identityId = sub) so whoAmI can
|
|
39
|
+
* answer NOT_FOUND and provisionMyAccount can run. One primary-key read
|
|
40
|
+
* per request, no cache; a store fault is an infrastructure fault. The
|
|
41
|
+
* issuer is the configured issuer; email/displayName come from the
|
|
42
|
+
* standard `email`/`name` claims when present (the DD-007 Q5 addendum
|
|
43
|
+
* added the fields for exactly these claims), whichever way the subject
|
|
44
|
+
* resolved. Claim-or-pass runs before any of this: a non-JWT token
|
|
45
|
+
* passes with no network.
|
|
33
46
|
*/
|
|
34
47
|
import { createRemoteJWKSet, jwtVerify, errors as joseErrors } from "jose";
|
|
35
48
|
import type { JWTPayload } from "jose";
|
|
36
49
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
37
50
|
|
|
51
|
+
import { identityIdForSubject } from "../domain/identityaccount/resolve.js";
|
|
52
|
+
import type { AccountsBySubject } from "../domain/identityaccount/resolve.js";
|
|
38
53
|
import type {
|
|
39
54
|
CallerIdentity,
|
|
40
55
|
IdentityVerifier,
|
|
41
56
|
} from "../extensions/identity.js";
|
|
57
|
+
import type { IssuerDiscovery } from "./oidc-discovery.js";
|
|
42
58
|
|
|
43
59
|
/** Java classifyAuthError arms — byte-pinned cross-edition copy. */
|
|
44
60
|
export const TOKEN_EXPIRED_MESSAGE = "token has expired";
|
|
@@ -52,42 +68,28 @@ export interface OidcVerifierConfig {
|
|
|
52
68
|
readonly issuer: string;
|
|
53
69
|
/** The audience access tokens must carry. */
|
|
54
70
|
readonly audience: string;
|
|
71
|
+
/** The identity-account domain's subject lookup — REQUIRED: what identityId means depends on it. */
|
|
72
|
+
readonly accounts: AccountsBySubject;
|
|
73
|
+
/** The composition root's one discovery for every lane (A8). */
|
|
74
|
+
readonly discovery: IssuerDiscovery;
|
|
55
75
|
}
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
readonly jwks: ReturnType<typeof createRemoteJWKSet>;
|
|
59
|
-
}
|
|
77
|
+
type RemoteJwks = ReturnType<typeof createRemoteJWKSet>;
|
|
60
78
|
|
|
61
79
|
export function newOidcIdentityVerifier(
|
|
62
80
|
config: OidcVerifierConfig,
|
|
63
81
|
): IdentityVerifier {
|
|
64
|
-
|
|
82
|
+
// Set on the first successful discovery only: a failed discovery leaves
|
|
83
|
+
// it unset so the next request retries (the module caches no failure).
|
|
84
|
+
let jwks: RemoteJwks | undefined;
|
|
65
85
|
|
|
66
|
-
async function
|
|
67
|
-
if (
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
const url = discoveryUrl(config.issuer);
|
|
71
|
-
const response = await fetch(url);
|
|
72
|
-
if (!response.ok) {
|
|
73
|
-
throw new Error(
|
|
74
|
-
`OIDC discovery failed: ${url} answered HTTP ${response.status}`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
const document = (await response.json()) as {
|
|
78
|
-
issuer?: string;
|
|
79
|
-
jwks_uri?: string;
|
|
80
|
-
};
|
|
81
|
-
if (document.issuer !== config.issuer) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
`OIDC discovery failed: document issuer '${document.issuer ?? ""}' does not match configured issuer '${config.issuer}'`,
|
|
84
|
-
);
|
|
86
|
+
async function jwksFor(): Promise<RemoteJwks> {
|
|
87
|
+
if (jwks !== undefined) {
|
|
88
|
+
return jwks;
|
|
85
89
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
discovered = { jwks: createRemoteJWKSet(new URL(document.jwks_uri)) };
|
|
90
|
-
return discovered;
|
|
90
|
+
const { jwksUri } = await config.discovery.discover(config.issuer);
|
|
91
|
+
jwks = createRemoteJWKSet(new URL(jwksUri));
|
|
92
|
+
return jwks;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
return {
|
|
@@ -96,10 +98,10 @@ export function newOidcIdentityVerifier(
|
|
|
96
98
|
if (!isJwtShaped(token)) {
|
|
97
99
|
return null;
|
|
98
100
|
}
|
|
99
|
-
const
|
|
101
|
+
const keys = await jwksFor();
|
|
100
102
|
let payload: JWTPayload;
|
|
101
103
|
try {
|
|
102
|
-
({ payload } = await jwtVerify(token,
|
|
104
|
+
({ payload } = await jwtVerify(token, keys, {
|
|
103
105
|
issuer: config.issuer,
|
|
104
106
|
audience: config.audience,
|
|
105
107
|
}));
|
|
@@ -110,7 +112,7 @@ export function newOidcIdentityVerifier(
|
|
|
110
112
|
throw new ConnectError(INVALID_TOKEN_MESSAGE, Code.Unauthenticated);
|
|
111
113
|
}
|
|
112
114
|
return {
|
|
113
|
-
identityId: payload.sub,
|
|
115
|
+
identityId: await identityIdForSubject(config.accounts, payload.sub),
|
|
114
116
|
callerClass: "user",
|
|
115
117
|
issuer: config.issuer,
|
|
116
118
|
rawToken: token,
|
|
@@ -125,11 +127,6 @@ export function newOidcIdentityVerifier(
|
|
|
125
127
|
};
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
/** issuer + /.well-known/openid-configuration (trailing-slash tolerant). */
|
|
129
|
-
function discoveryUrl(issuer: string): string {
|
|
130
|
-
return `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
130
|
/** Three non-empty dot-separated segments — the JWS compact shape. */
|
|
134
131
|
function isJwtShaped(token: string): boolean {
|
|
135
132
|
const segments = token.split(".");
|
package/src/index.ts
CHANGED
|
@@ -109,6 +109,25 @@ export type {
|
|
|
109
109
|
} from "./extensions/resource-authorization.js";
|
|
110
110
|
export type { OrganizationDirectory } from "./extensions/organization-directory.js";
|
|
111
111
|
export { ALL_ORGANIZATIONS } from "./extensions/organization-directory.js";
|
|
112
|
+
// The 20260911.11 identity-account seams (Q-IA-9): the store PORT a
|
|
113
|
+
// composition drives the domain through (drivers.identityAccountStore;
|
|
114
|
+
// a driver throws DuplicateAccountError for a held id), the federation
|
|
115
|
+
// capability (drivers.identityFederation), and the ONE subject →
|
|
116
|
+
// identityId rule both OSS verifiers already apply, exported so a
|
|
117
|
+
// composition's own verifier converges on it instead of restating it.
|
|
118
|
+
export type { IdentityAccountStore } from "./domain/identityaccount/store.js";
|
|
119
|
+
export { DuplicateAccountError } from "./domain/identityaccount/store.js";
|
|
120
|
+
// The port's contract as a vitest-free kit: a driver's own test iterates
|
|
121
|
+
// the same cases the OSS adapter passes, so the port is proven per
|
|
122
|
+
// driver, never restated per repository.
|
|
123
|
+
export type {
|
|
124
|
+
IdentityAccountStoreContractCase,
|
|
125
|
+
IdentityAccountStoreContractFixture,
|
|
126
|
+
} from "./domain/identityaccount/store-contract.js";
|
|
127
|
+
export { identityAccountStoreContract } from "./domain/identityaccount/store-contract.js";
|
|
128
|
+
export type { IdentityFederation } from "./extensions/identity-federation.js";
|
|
129
|
+
export type { AccountsBySubject } from "./domain/identityaccount/resolve.js";
|
|
130
|
+
export { identityIdForSubject } from "./domain/identityaccount/resolve.js";
|
|
112
131
|
export type {
|
|
113
132
|
ListReadScope,
|
|
114
133
|
ListEntryMeta,
|
|
@@ -172,7 +172,19 @@ export function callerIdentityOf(ctx: HandlerContext): CallerIdentity {
|
|
|
172
172
|
* operator seam remains process-global.
|
|
173
173
|
*/
|
|
174
174
|
export function trustedLocalIdentity(): CallerIdentity {
|
|
175
|
-
|
|
175
|
+
return trustedLocalIdentityFor(operatorIdentitySnapshot());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The trusted-local identity for a given operator — the one construction
|
|
180
|
+
* of that principal, shared by the per-request stamp above and by the
|
|
181
|
+
* boot-time operator account (domain/identityaccount/operator.ts), so the
|
|
182
|
+
* row the server ensures and the caller it stamps can never disagree.
|
|
183
|
+
*/
|
|
184
|
+
export function trustedLocalIdentityFor(operator: {
|
|
185
|
+
readonly email: string;
|
|
186
|
+
readonly displayName: string;
|
|
187
|
+
}): CallerIdentity {
|
|
176
188
|
if (operator.email === "") {
|
|
177
189
|
return {
|
|
178
190
|
identityId: "system",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The annotation-completeness invariant for the Authorize step (stigmer#1073;
|
|
3
|
+
* found on the 20260911.11 cloud readout, 2026-09-13): an `rpc.config`
|
|
4
|
+
* annotation that names a resource KIND must also name where the resource
|
|
5
|
+
* ID comes from (`field_path` or `resource_id`). A kind without an id is a
|
|
6
|
+
* check against `<kind>:` with an empty id — the step resolves it to "" by
|
|
7
|
+
* doctrine (resolution never throws, see authorize.ts) and an enforcing
|
|
8
|
+
* authorizer refuses it for EVERY caller, while the open-source permissive
|
|
9
|
+
* default allows it, so no `local*` conformance target can see the gap.
|
|
10
|
+
* `getActorInfo` shipped that way: green on every OSS gate, denied on the
|
|
11
|
+
* cloud composition the moment the annotation-driven step replaced the
|
|
12
|
+
* cloud's hand-written handler (which had passed `id.value` by hand).
|
|
13
|
+
*
|
|
14
|
+
* Scope is every method the open-source server SERVES — the empty
|
|
15
|
+
* composition's `routes` replayed into a recording router, the
|
|
16
|
+
* tier-truthfulness precedent — because that is exactly the set the OSS
|
|
17
|
+
* Authorize step governs; a composition's own services are its own
|
|
18
|
+
* `direct-handler-authorization` conformance suite's business. Two arms are
|
|
19
|
+
* exempt by construction: `is_public` and `is_skip_authorization` methods
|
|
20
|
+
* never reach the annotation's resolution (the skip lanes resolve their
|
|
21
|
+
* target in the handler through `authorizeDirect`'s override). A config
|
|
22
|
+
* that names NO kind (static or by path) is outside this invariant: the
|
|
23
|
+
* check is then about the kind, not the id, and today no OSS-served method
|
|
24
|
+
* is shaped that way (the IamPolicy RPCs that are belong to
|
|
25
|
+
* `sp.oss-organization-roles`).
|
|
26
|
+
*
|
|
27
|
+
* The mutation arms prove the check bites over the same function.
|
|
28
|
+
*/
|
|
29
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
30
|
+
import { tmpdir } from "node:os";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
|
|
33
|
+
import { getOption, hasOption } from "@bufbuild/protobuf";
|
|
34
|
+
import type { DescService } from "@bufbuild/protobuf";
|
|
35
|
+
import type { ConnectRouter } from "@connectrpc/connect";
|
|
36
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
37
|
+
|
|
38
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
39
|
+
import {
|
|
40
|
+
config as rpcAuthorizationConfig,
|
|
41
|
+
is_public,
|
|
42
|
+
is_skip_authorization,
|
|
43
|
+
} from "@stigmer/protos/ai/stigmer/commons/rpc/method_options_pb";
|
|
44
|
+
|
|
45
|
+
import { loadConfig } from "../../../boot/config.js";
|
|
46
|
+
import { composeServer } from "../../../boot/compose.js";
|
|
47
|
+
import type { ComposedServer } from "../../../boot/compose.js";
|
|
48
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
49
|
+
|
|
50
|
+
/** One annotated method as the invariant reads it. */
|
|
51
|
+
interface AnnotationTarget {
|
|
52
|
+
readonly method: string;
|
|
53
|
+
readonly namesKind: boolean;
|
|
54
|
+
readonly namesId: boolean;
|
|
55
|
+
readonly skipsResolution: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Replays the composed routes into a recorder and returns the served services. */
|
|
59
|
+
function servedServices(
|
|
60
|
+
routes: (router: ConnectRouter) => void,
|
|
61
|
+
): DescService[] {
|
|
62
|
+
const served: DescService[] = [];
|
|
63
|
+
const recorder = {
|
|
64
|
+
handlers: [],
|
|
65
|
+
service(desc: DescService) {
|
|
66
|
+
served.push(desc);
|
|
67
|
+
return recorder;
|
|
68
|
+
},
|
|
69
|
+
rpc() {
|
|
70
|
+
return recorder;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
routes(recorder as unknown as ConnectRouter);
|
|
74
|
+
return served;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Every `rpc.config`-annotated method of the given services, read once. */
|
|
78
|
+
function annotationTargets(services: DescService[]): AnnotationTarget[] {
|
|
79
|
+
const targets: AnnotationTarget[] = [];
|
|
80
|
+
for (const service of services) {
|
|
81
|
+
for (const method of service.methods) {
|
|
82
|
+
if (!hasOption(method, rpcAuthorizationConfig)) continue;
|
|
83
|
+
const config = getOption(method, rpcAuthorizationConfig);
|
|
84
|
+
targets.push({
|
|
85
|
+
method: `${service.typeName}/${method.name}`,
|
|
86
|
+
namesKind:
|
|
87
|
+
config.resourceKind !== ApiResourceKind.api_resource_kind_unknown ||
|
|
88
|
+
config.resourceKindPath !== "",
|
|
89
|
+
namesId: config.fieldPath !== "" || config.resourceId !== "",
|
|
90
|
+
skipsResolution:
|
|
91
|
+
getOption(method, is_public) ||
|
|
92
|
+
getOption(method, is_skip_authorization),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return targets;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The methods whose annotation names a kind but no id, one sentence each. */
|
|
100
|
+
function unresolvableTargets(targets: AnnotationTarget[]): string[] {
|
|
101
|
+
return targets
|
|
102
|
+
.filter((t) => !t.skipsResolution && t.namesKind && !t.namesId)
|
|
103
|
+
.map(
|
|
104
|
+
(t) =>
|
|
105
|
+
`${t.method} names a resource kind but neither field_path nor resource_id — the Authorize step would check an empty id`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describe("authorization annotations name where the resource id comes from", () => {
|
|
110
|
+
let dir: string;
|
|
111
|
+
let server: ComposedServer;
|
|
112
|
+
|
|
113
|
+
beforeAll(async () => {
|
|
114
|
+
dir = mkdtempSync(path.join(tmpdir(), "authorize-annotations-test-"));
|
|
115
|
+
server = await composeServer({
|
|
116
|
+
config: loadConfig({
|
|
117
|
+
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
118
|
+
TEMPORAL_HOST_PORT: "127.0.0.1:1",
|
|
119
|
+
DB_PATH: path.join(dir, "stigmer.db"),
|
|
120
|
+
STORAGE_PATH: path.join(dir, "storage"),
|
|
121
|
+
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
122
|
+
}),
|
|
123
|
+
logger: createLogger({ level: "error", pretty: false, write: () => {} }),
|
|
124
|
+
portOverride: 0,
|
|
125
|
+
host: "127.0.0.1",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
afterAll(async () => {
|
|
130
|
+
await server.shutdown();
|
|
131
|
+
rmSync(dir, { recursive: true, force: true });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("every served method that names a kind also names its id (field_path or resource_id)", () => {
|
|
135
|
+
const targets = annotationTargets(servedServices(server.routes));
|
|
136
|
+
expect(targets.length).toBeGreaterThan(100);
|
|
137
|
+
expect(unresolvableTargets(targets)).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("getActorInfo is annotated exactly like get — the arm stigmer#1073 was missing", () => {
|
|
141
|
+
const targets = annotationTargets(servedServices(server.routes));
|
|
142
|
+
const byName = new Map(targets.map((t) => [t.method, t]));
|
|
143
|
+
const get = byName.get(
|
|
144
|
+
"ai.stigmer.iam.identityaccount.v1.IdentityAccountQueryController/get",
|
|
145
|
+
);
|
|
146
|
+
const actor = byName.get(
|
|
147
|
+
"ai.stigmer.iam.identityaccount.v1.IdentityAccountQueryController/getActorInfo",
|
|
148
|
+
);
|
|
149
|
+
expect(get?.namesId, "get names its id").toBe(true);
|
|
150
|
+
expect(actor?.namesId, "getActorInfo names its id").toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("the check bites (mutation proofs over the same function)", () => {
|
|
155
|
+
const shaped = (overrides: Partial<AnnotationTarget>): AnnotationTarget => ({
|
|
156
|
+
method: "x.v1.Query/probe",
|
|
157
|
+
namesKind: true,
|
|
158
|
+
namesId: true,
|
|
159
|
+
skipsResolution: false,
|
|
160
|
+
...overrides,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("a kind with no id is reported, by method name", () => {
|
|
164
|
+
expect(unresolvableTargets([shaped({ namesId: false })])).toEqual([
|
|
165
|
+
"x.v1.Query/probe names a resource kind but neither field_path nor resource_id — the Authorize step would check an empty id",
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("a kind with an id is not", () => {
|
|
170
|
+
expect(unresolvableTargets([shaped({})])).toEqual([]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("a skip or public method is exempt — its target is the handler's, not the annotation's", () => {
|
|
174
|
+
expect(
|
|
175
|
+
unresolvableTargets([shaped({ namesId: false, skipsResolution: true })]),
|
|
176
|
+
).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("a config that names no kind is outside this invariant", () => {
|
|
180
|
+
expect(
|
|
181
|
+
unresolvableTargets([shaped({ namesKind: false, namesId: false })]),
|
|
182
|
+
).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
});
|