feat: isolate asset release access classes (TASK-WP5-04)
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 59s
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 59s
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dada/asset-release-manifest": "workspace:*",
|
||||
"@dada/shared-contracts": "workspace:*",
|
||||
"@fastify/multipart": "10.1.0",
|
||||
"@fastify/swagger": "9.8.1",
|
||||
|
||||
+133
-1
@@ -134,6 +134,7 @@ import {
|
||||
type RegistrationCompleteRequest,
|
||||
type RegistrationSendRequest,
|
||||
} from "@dada/shared-contracts";
|
||||
import type { AssetReleaseReader } from "@dada/asset-release-manifest";
|
||||
import swagger from "@fastify/swagger";
|
||||
import multipart from "@fastify/multipart";
|
||||
import Fastify, { type FastifyReply } from "fastify";
|
||||
@@ -192,6 +193,7 @@ const defaultBootstrap: BootstrapResponse = {
|
||||
|
||||
export interface CreateAppOptions {
|
||||
amap?: AmapAdapter;
|
||||
assetReleases?: AssetReleaseReader;
|
||||
bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>;
|
||||
browserGate?: boolean;
|
||||
browserSupportRelease?: BrowserSupportRelease;
|
||||
@@ -205,6 +207,17 @@ export interface CreateAppOptions {
|
||||
publicAssets?: PublicAssetResolver;
|
||||
recentAssets?: RecentAssetService;
|
||||
projects?: ProjectService;
|
||||
previewAssetAuthorizer?: (input: {
|
||||
releaseVersion: string;
|
||||
resourceId: string;
|
||||
userId: string;
|
||||
}) => boolean | Promise<boolean>;
|
||||
privateAssetAdminAuthorizer?: (input: {
|
||||
adminUserId: string;
|
||||
ownerId: string;
|
||||
releaseVersion: string;
|
||||
resourceId: string;
|
||||
}) => boolean | Promise<boolean>;
|
||||
registration?: RegistrationService;
|
||||
}
|
||||
|
||||
@@ -816,13 +829,27 @@ export async function createApp(options: CreateAppOptions = {}) {
|
||||
status: "ready",
|
||||
}));
|
||||
|
||||
app.get(
|
||||
"/api/v1/assets/public/:resourceVersion/manifest",
|
||||
{ schema: { hide: true } },
|
||||
async (request, reply) => {
|
||||
const { resourceVersion } = request.params as { resourceVersion: string };
|
||||
const manifest = options.assetReleases?.project("public_release_asset", resourceVersion);
|
||||
if (!manifest) return reply.code(404).send();
|
||||
reply.header("Cache-Control", "public, max-age=31536000, immutable");
|
||||
reply.header("ETag", `"sha256-${manifest.manifest_sha256}"`);
|
||||
return manifest;
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/v1/assets/public/:resourceVersion/:assetId",
|
||||
{ schema: { hide: true } },
|
||||
async (request, reply) => {
|
||||
const { assetId, resourceVersion } = request.params as { assetId?: string; resourceVersion?: string };
|
||||
const resource = assetId && resourceVersion
|
||||
? options.publicAssets?.read(resourceVersion, assetId)
|
||||
? options.assetReleases?.read("public_release_asset", resourceVersion, assetId)
|
||||
?? options.publicAssets?.read(resourceVersion, assetId)
|
||||
: undefined;
|
||||
if (!resource) return reply.code(404).send();
|
||||
reply.type(resource.mimeType);
|
||||
@@ -833,6 +860,111 @@ export async function createApp(options: CreateAppOptions = {}) {
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/v1/assets/preview/:resourceVersion/manifest",
|
||||
{ schema: { hide: true } },
|
||||
async (request, reply) => {
|
||||
if (!options.registration) {
|
||||
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
|
||||
}
|
||||
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
|
||||
const session = token ? options.registration.readUserSession(token) : undefined;
|
||||
if (!session) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
|
||||
const { resourceVersion } = request.params as { resourceVersion: string };
|
||||
const available = options.assetReleases?.project("internal_preview_asset", resourceVersion);
|
||||
if (!available || !options.previewAssetAuthorizer) return reply.code(404).send();
|
||||
const authorizedIds: string[] = [];
|
||||
for (const item of available.items) {
|
||||
if (await options.previewAssetAuthorizer({
|
||||
releaseVersion: resourceVersion,
|
||||
resourceId: item.resource_id,
|
||||
userId: session.userId,
|
||||
})) authorizedIds.push(item.resource_id);
|
||||
}
|
||||
if (authorizedIds.length === 0) return reply.code(404).send();
|
||||
const manifest = options.assetReleases?.project("internal_preview_asset", resourceVersion, { resourceIds: authorizedIds });
|
||||
if (!manifest) return reply.code(404).send();
|
||||
reply.header("Cache-Control", "private, no-store");
|
||||
reply.header("Vary", "Cookie");
|
||||
return manifest;
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/v1/assets/preview/:resourceVersion/:assetId",
|
||||
{ schema: { hide: true } },
|
||||
async (request, reply) => {
|
||||
if (!options.registration) {
|
||||
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
|
||||
}
|
||||
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
|
||||
const session = token ? options.registration.readUserSession(token) : undefined;
|
||||
if (!session) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
|
||||
const { assetId, resourceVersion } = request.params as { assetId: string; resourceVersion: string };
|
||||
const authorized = await options.previewAssetAuthorizer?.({ resourceId: assetId, releaseVersion: resourceVersion, userId: session.userId });
|
||||
const resource = authorized ? options.assetReleases?.read("internal_preview_asset", resourceVersion, assetId) : undefined;
|
||||
if (!resource) return reply.code(404).send();
|
||||
reply.type(resource.mimeType);
|
||||
reply.header("Cache-Control", "private, no-store");
|
||||
reply.header("Content-Disposition", "inline");
|
||||
reply.header("Vary", "Cookie");
|
||||
return resource.bytes;
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/v1/private-assets/:resourceVersion/manifest",
|
||||
{ schema: { hide: true } },
|
||||
async (request, reply) => {
|
||||
if (!options.registration) {
|
||||
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
|
||||
}
|
||||
const token = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
|
||||
const session = token ? options.registration.readUserSession(token) : undefined;
|
||||
if (!session) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
|
||||
const { resourceVersion } = request.params as { resourceVersion: string };
|
||||
const manifest = options.assetReleases?.project("private_user_asset", resourceVersion, { ownerId: session.userId });
|
||||
if (!manifest) return reply.code(404).send();
|
||||
reply.header("Cache-Control", "private, no-store");
|
||||
reply.header("Vary", "Cookie");
|
||||
return manifest;
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/v1/private-assets/:resourceVersion/:assetId",
|
||||
{ schema: { hide: true } },
|
||||
async (request, reply) => {
|
||||
if (!options.registration) {
|
||||
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
|
||||
}
|
||||
const userToken = cookieValue(headerValue(request.headers.cookie), userSessionCookieName);
|
||||
const userSession = userToken ? options.registration.readUserSession(userToken) : undefined;
|
||||
const adminToken = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
|
||||
const adminSession = adminToken ? options.registration.readAdminSession(adminToken) : undefined;
|
||||
if (!userSession && !adminSession) {
|
||||
return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
|
||||
}
|
||||
const { assetId, resourceVersion } = request.params as { assetId: string; resourceVersion: string };
|
||||
const resource = options.assetReleases?.read("private_user_asset", resourceVersion, assetId);
|
||||
if (!resource?.ownerId) return reply.code(404).send();
|
||||
const controlledAdmin = adminSession
|
||||
? await options.privateAssetAdminAuthorizer?.({
|
||||
adminUserId: adminSession.user_id,
|
||||
ownerId: resource.ownerId,
|
||||
releaseVersion: resource.releaseVersion,
|
||||
resourceId: resource.resourceId,
|
||||
})
|
||||
: false;
|
||||
if (userSession?.userId !== resource.ownerId && !controlledAdmin) return reply.code(404).send();
|
||||
reply.type(resource.mimeType);
|
||||
reply.header("Cache-Control", "private, no-store");
|
||||
reply.header("Content-Disposition", "inline");
|
||||
reply.header("Vary", "Cookie");
|
||||
return resource.bytes;
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/v1/assets/recent",
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user