swallowkit 1.0.0-beta.47 → 1.0.0-beta.48
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/cli/commands/add-auth.d.ts +1 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +136 -1
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/dev.js +5 -2
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +9 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +56 -12
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +7 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +81 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.js +2 -2
- package/dist/core/mock/connector-mock-server.js.map +1 -1
- package/dist/core/project/manifest.js +2 -2
- package/dist/core/project/manifest.js.map +1 -1
- package/dist/core/project/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +17 -0
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts +9 -2
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +230 -13
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.js +4 -2
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +4 -2
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +8 -3
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +3 -0
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +12 -8
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/types/index.d.ts +34 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +1 -0
- package/dist/utils/package-manager.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/auth.test.ts +4 -0
- package/src/__tests__/config.test.ts +22 -1
- package/src/__tests__/mcp.test.ts +2 -2
- package/src/__tests__/multi-auth.test.ts +133 -0
- package/src/cli/commands/add-auth.ts +112 -2
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/commands/scaffold.ts +58 -14
- package/src/cli/index.ts +2 -0
- package/src/core/config.ts +69 -2
- package/src/core/mock/connector-mock-server.ts +2 -2
- package/src/core/project/manifest.ts +3 -3
- package/src/core/project/validation.ts +16 -0
- package/src/core/scaffold/auth-generator.ts +236 -14
- package/src/core/scaffold/connector-functions-generator.ts +4 -2
- package/src/core/scaffold/functions-generator.ts +4 -2
- package/src/core/scaffold/model-parser.ts +9 -3
- package/src/core/scaffold/ui-generator.ts +15 -8
- package/src/types/index.ts +35 -4
- package/src/utils/package-manager.ts +1 -0
package/src/core/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import { createRequire } from "module";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { AuthConfig, BackendLanguage, ConnectorDefinition, SwallowKitConfig } from "../types";
|
|
4
|
+
import { AuthConfig, AuthProvider, BackendLanguage, ConnectorDefinition, NormalizedAuthConfig, SwallowKitConfig } from "../types";
|
|
5
5
|
import { detectFromProject, getCommands } from "../utils/package-manager";
|
|
6
6
|
|
|
7
7
|
const requireFromHere = createRequire(__filename);
|
|
@@ -37,7 +37,7 @@ const DEFAULT_CONFIG: SwallowKitConfig = {
|
|
|
37
37
|
/**
|
|
38
38
|
* 設定ファイルを読み込み
|
|
39
39
|
*/
|
|
40
|
-
export function loadConfig(configPath?: string): SwallowKitConfig {
|
|
40
|
+
export function loadConfig(configPath?: string, throwOnError = false): SwallowKitConfig {
|
|
41
41
|
const defaultPaths = [
|
|
42
42
|
"swallowkit.config.json",
|
|
43
43
|
"swallowkit.config.js",
|
|
@@ -63,6 +63,7 @@ export function loadConfig(configPath?: string): SwallowKitConfig {
|
|
|
63
63
|
return mergeConfig(DEFAULT_CONFIG, unwrapConfigModule(loadedConfig));
|
|
64
64
|
}
|
|
65
65
|
} catch (error) {
|
|
66
|
+
if (throwOnError) throw error;
|
|
66
67
|
console.warn(`⚠️ 設定ファイルの読み込みに失敗: ${filePath}`, error);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
@@ -200,6 +201,40 @@ export function getAuthConfig(configPath?: string): AuthConfig | undefined {
|
|
|
200
201
|
return config.auth;
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
/** Load configuration for mutating/generation commands and fail instead of silently falling back. */
|
|
205
|
+
export function getValidatedFullConfig(configPath?: string): SwallowKitConfig {
|
|
206
|
+
const config = mergeConfig(loadConfig(configPath, true), loadConfigFromEnv());
|
|
207
|
+
const validation = validateConfig(config);
|
|
208
|
+
if (!validation.valid) throw new Error(`Invalid SwallowKit configuration:\n${validation.errors.map(error => `- ${error}`).join("\n")}`);
|
|
209
|
+
return config;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const VALID_AUTH_PROVIDERS: AuthProvider[] = ["custom-jwt", "swa", "external-token", "swa-custom", "none"];
|
|
213
|
+
const AUTH_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
214
|
+
|
|
215
|
+
/** Convert the legacy single-provider shape to the canonical named-scheme shape. */
|
|
216
|
+
export function normalizeAuthConfig(auth?: AuthConfig): NormalizedAuthConfig | undefined {
|
|
217
|
+
if (!auth) return undefined;
|
|
218
|
+
const schemes = { ...(auth.schemes ?? {}) };
|
|
219
|
+
if (auth.provider && auth.provider !== "none" && !schemes.default) {
|
|
220
|
+
schemes.default = { provider: auth.provider, customJwt: auth.customJwt, swa: auth.swa };
|
|
221
|
+
}
|
|
222
|
+
const rawDefault = auth.authorization?.defaultPolicy ?? (Object.keys(schemes).length ? "authenticated" : "anonymous");
|
|
223
|
+
return {
|
|
224
|
+
...auth,
|
|
225
|
+
schemes,
|
|
226
|
+
authorization: {
|
|
227
|
+
...auth.authorization,
|
|
228
|
+
defaultPolicy: rawDefault === "public" ? "anonymous" : rawDefault,
|
|
229
|
+
policies: { ...(auth.authorization?.policies ?? {}) },
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function getNormalizedAuthConfig(configPath?: string): NormalizedAuthConfig | undefined {
|
|
235
|
+
return normalizeAuthConfig(getAuthConfig(configPath));
|
|
236
|
+
}
|
|
237
|
+
|
|
203
238
|
/**
|
|
204
239
|
* 設定の検証
|
|
205
240
|
*/
|
|
@@ -214,6 +249,38 @@ function hasConfiguredDatabase(config: SwallowKitConfig): boolean {
|
|
|
214
249
|
export function validateConfig(config: SwallowKitConfig): { valid: boolean; errors: string[] } {
|
|
215
250
|
const errors: string[] = [];
|
|
216
251
|
|
|
252
|
+
const auth = normalizeAuthConfig(config.auth);
|
|
253
|
+
if (auth) {
|
|
254
|
+
for (const [name, scheme] of Object.entries(auth.schemes)) {
|
|
255
|
+
const base = `auth.schemes.${name}`;
|
|
256
|
+
if (!AUTH_NAME.test(name)) errors.push(`${base}: scheme name must match ${AUTH_NAME}; rename the scheme`);
|
|
257
|
+
if (!VALID_AUTH_PROVIDERS.includes(scheme.provider)) errors.push(`${base}.provider: unsupported provider '${scheme.provider}'; use ${VALID_AUTH_PROVIDERS.join(", ")}`);
|
|
258
|
+
if (scheme.provider === "none" || scheme.provider === "swa-custom") errors.push(`${base}.provider: '${scheme.provider}' cannot be used as a named scheme; use swa, external-token, or custom-jwt`);
|
|
259
|
+
if (scheme.provider === "custom-jwt") {
|
|
260
|
+
const jwt = scheme.customJwt ?? (name === "default" ? auth.customJwt : undefined);
|
|
261
|
+
for (const key of ["userConnector", "userTable", "loginIdColumn", "passwordHashColumn", "rolesColumn"] as const) {
|
|
262
|
+
if (!jwt?.[key]) errors.push(`${base}.customJwt.${key}: required for custom-jwt; configure this value`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const [name, policy] of Object.entries(auth.authorization.policies)) {
|
|
267
|
+
const base = `auth.authorization.policies.${name}`;
|
|
268
|
+
if (!AUTH_NAME.test(name)) errors.push(`${base}: policy name must match ${AUTH_NAME}; rename the policy`);
|
|
269
|
+
if (!Array.isArray(policy.schemes) || policy.schemes.length === 0) errors.push(`${base}.schemes: provide at least one named scheme`);
|
|
270
|
+
for (const scheme of policy.schemes ?? []) if (!auth.schemes[scheme]) errors.push(`${base}.schemes: undefined scheme '${scheme}'; define auth.schemes.${scheme}`);
|
|
271
|
+
const sources = new Map<string, string>();
|
|
272
|
+
for (const schemeName of policy.schemes ?? []) {
|
|
273
|
+
const provider = auth.schemes[schemeName]?.provider;
|
|
274
|
+
const source = provider === "swa" ? "x-ms-client-principal" : provider === "custom-jwt" || provider === "external-token" ? "authorization-bearer" : provider;
|
|
275
|
+
if (source && sources.has(source)) errors.push(`${base}.schemes: '${sources.get(source)}' and '${schemeName}' both use ${source}; split the policy so credential selection is deterministic`);
|
|
276
|
+
else if (source) sources.set(source, schemeName);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const defaultPolicy = auth.authorization.defaultPolicy;
|
|
280
|
+
if (defaultPolicy === "authenticated" && !auth.schemes.default && Object.keys(auth.schemes).length !== 1) errors.push("auth.authorization.defaultPolicy: 'authenticated' requires auth.schemes.default (or exactly one scheme); set a default scheme or use a named policy");
|
|
281
|
+
else if (!["anonymous", "authenticated"].includes(defaultPolicy) && !auth.authorization.policies[defaultPolicy]) errors.push(`auth.authorization.defaultPolicy: undefined policy '${defaultPolicy}'; define auth.authorization.policies.${defaultPolicy}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
217
284
|
// データベース設定の検証
|
|
218
285
|
if (hasConfiguredDatabase(config) && !config.database?.connectionString) {
|
|
219
286
|
errors.push("Cosmos DB connection string is required");
|
|
@@ -503,9 +503,9 @@ export class ConnectorMockServer {
|
|
|
503
503
|
if (!policy) return undefined; // defaultPolicy: authenticated → 認証のみ、ロールチェック不要
|
|
504
504
|
|
|
505
505
|
if (isWrite) {
|
|
506
|
-
return policy.write
|
|
506
|
+
return Array.isArray(policy.write) ? policy.write : policy.roles;
|
|
507
507
|
}
|
|
508
|
-
return policy.read
|
|
508
|
+
return Array.isArray(policy.read) ? policy.read : policy.roles;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
private readBody(req: http.IncomingMessage, callback: (body: MockDocument) => void) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { getBackendLanguage, getFullConfig, validateConfig } from "../config";
|
|
3
|
+
import { getBackendLanguage, getFullConfig, normalizeAuthConfig, validateConfig } from "../config";
|
|
4
4
|
import { getAllModels, toCamelCase, toKebabCase } from "../scaffold/model-parser";
|
|
5
5
|
import { AuthConfig, BackendLanguage, ConnectorDefinition, ModelAuthPolicy, ModelConnectorConfig } from "../../types";
|
|
6
6
|
import { captureConsoleMessages, withWorkingDirectory } from "../operations/runtime";
|
|
@@ -340,7 +340,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
|
|
|
340
340
|
connectors: Object.entries(config.connectors || {})
|
|
341
341
|
.map(([name, definition]) => ({ name, definition }))
|
|
342
342
|
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
343
|
-
auth: config.auth || null,
|
|
343
|
+
auth: normalizeAuthConfig(config.auth) || null,
|
|
344
344
|
entities: entities.sort((left, right) => left.name.localeCompare(right.name)),
|
|
345
345
|
routes,
|
|
346
346
|
modules: buildModules(projectRoot),
|
|
@@ -356,7 +356,7 @@ export async function buildProjectManifest(projectRoot: string = process.cwd()):
|
|
|
356
356
|
],
|
|
357
357
|
hasSharedWorkspace: fs.existsSync(path.join(projectRoot, "shared", "package.json")),
|
|
358
358
|
hasConnectors: Object.keys(config.connectors || {}).length > 0,
|
|
359
|
-
hasAuth: Boolean(config.auth && config.auth.provider !== "none"),
|
|
359
|
+
hasAuth: Boolean(config.auth && (config.auth.provider !== "none" || Object.keys(config.auth.schemes ?? {}).length > 0)),
|
|
360
360
|
},
|
|
361
361
|
};
|
|
362
362
|
|
|
@@ -209,6 +209,22 @@ export async function validateProject(projectRoot: string = process.cwd()): Prom
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
const namedPolicies = loaded.manifest.auth?.authorization?.policies ?? {};
|
|
213
|
+
for (const entity of loaded.manifest.entities) {
|
|
214
|
+
const policy = entity.authPolicy;
|
|
215
|
+
if (!policy) continue;
|
|
216
|
+
const references = [policy.policy, typeof policy.read === "string" ? policy.read : undefined, typeof policy.write === "string" ? policy.write : undefined].filter((value): value is string => Boolean(value));
|
|
217
|
+
for (const name of references) {
|
|
218
|
+
if (!namedPolicies[name]) pushViolation(violations, {
|
|
219
|
+
code: "undefined-auth-policy",
|
|
220
|
+
severity: "error",
|
|
221
|
+
message: `Model '${entity.name}' references undefined authorization policy '${name}'.`,
|
|
222
|
+
location: { path: entity.filePath, entity: entity.name },
|
|
223
|
+
suggestedFix: `Define auth.authorization.policies.${name} or update the model authPolicy reference.`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
212
228
|
for (const entity of loaded.manifest.entities) {
|
|
213
229
|
validateSchemaNaming(entity, violations);
|
|
214
230
|
}
|
|
@@ -3,10 +3,212 @@
|
|
|
3
3
|
* add-auth コマンドおよび scaffold ロールガード挿入で使用するテンプレート群
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { AuthProvider, CustomJwtConfig, ModelAuthPolicy, RdbConnectorConfig } from "../../types";
|
|
6
|
+
import { AuthProvider, CustomJwtConfig, ModelAuthPolicy, NormalizedAuthConfig, RdbConnectorConfig } from "../../types";
|
|
7
7
|
|
|
8
8
|
type RdbProvider = RdbConnectorConfig["provider"]; // "mysql" | "postgres" | "sqlserver"
|
|
9
9
|
|
|
10
|
+
export function generateNamedAuthRouterTS(config: NormalizedAuthConfig): string {
|
|
11
|
+
const external = Object.entries(config.schemes).filter(([, s]) => s.provider === "external-token");
|
|
12
|
+
const custom = Object.entries(config.schemes).filter(([, s]) => s.provider === "custom-jwt");
|
|
13
|
+
const imports = external.map(([name]) => {
|
|
14
|
+
const slug = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
15
|
+
const id = safeIdentifier(name);
|
|
16
|
+
return `import { verifyExternalToken as verify_${id}, ExternalTokenVerificationError as Error_${id} } from './schemes/${slug}/verifier';`;
|
|
17
|
+
}).concat(custom.map(([name]) => {
|
|
18
|
+
const slug = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
19
|
+
return `import { requireAuth as verify_${safeIdentifier(name)} } from './schemes/${slug}/jwt-helper';`;
|
|
20
|
+
})).join("\n");
|
|
21
|
+
const verifiers = external.map(([name]) => { const id = safeIdentifier(name); return `${JSON.stringify(name)}: async (token: string) => { try { return await verify_${id}(token); } catch (e) { if (e instanceof Error_${id}) throw new AuthError(e.status, e.message); throw e; } }`; }).join(",\n ");
|
|
22
|
+
return `import type { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
23
|
+
${imports}
|
|
24
|
+
export type AuthPrincipal = { subject: string; scheme: string; issuer: string; roles: string[]; claims: Record<string, unknown>; /** @deprecated */ userId?: string; /** @deprecated */ userDetails?: string };
|
|
25
|
+
export class AuthError extends Error { constructor(public status: 401 | 403 | 503, message: string) { super(message); } }
|
|
26
|
+
const schemes = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n,s]) => [n,{provider:s.provider,allowedProviders:s.swa?.allowedProviders??[]}])))} as const;
|
|
27
|
+
const policies = ${JSON.stringify(config.authorization.policies)} as Record<string, { schemes: string[]; roles?: string[] }>;
|
|
28
|
+
const externalVerifiers: Record<string, (token: string) => Promise<any>> = { ${verifiers} };
|
|
29
|
+
const customVerifiers: Record<string, (request: HttpRequest) => any> = { ${custom.map(([name]) => `${JSON.stringify(name)}: verify_${safeIdentifier(name)}`).join(", ")} };
|
|
30
|
+
function swa(request: HttpRequest, scheme: string): AuthPrincipal {
|
|
31
|
+
const encoded = request.headers.get('x-ms-client-principal');
|
|
32
|
+
if (!encoded) throw new AuthError(401, 'Authentication required');
|
|
33
|
+
try { const value = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')) as Record<string, unknown>;
|
|
34
|
+
const roles = Array.isArray(value.userRoles) ? value.userRoles.filter((r): r is string => typeof r === 'string') : [];
|
|
35
|
+
if (typeof value.userId !== 'string' || !value.userId || !roles.includes('authenticated')) throw new Error();
|
|
36
|
+
const allowed = (schemes as any)[scheme]?.allowedProviders as string[] | undefined;
|
|
37
|
+
if (allowed?.length && (typeof value.identityProvider !== 'string' || !allowed.includes(value.identityProvider))) throw new Error();
|
|
38
|
+
return { subject: value.userId, scheme, issuer: typeof value.identityProvider === 'string' ? 'swa:' + value.identityProvider : 'swa', roles, claims: value, userId: value.userId, userDetails: typeof value.userDetails === 'string' ? value.userDetails : undefined };
|
|
39
|
+
} catch (e) { if (e instanceof AuthError) throw e; throw new AuthError(401, 'Invalid SWA client principal'); }
|
|
40
|
+
}
|
|
41
|
+
export async function requireAuth(request: HttpRequest, policyName: string): Promise<AuthPrincipal> {
|
|
42
|
+
const policy = policies[policyName]; if (!policy) throw new AuthError(401, 'Undefined authorization policy');
|
|
43
|
+
const bearer = request.headers.get('authorization'); const hasSwa = Boolean(request.headers.get('x-ms-client-principal'));
|
|
44
|
+
const candidates = policy.schemes.filter(name => { const p=(schemes as any)[name]?.provider; return p === 'swa' ? hasSwa : (p === 'external-token' || p === 'custom-jwt') ? Boolean(bearer?.startsWith('Bearer ')) : false; });
|
|
45
|
+
if (candidates.length !== 1) throw new AuthError(401, candidates.length ? 'Ambiguous credentials' : 'Authentication required');
|
|
46
|
+
const scheme = candidates[0]; const provider = (schemes as any)[scheme]?.provider; let principal: AuthPrincipal;
|
|
47
|
+
if (provider === 'swa') principal = swa(request, scheme);
|
|
48
|
+
else if (provider === 'external-token') { const token=bearer!.slice(7); try { const p=await externalVerifiers[scheme](token); const subject=p?.subject ?? p?.userId; if (typeof subject !== 'string' || !subject || !Array.isArray(p.roles)) throw new AuthError(401,'Invalid external principal'); principal={subject,scheme,issuer:typeof p.issuer==='string'?p.issuer:scheme,roles:p.roles,claims:p.claims && typeof p.claims==='object'?p.claims:p,userId:subject,userDetails:p.userDetails}; } catch(e) { if(e instanceof AuthError) throw e; throw new AuthError(503,'Identity provider unavailable'); } }
|
|
49
|
+
else if (provider === 'custom-jwt') { try { const p=customVerifiers[scheme](request); const subject=p?.sub ?? p?.userId; if(typeof subject !== 'string' || !subject || !Array.isArray(p.roles)) throw new Error(); principal={subject,scheme,issuer:scheme,roles:p.roles,claims:p,userId:subject,userDetails:p.loginId}; } catch { throw new AuthError(401,'Invalid or expired token'); } }
|
|
50
|
+
else throw new AuthError(401, 'Authentication adapter is not configured');
|
|
51
|
+
requireRoles(principal, policy.roles || []); return principal;
|
|
52
|
+
}
|
|
53
|
+
export function requireRoles(principal: AuthPrincipal, roles: string[]): void { if (roles.length && !roles.some(r => principal.roles.includes(r))) throw new AuthError(403, 'Insufficient permissions'); }
|
|
54
|
+
export function identityKey(principal: AuthPrincipal): string { return principal.scheme + ':' + principal.subject; }
|
|
55
|
+
export function handleAuthError(error: unknown): HttpResponseInit | null { return error instanceof AuthError ? { status:error.status, jsonBody:{error:error.message} } : null; }
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeIdentifier(name: string): string {
|
|
60
|
+
return name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function schemeSlug(name: string): string {
|
|
64
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function generateNamedExternalTokenVerifierCSharp(scheme: string): string {
|
|
68
|
+
const id = safeIdentifier(scheme);
|
|
69
|
+
return `namespace Functions.Auth.Schemes;
|
|
70
|
+
public record ExternalPrincipal_${id}(string Subject, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims);
|
|
71
|
+
public sealed class ExternalTokenVerificationException_${id} : Exception
|
|
72
|
+
{
|
|
73
|
+
public bool IsIdentityProviderUnavailable { get; }
|
|
74
|
+
public ExternalTokenVerificationException_${id}(string message, bool unavailable = false) : base(message) => IsIdentityProviderUnavailable = unavailable;
|
|
75
|
+
}
|
|
76
|
+
public static class ExternalTokenVerifier_${id}
|
|
77
|
+
{
|
|
78
|
+
// Validate signature, issuer, audience and expiry. This stub deliberately fails closed.
|
|
79
|
+
public static Task<ExternalPrincipal_${id}> VerifyAsync(string token) => throw new InvalidOperationException("External token verifier for ${scheme} is not configured");
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function generateNamedExternalTokenVerifierPython(scheme: string): string {
|
|
85
|
+
return `class ExternalTokenVerificationError(Exception):
|
|
86
|
+
def __init__(self, message: str, identity_provider_unavailable: bool = False):
|
|
87
|
+
super().__init__(message)
|
|
88
|
+
self.identity_provider_unavailable = identity_provider_unavailable
|
|
89
|
+
|
|
90
|
+
def verify_external_token(token: str) -> dict:
|
|
91
|
+
# Validate signature, issuer, audience and expiry. This stub deliberately fails closed.
|
|
92
|
+
raise RuntimeError("External token verifier for ${scheme} is not configured")
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function generateNamedAuthRouterCSharp(config: NormalizedAuthConfig): string {
|
|
97
|
+
const externalCases = Object.entries(config.schemes).filter(([,s]) => s.provider === "external-token").map(([name]) => {
|
|
98
|
+
const id = safeIdentifier(name);
|
|
99
|
+
return ` case "${name}":
|
|
100
|
+
try
|
|
101
|
+
{
|
|
102
|
+
var external = await ExternalTokenVerifier_${id}.VerifyAsync(bearer!);
|
|
103
|
+
principal = new AuthPrincipal(external.Subject, "${name}", external.Issuer, external.Roles, external.Claims);
|
|
104
|
+
}
|
|
105
|
+
catch (ExternalTokenVerificationException_${id} ex) when (ex.IsIdentityProviderUnavailable) { return (null, await Error(request, HttpStatusCode.ServiceUnavailable, "Identity provider unavailable")); }
|
|
106
|
+
catch (ExternalTokenVerificationException_${id}) { return (null, await Error(request, HttpStatusCode.Unauthorized, "Invalid token")); }
|
|
107
|
+
catch { return (null, await Error(request, HttpStatusCode.ServiceUnavailable, "External token verifier unavailable")); }
|
|
108
|
+
break;`;
|
|
109
|
+
}).join("\n");
|
|
110
|
+
return `using System.Net;
|
|
111
|
+
using System.Text;
|
|
112
|
+
using System.Text.Json;
|
|
113
|
+
using Microsoft.Azure.Functions.Worker.Http;
|
|
114
|
+
using Functions.Auth.Schemes;
|
|
115
|
+
namespace Functions.Auth;
|
|
116
|
+
public record AuthPrincipal(string Subject, string Scheme, string Issuer, string[] Roles, IReadOnlyDictionary<string, object?> Claims)
|
|
117
|
+
{
|
|
118
|
+
[Obsolete("Use Subject")] public string UserId => Subject;
|
|
119
|
+
public string IdentityKey => $"{Scheme}:{Subject}";
|
|
120
|
+
}
|
|
121
|
+
public static class AuthRouter
|
|
122
|
+
{
|
|
123
|
+
private static readonly Dictionary<string, (string[] Schemes, string[] Roles)> Policies = new()
|
|
124
|
+
{
|
|
125
|
+
${Object.entries(config.authorization.policies).map(([n,p]) => ` ["${n}"] = (new[] { ${p.schemes.map(s=>`"${s}"`).join(", ")} }, new[] { ${(p.roles??[]).map(r=>`"${r}"`).join(", ")} }),`).join("\n")}
|
|
126
|
+
};
|
|
127
|
+
private static readonly Dictionary<string,string> Providers = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]="${s.provider}"`).join(", ")} };
|
|
128
|
+
private static readonly Dictionary<string,string[]> AllowedProviders = new() { ${Object.entries(config.schemes).map(([n,s])=>`["${n}"]=new[] { ${(s.swa?.allowedProviders??[]).map(p=>`"${p}"`).join(", ")} }`).join(", ")} };
|
|
129
|
+
public static async Task<(AuthPrincipal? Principal, HttpResponseData? ErrorResponse)> Authorize(HttpRequestData request, string policyName)
|
|
130
|
+
{
|
|
131
|
+
if (!Policies.TryGetValue(policyName, out var policy)) return (null, await Error(request, HttpStatusCode.Unauthorized, "Undefined authorization policy"));
|
|
132
|
+
var bearerHeader = request.Headers.TryGetValues("Authorization", out var authValues) ? authValues.FirstOrDefault() : null;
|
|
133
|
+
var bearer = bearerHeader?.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) == true ? bearerHeader[7..] : null;
|
|
134
|
+
var swa = request.Headers.TryGetValues("x-ms-client-principal", out var swaValues) ? swaValues.FirstOrDefault() : null;
|
|
135
|
+
var candidates = policy.Schemes.Where(s => Providers[s] == "swa" ? swa != null : Providers[s] is "external-token" or "custom-jwt" ? bearer != null : false).ToArray();
|
|
136
|
+
if (candidates.Length != 1) return (null, await Error(request, HttpStatusCode.Unauthorized, candidates.Length == 0 ? "Authentication required" : "Ambiguous credentials"));
|
|
137
|
+
var scheme = candidates[0]; AuthPrincipal principal;
|
|
138
|
+
switch (scheme)
|
|
139
|
+
{
|
|
140
|
+
${externalCases}
|
|
141
|
+
default:
|
|
142
|
+
if (Providers[scheme] != "swa") return (null, await Error(request, HttpStatusCode.Unauthorized, "Authentication adapter is not configured"));
|
|
143
|
+
try
|
|
144
|
+
{
|
|
145
|
+
using var doc = JsonDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(swa!)));
|
|
146
|
+
var root = doc.RootElement; var subject = root.GetProperty("userId").GetString();
|
|
147
|
+
var roles = root.TryGetProperty("userRoles", out var rv) ? rv.EnumerateArray().Where(v=>v.ValueKind==JsonValueKind.String).Select(v=>v.GetString()!).ToArray() : Array.Empty<string>();
|
|
148
|
+
if (string.IsNullOrEmpty(subject) || !roles.Contains("authenticated")) throw new InvalidOperationException();
|
|
149
|
+
var identityProvider = root.TryGetProperty("identityProvider", out var iv) ? iv.GetString() : null;
|
|
150
|
+
if (AllowedProviders[scheme].Length > 0 && (identityProvider == null || !AllowedProviders[scheme].Contains(identityProvider))) throw new InvalidOperationException();
|
|
151
|
+
var issuer = identityProvider == null ? "swa" : "swa:" + identityProvider;
|
|
152
|
+
principal = new AuthPrincipal(subject!, scheme, issuer, roles, new Dictionary<string,object?> { ["clientPrincipal"] = root.Clone() });
|
|
153
|
+
}
|
|
154
|
+
catch { return (null, await Error(request, HttpStatusCode.Unauthorized, "Invalid SWA client principal")); }
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (policy.Roles.Length > 0 && !policy.Roles.Any(principal.Roles.Contains)) return (null, await Error(request, HttpStatusCode.Forbidden, "Insufficient permissions"));
|
|
158
|
+
return (principal, null);
|
|
159
|
+
}
|
|
160
|
+
private static async Task<HttpResponseData> Error(HttpRequestData request, HttpStatusCode status, string message) { var response=request.CreateResponse(status); await response.WriteAsJsonAsync(new { error=message }); return response; }
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function generateNamedAuthRouterPython(config: NormalizedAuthConfig): string {
|
|
166
|
+
const external = Object.entries(config.schemes).filter(([,s])=>s.provider === "external-token");
|
|
167
|
+
const imports = external.map(([n])=>`from auth.schemes.${schemeSlug(n).replace(/-/g,"_")}.verifier import verify_external_token as verify_${safeIdentifier(n)}, ExternalTokenVerificationError as Error_${safeIdentifier(n)}`).join("\n");
|
|
168
|
+
const cases = external.map(([n], index)=>` ${index === 0 ? "if" : "elif"} scheme == ${JSON.stringify(n)}:
|
|
169
|
+
try:
|
|
170
|
+
value = verify_${safeIdentifier(n)}(bearer)
|
|
171
|
+
except Error_${safeIdentifier(n)} as exc:
|
|
172
|
+
raise AuthError(503 if exc.identity_provider_unavailable else 401, "Identity provider unavailable" if exc.identity_provider_unavailable else "Invalid token")
|
|
173
|
+
except Exception:
|
|
174
|
+
raise AuthError(503, "External token verifier unavailable")
|
|
175
|
+
if not value.get("subject") or not isinstance(value.get("roles"), list): raise AuthError(401, "Invalid external principal")
|
|
176
|
+
principal = {"subject": value["subject"], "scheme": scheme, "issuer": value.get("issuer", scheme), "roles": value["roles"], "claims": value.get("claims", {}), "userId": value["subject"]}`).join("\n");
|
|
177
|
+
return `import base64
|
|
178
|
+
import json
|
|
179
|
+
import azure.functions as func
|
|
180
|
+
${imports}
|
|
181
|
+
POLICIES = ${JSON.stringify(config.authorization.policies)}
|
|
182
|
+
PROVIDERS = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n,s])=>[n,s.provider])))}
|
|
183
|
+
ALLOWED_PROVIDERS = ${JSON.stringify(Object.fromEntries(Object.entries(config.schemes).map(([n,s])=>[n,s.swa?.allowedProviders??[]])))}
|
|
184
|
+
class AuthError(Exception):
|
|
185
|
+
def __init__(self, status, message): self.status, self.message = status, message
|
|
186
|
+
def require_auth(req: func.HttpRequest, policy_name: str):
|
|
187
|
+
policy = POLICIES.get(policy_name)
|
|
188
|
+
if not policy: raise AuthError(401, "Undefined authorization policy")
|
|
189
|
+
header = req.headers.get("authorization", "")
|
|
190
|
+
bearer = header[7:] if header.lower().startswith("bearer ") else None
|
|
191
|
+
swa = req.headers.get("x-ms-client-principal")
|
|
192
|
+
candidates = [s for s in policy["schemes"] if (PROVIDERS[s] == "swa" and swa) or (PROVIDERS[s] in ("external-token", "custom-jwt") and bearer)]
|
|
193
|
+
if len(candidates) != 1: raise AuthError(401, "Authentication required" if not candidates else "Ambiguous credentials")
|
|
194
|
+
scheme = candidates[0]
|
|
195
|
+
${cases}
|
|
196
|
+
${external.length ? "elif" : "if"} PROVIDERS[scheme] == "swa":
|
|
197
|
+
try:
|
|
198
|
+
value=json.loads(base64.b64decode(swa).decode("utf-8")); roles=[r for r in value.get("userRoles",[]) if isinstance(r,str)]
|
|
199
|
+
if not value.get("userId") or "authenticated" not in roles: raise ValueError()
|
|
200
|
+
if ALLOWED_PROVIDERS[scheme] and value.get("identityProvider") not in ALLOWED_PROVIDERS[scheme]: raise ValueError()
|
|
201
|
+
principal={"subject":value["userId"],"scheme":scheme,"issuer":"swa:"+value.get("identityProvider", "unknown"),"roles":roles,"claims":value,"userId":value["userId"]}
|
|
202
|
+
except Exception: raise AuthError(401,"Invalid SWA client principal")
|
|
203
|
+
else: raise AuthError(401,"Authentication adapter is not configured")
|
|
204
|
+
require_roles(principal, policy.get("roles", [])); return principal
|
|
205
|
+
def require_roles(principal, roles):
|
|
206
|
+
if roles and not any(role in principal.get("roles",[]) for role in roles): raise AuthError(403,"Insufficient permissions")
|
|
207
|
+
def identity_key(principal): return principal["scheme"] + ":" + principal["subject"]
|
|
208
|
+
def handle_auth_error(error): return func.HttpResponse(json.dumps({"error":error.message}),status_code=error.status,mimetype="application/json") if isinstance(error,AuthError) else None
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
10
212
|
// ============================================================
|
|
11
213
|
// 1. shared/models/auth.ts(Zod スキーマ)
|
|
12
214
|
// ============================================================
|
|
@@ -1249,8 +1451,9 @@ export function generateBFFCallFunctionWithSwaAuth(): string {
|
|
|
1249
1451
|
if (authorization) {
|
|
1250
1452
|
fetchHeaders['Authorization'] = authorization;
|
|
1251
1453
|
}`,
|
|
1252
|
-
`
|
|
1253
|
-
|
|
1454
|
+
` // Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.
|
|
1455
|
+
let principal: string | null = null;
|
|
1456
|
+
{
|
|
1254
1457
|
const cookie = reqHeaders.get('cookie');
|
|
1255
1458
|
const host = reqHeaders.get('host');
|
|
1256
1459
|
if (cookie && host) {
|
|
@@ -1282,6 +1485,16 @@ export function generateBFFCallFunctionWithSwaAuth(): string {
|
|
|
1282
1485
|
);
|
|
1283
1486
|
}
|
|
1284
1487
|
|
|
1488
|
+
/** Combined transport for named schemes. Functions still performs all verification. */
|
|
1489
|
+
export function generateBFFCallFunctionWithMultipleAuth(): string {
|
|
1490
|
+
return generateBFFCallFunctionWithSwaAuth().replace(
|
|
1491
|
+
` // Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.`,
|
|
1492
|
+
` const authorization = reqHeaders.get('authorization');
|
|
1493
|
+
if (authorization?.startsWith('Bearer ')) fetchHeaders['Authorization'] = authorization;
|
|
1494
|
+
// Never forward a browser-supplied principal header. Resolve it from the SWA session endpoint.`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1285
1498
|
export function generateSwaAuthHelperTS(): string {
|
|
1286
1499
|
return `import { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
1287
1500
|
|
|
@@ -1485,17 +1698,20 @@ public static class JwtHelper
|
|
|
1485
1698
|
/**
|
|
1486
1699
|
* TypeScript Functions のハンドラーにロールガードを挿入するためのインポート文を生成
|
|
1487
1700
|
*/
|
|
1488
|
-
export function generateAuthImportTS(): string {
|
|
1489
|
-
return `import { requireAuth, requireRoles, handleAuthError } from './auth
|
|
1701
|
+
export function generateAuthImportTS(namedPolicy = false): string {
|
|
1702
|
+
return `import { requireAuth, requireRoles, handleAuthError } from './auth/${namedPolicy ? "auth-router" : "jwt-helper"}';`;
|
|
1490
1703
|
}
|
|
1491
1704
|
|
|
1492
1705
|
/**
|
|
1493
1706
|
* TypeScript Functions のハンドラー先頭に挿入する認証チェックコードを生成
|
|
1494
1707
|
*/
|
|
1495
1708
|
export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' | 'write', provider?: AuthProvider): string {
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
:
|
|
1709
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1710
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1711
|
+
const name = typeof selected === 'string' ? selected : policy.policy!;
|
|
1712
|
+
return ` const authUser = await requireAuth(request, ${JSON.stringify(name)});`;
|
|
1713
|
+
}
|
|
1714
|
+
const roles = selected || policy.roles || [];
|
|
1499
1715
|
|
|
1500
1716
|
if (roles.length > 0) {
|
|
1501
1717
|
return ` const authUser = ${provider === 'external-token' ? 'await ' : ''}requireAuth(request);
|
|
@@ -1508,9 +1724,12 @@ export function generateAuthGuardTS(policy: ModelAuthPolicy, operation: 'read' |
|
|
|
1508
1724
|
* C# Functions のハンドラーに挿入するロールガードコードを生成
|
|
1509
1725
|
*/
|
|
1510
1726
|
export function generateAuthGuardCSharp(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
|
|
1511
|
-
const
|
|
1512
|
-
|
|
1513
|
-
:
|
|
1727
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1728
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1729
|
+
const name = typeof selected === 'string' ? selected : policy.policy!;
|
|
1730
|
+
return ` var (principal, errorResponse) = await AuthRouter.Authorize(request, ${JSON.stringify(name)});\n if (errorResponse != null) return errorResponse;`;
|
|
1731
|
+
}
|
|
1732
|
+
const roles = selected || policy.roles || [];
|
|
1514
1733
|
|
|
1515
1734
|
if (roles.length > 0) {
|
|
1516
1735
|
const rolesStr = roles.map(r => `"${r}"`).join(', ');
|
|
@@ -1525,9 +1744,12 @@ export function generateAuthGuardCSharp(policy: ModelAuthPolicy, operation: 'rea
|
|
|
1525
1744
|
* Python Functions のハンドラーに挿入するロールガードコードを生成
|
|
1526
1745
|
*/
|
|
1527
1746
|
export function generateAuthGuardPython(policy: ModelAuthPolicy, operation: 'read' | 'write'): string {
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
:
|
|
1747
|
+
const selected = operation === 'read' ? policy.read : policy.write;
|
|
1748
|
+
if (typeof selected === 'string' || policy.policy) {
|
|
1749
|
+
const name = typeof selected === 'string' ? selected : policy.policy!;
|
|
1750
|
+
return ` user = require_auth(req, ${JSON.stringify(name)})`;
|
|
1751
|
+
}
|
|
1752
|
+
const roles = selected || policy.roles || [];
|
|
1531
1753
|
|
|
1532
1754
|
if (roles.length > 0) {
|
|
1533
1755
|
const rolesStr = roles.map(r => `"${r}"`).join(', ');
|
|
@@ -124,7 +124,8 @@ export function generateRdbConnectorFunctionTS(
|
|
|
124
124
|
|
|
125
125
|
// Auth guard setup
|
|
126
126
|
const hasAuth = !!authPolicy;
|
|
127
|
-
const
|
|
127
|
+
const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
|
|
128
|
+
const authImport = hasAuth ? `\n${generateAuthImportTS(namedAuth)}\n` : '';
|
|
128
129
|
const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
|
|
129
130
|
const authCatchBlock = hasAuth
|
|
130
131
|
? ` const authErr = handleAuthError(error);
|
|
@@ -205,7 +206,8 @@ export function generateApiConnectorFunctionTS(
|
|
|
205
206
|
|
|
206
207
|
// Auth guard setup (user access control, separate from connector auth to external APIs)
|
|
207
208
|
const hasAuth = !!authPolicy;
|
|
208
|
-
const
|
|
209
|
+
const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
|
|
210
|
+
const authImportLine = hasAuth ? `\n${generateAuthImportTS(namedAuth)}\n` : '';
|
|
209
211
|
const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
|
|
210
212
|
const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write', authProvider)}\n` : '';
|
|
211
213
|
const authCatchBlock = hasAuth
|
|
@@ -22,7 +22,8 @@ export function generateCompactAzureFunctionsCRUD(model: ModelInfo, sharedPackag
|
|
|
22
22
|
const isIdPartition = partitionKeyField === 'id';
|
|
23
23
|
|
|
24
24
|
const hasAuth = !!authPolicy;
|
|
25
|
-
const
|
|
25
|
+
const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
|
|
26
|
+
const authImport = hasAuth ? `\n${generateAuthImportTS(namedAuth)}\n` : '';
|
|
26
27
|
const readGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'read', authProvider)}\n` : '';
|
|
27
28
|
const writeGuard = hasAuth ? `\n${generateAuthGuardTS(authPolicy!, 'write', authProvider)}\n` : '';
|
|
28
29
|
const authCatchBlock = hasAuth
|
|
@@ -779,7 +780,8 @@ export function generatePythonAzureFunctionsCRUD(model: ModelInfo, authPolicy?:
|
|
|
779
780
|
const isIdPartition = partitionKeyField === 'id';
|
|
780
781
|
|
|
781
782
|
const hasAuth = !!authPolicy;
|
|
782
|
-
const
|
|
783
|
+
const namedAuth = Boolean(authPolicy && (authPolicy.policy || typeof authPolicy.read === 'string' || typeof authPolicy.write === 'string'));
|
|
784
|
+
const authImport = hasAuth ? `\nfrom auth.${namedAuth ? 'auth_router' : 'jwt_helper'} import require_auth, require_roles, handle_auth_error\n` : '';
|
|
783
785
|
// generateAuthGuardPython outputs at 4-space indent; inside try: we need 8-space
|
|
784
786
|
const readGuardRaw = hasAuth ? generateAuthGuardPython(authPolicy!, 'read') : '';
|
|
785
787
|
const writeGuardRaw = hasAuth ? generateAuthGuardPython(authPolicy!, 'write') : '';
|
|
@@ -779,14 +779,20 @@ export function parseAuthPolicy(content: string): ModelAuthPolicy | undefined {
|
|
|
779
779
|
const read = extractRoles('read');
|
|
780
780
|
const write = extractRoles('write');
|
|
781
781
|
|
|
782
|
-
|
|
782
|
+
const extractName = (key: string): string | undefined => objectStr.match(new RegExp(`${key}\\s*:\\s*['"]([^'"]+)['"]`))?.[1];
|
|
783
|
+
const namedPolicy = extractName('policy');
|
|
784
|
+
const namedRead = extractName('read');
|
|
785
|
+
const namedWrite = extractName('write');
|
|
786
|
+
|
|
787
|
+
if (!roles && !read && !write && !namedPolicy && !namedRead && !namedWrite) {
|
|
783
788
|
return undefined;
|
|
784
789
|
}
|
|
785
790
|
|
|
786
791
|
return {
|
|
787
792
|
...(roles ? { roles } : {}),
|
|
788
|
-
...(read ? { read } : {}),
|
|
789
|
-
...(write ? { write } : {}),
|
|
793
|
+
...(namedRead ? { read: namedRead } : read ? { read } : {}),
|
|
794
|
+
...(namedWrite ? { write: namedWrite } : write ? { write } : {}),
|
|
795
|
+
...(namedPolicy ? { policy: namedPolicy } : {}),
|
|
790
796
|
};
|
|
791
797
|
}
|
|
792
798
|
|
|
@@ -10,6 +10,9 @@ import { ModelAuthPolicy } from "../../types";
|
|
|
10
10
|
export interface UIAuthOptions {
|
|
11
11
|
/** モデル固有のロールポリシー */
|
|
12
12
|
authPolicy: ModelAuthPolicy;
|
|
13
|
+
writeRoles?: string[];
|
|
14
|
+
authenticatedFetchImport?: string;
|
|
15
|
+
authContextImport?: string;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -21,7 +24,8 @@ export function generateListPage(model: ModelInfo, sharedPackageName: string, au
|
|
|
21
24
|
const modelKebab = toKebabCase(modelName);
|
|
22
25
|
|
|
23
26
|
const hasAuth = !!authOptions;
|
|
24
|
-
const
|
|
27
|
+
const authContextImport = authOptions?.authContextImport ?? '@/lib/auth/auth-context';
|
|
28
|
+
const writeRoles = authOptions?.writeRoles ?? (Array.isArray(authOptions?.authPolicy?.write) ? authOptions.authPolicy.write : authOptions?.authPolicy?.roles);
|
|
25
29
|
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
26
30
|
|
|
27
31
|
// フィールドから表示するカラムを抽出(id以外の最初の3つ)
|
|
@@ -71,7 +75,7 @@ import { useEffect, useState } from 'react';
|
|
|
71
75
|
import Link from 'next/link';
|
|
72
76
|
import { z } from 'zod/v4';
|
|
73
77
|
${schemaImportLine}
|
|
74
|
-
${hasAuth ? `import { useAuth } from '
|
|
78
|
+
${hasAuth ? `import { useAuth } from '${authContextImport}';` : ''}
|
|
75
79
|
|
|
76
80
|
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
77
81
|
|
|
@@ -266,7 +270,8 @@ export function generateDetailPage(model: ModelInfo, sharedPackageName: string,
|
|
|
266
270
|
const modelKebab = toKebabCase(modelName);
|
|
267
271
|
|
|
268
272
|
const hasAuth = !!authOptions;
|
|
269
|
-
const
|
|
273
|
+
const authContextImport = authOptions?.authContextImport ?? '@/lib/auth/auth-context';
|
|
274
|
+
const writeRoles = authOptions?.writeRoles ?? (Array.isArray(authOptions?.authPolicy?.write) ? authOptions.authPolicy.write : authOptions?.authPolicy?.roles);
|
|
270
275
|
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
271
276
|
|
|
272
277
|
// 外部キーフィールドを検出
|
|
@@ -311,7 +316,7 @@ import { useParams, useRouter } from 'next/navigation';
|
|
|
311
316
|
import Link from 'next/link';
|
|
312
317
|
import { z } from 'zod/v4';
|
|
313
318
|
${schemaImportLine}
|
|
314
|
-
${hasAuth ? `import { useAuth } from '
|
|
319
|
+
${hasAuth ? `import { useAuth } from '${authContextImport}';` : ''}
|
|
315
320
|
|
|
316
321
|
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
317
322
|
|
|
@@ -910,7 +915,8 @@ export function generateNewPage(model: ModelInfo, authOptions?: UIAuthOptions):
|
|
|
910
915
|
const modelKebab = toKebabCase(modelName);
|
|
911
916
|
|
|
912
917
|
const hasAuth = !!authOptions;
|
|
913
|
-
const
|
|
918
|
+
const authContextImport = authOptions?.authContextImport ?? '@/lib/auth/auth-context';
|
|
919
|
+
const writeRoles = authOptions?.writeRoles ?? (Array.isArray(authOptions?.authPolicy?.write) ? authOptions.authPolicy.write : authOptions?.authPolicy?.roles);
|
|
914
920
|
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
915
921
|
|
|
916
922
|
if (!hasAuth) {
|
|
@@ -936,7 +942,7 @@ export default function New${modelName}Page() {
|
|
|
936
942
|
import { useEffect } from 'react';
|
|
937
943
|
import { useRouter } from 'next/navigation';
|
|
938
944
|
import ${modelName}Form from '../_components/${modelName}Form';
|
|
939
|
-
import { useAuth } from '
|
|
945
|
+
import { useAuth } from '${authContextImport}';
|
|
940
946
|
|
|
941
947
|
export default function New${modelName}Page() {
|
|
942
948
|
const router = useRouter();
|
|
@@ -979,7 +985,8 @@ export function generateEditPage(model: ModelInfo, sharedPackageName: string, au
|
|
|
979
985
|
const modelKebab = toKebabCase(modelName);
|
|
980
986
|
|
|
981
987
|
const hasAuth = !!authOptions;
|
|
982
|
-
const
|
|
988
|
+
const authContextImport = authOptions?.authContextImport ?? '@/lib/auth/auth-context';
|
|
989
|
+
const writeRoles = authOptions?.writeRoles ?? (Array.isArray(authOptions?.authPolicy?.write) ? authOptions.authPolicy.write : authOptions?.authPolicy?.roles);
|
|
983
990
|
const hasWriteRoles = hasAuth && writeRoles && writeRoles.length > 0;
|
|
984
991
|
|
|
985
992
|
const schemaName = model.schemaName;
|
|
@@ -997,7 +1004,7 @@ import { useParams, useRouter } from 'next/navigation';
|
|
|
997
1004
|
import ${modelName}Form from '../../_components/${modelName}Form';
|
|
998
1005
|
import { z } from 'zod/v4';
|
|
999
1006
|
${schemaImportLine}
|
|
1000
|
-
${hasAuth ? `import { useAuth } from '
|
|
1007
|
+
${hasAuth ? `import { useAuth } from '${authContextImport}';` : ''}
|
|
1001
1008
|
|
|
1002
1009
|
type ${modelName} = z.infer<typeof ${localSchemaName}>;
|
|
1003
1010
|
|