Skip to content

Commit

Permalink
Reapply "project: user: create default project id (#2163)"
Browse files Browse the repository at this point in the history
This reverts commit eed03f8.
  • Loading branch information
victorges committed Jun 11, 2024
1 parent faf3926 commit 3a9cbe0
Show file tree
Hide file tree
Showing 21 changed files with 432 additions and 67 deletions.
24 changes: 16 additions & 8 deletions packages/api/src/controllers/api-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@ import Router from "express/lib/router";
import { v4 as uuid } from "uuid";
import sql from "sql-template-strings";

import { makeNextHREF, parseOrder, parseFilters } from "./helpers";
import {
makeNextHREF,
parseOrder,
parseFilters,
addDefaultProjectId,
} from "./helpers";
import { authorizer, validatePost } from "../middleware";
import { AuthPolicy } from "../middleware/authPolicy";
import { db } from "../store";
import mung from "express-mung";

const app = Router();

app.use(authorizer({ noApiToken: true }));

app.use(mung.jsonAsync(addDefaultProjectId));

app.get("/:id", async (req, res) => {
const { id } = req.params;
const apiToken = await req.store.get(`api-token/${id}`);
Expand Down Expand Up @@ -67,10 +75,6 @@ app.get("/", async (req, res) => {

if (!userId) {
const query = parseFilters(fieldsMap, filters);
query.push(
sql`coalesce(api_token.data->>'projectId', '') = ${req.project?.id || ""}`
);

let fields =
" api_token.id as id, api_token.data as data, users.id as usersId, users.data as usersdata";
if (count) {
Expand Down Expand Up @@ -108,9 +112,13 @@ app.get("/", async (req, res) => {
}
const query = parseFilters(fieldsMap, filters);
query.push(sql`api_token.data->>'userId' = ${userId}`);
query.push(
sql`coalesce(api_token.data->>'projectId', '') = ${req.project?.id || ""}`
);
if (!req.user.admin) {
query.push(
sql`coalesce(api_token.data->>'projectId', ${
req.user.defaultProjectId || ""
}) = ${req.project?.id || ""}`
);
}

let fields = " api_token.id as id, api_token.data as data";
if (count) {
Expand Down
34 changes: 31 additions & 3 deletions packages/api/src/controllers/asset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe("controllers/asset", () => {
type: "url",
url: spec.url,
},
projectId: "", //should be blank when using jwt and projectId not specified as query-param
projectId: expect.any(String), //should have a default project id
status: { phase: "waiting" },
});

Expand Down Expand Up @@ -191,7 +191,7 @@ describe("controllers/asset", () => {
});
});

it("should import asset (using jwt) for existing project (created with jwt)", async () => {
it("should import asset (using jwt) for existing project (created with jwt) and list with filters", async () => {
const spec = {
name: "test",
url: "https://example.com/test.mp4",
Expand Down Expand Up @@ -221,6 +221,15 @@ describe("controllers/asset", () => {
res = await client.get(`/project/${projectId}`);
expect(res.status).toBe(200);
expect(await res.json()).toBeDefined(); //api-key be retrieve if adminApiKey is used..

res = await client.get(
`/asset?limit=10&allUsers=true&filters=[{"id":"playbackId","value":"${asset.playbackId}"}]`
);
expect(res.status).toBe(200);
let assets = await res.json();
expect(assets).toHaveLength(1);
expect(assets[0].projectId).toBe(projectId);
expect(assets[0].playbackId).toBe(asset.playbackId);
});

it("should import asset (using api-token) for existing project (created with jwt)", async () => {
Expand Down Expand Up @@ -254,6 +263,25 @@ describe("controllers/asset", () => {
expect(project.id).toBeDefined();
});

it("should import asset when projectId is not passed and list with projectId", async () => {
const spec = {
name: "test",
url: "https://example.com/test.mp4",
};
let res = await client.post(`/asset/upload/url`, spec);
expect(res.status).toBe(201);
const { asset, task } = await res.json();

client.jwtAuth = null;
client.apiKey = adminApiKey;

res = await client.get(`/asset?limit=10&allUsers=true`);
expect(res.status).toBe(200);
let assets = await res.json();
expect(assets).toHaveLength(1);
expect(assets[0].projectId).toBe(asset.projectId);
});

it("should NOT import asset (using api-key) when projectId passed as ouery-param", async () => {
const spec = {
name: "test",
Expand Down Expand Up @@ -282,7 +310,7 @@ describe("controllers/asset", () => {
type: "url",
url: spec.url,
},
projectId: "", //should be blank when using an existing api-key and new project was created
projectId: adminUser.defaultProjectId,
status: { phase: "waiting" },
});
});
Expand Down
37 changes: 24 additions & 13 deletions packages/api/src/controllers/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
reqUseReplica,
isValidBase64,
mapInputCreatorId,
addDefaultProjectId,
} from "./helpers";
import { db } from "../store";
import sql from "sql-template-strings";
Expand Down Expand Up @@ -239,7 +240,7 @@ export async function validateAssetPayload(
name: payload.name,
source,
staticMp4: payload.staticMp4,
projectId: req.project?.id ?? "",
projectId: req.project?.id,
creatorId: mapInputCreatorId(payload.creatorId),
playbackPolicy,
objectStoreId: payload.objectStoreId || (await defaultObjectStoreId(req)),
Expand Down Expand Up @@ -495,6 +496,7 @@ export async function createAsset(asset: WithID<Asset>, queue: Queue) {
timestamp: asset.createdAt,
event: "asset.created",
userId: asset.userId,
projectId: asset.projectId,
payload: {
asset: {
id: asset.id,
Expand Down Expand Up @@ -613,14 +615,24 @@ export async function toExternalAsset(
return a;
}

app.use(mung.jsonAsync(addDefaultProjectId));

app.use(
mung.jsonAsync(async function cleanWriteOnlyResponses(
data: WithID<Asset>[] | WithID<Asset> | { asset: WithID<Asset> },
req
) {
const { details } = toStringValues(req.query);
const toExternalAssetFunc = (a: Asset) =>
toExternalAsset(a, req.config, !!details, req.user.admin);
const toExternalAssetFunc = async (a: Asset) => {
const modifiedAsset = await toExternalAsset(
a,
req.config,
!!details,
req.user.admin
);

return modifiedAsset;
};

if (Array.isArray(data)) {
return Promise.all(data.map(toExternalAssetFunc));
Expand Down Expand Up @@ -700,9 +712,14 @@ app.get("/", authorizer({}), async (req, res) => {
query.push(sql`asset.data->>'deleted' IS NULL`);
}

query.push(
sql`coalesce(asset.data->>'projectId', '') = ${req.project?.id || ""}`
);
if (!req.user.admin) {
query.push(
sql`coalesce(asset.data->>'projectId', ${
req.user.defaultProjectId || ""
}) = ${req.project?.id || ""}`
);
}

if (req.user.admin && deleting === "true") {
const deletionThreshold = new Date(
Date.now() - DELETE_ASSET_DELAY
Expand Down Expand Up @@ -775,13 +792,7 @@ app.get("/:id", authorizer({}), async (req, res) => {
if (!asset || asset.deleted) {
throw new NotFoundError(`Asset not found`);
}

if (req.user.admin !== true && req.user.id !== asset.userId) {
throw new ForbiddenError(
"user can only request information on their own assets"
);
}

req.checkResourceAccess(asset);
res.json(asset);
});

Expand Down
1 change: 1 addition & 0 deletions packages/api/src/controllers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ app.all(
: userIds.find((id) => !!id) || req.user.id;
res.header("x-livepeer-user-id", userId);
res.header("x-livepeer-is-caller-admin", req.user.admin ? "true" : "false");
res.header("x-livepeer-project-id", req.project?.id || "");

res.status(204).end();
}
Expand Down
8 changes: 8 additions & 0 deletions packages/api/src/controllers/clip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ app.post(
user: owner,
token: req.token,
config: req.config,
project: req.project,
},
id,
uPlaybackId,
Expand Down Expand Up @@ -224,6 +225,7 @@ const fieldsMap = {
createdAt: { val: `asset.data->'createdAt'`, type: "int" },
updatedAt: { val: `asset.data->'status'->'updatedAt'`, type: "int" },
userId: `asset.data->>'userId'`,
projectId: `asset.data->>'projectId'`,
playbackId: `asset.data->>'playbackId'`,
sourceType: `asset.data->'source'->>'type'`,
sourceSessionId: `asset.data->'source'->>'sessionId'`,
Expand Down Expand Up @@ -269,6 +271,12 @@ export const getClips = async (
let output: WithID<Asset>[];
let newCursor: string;

if (!req.user.admin) {
query.push(
sql`coalesce(asset.data->>'projectId', '') = ${req.project?.id || ""}`
);
}

query.push(sql`asset.data->>'userId' = ${req.user.id}`);

if (!content) {
Expand Down
59 changes: 59 additions & 0 deletions packages/api/src/controllers/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { ObjectStore, User } from "../schema/types";
import { db } from "../store";
import { v4 as uuid } from "uuid";
import {
addDefaultProjectId,
deleteCredentials,
getS3PresignedUrl,
toObjectStoreUrl,
toWeb3StorageUrl,
} from "./helpers";
import { Request, Response } from "express";

let server: TestServer;

Expand Down Expand Up @@ -231,4 +233,61 @@ describe("convert w3 storage to object store URL", () => {
"undefined property 'credentials.proof'"
);
});

it("should append default project id", async () => {
const mockAsset = {
id: "asset1",
userId: "test",
user: {
id: "test",
admin: false,
defaultProjectId: "defaultProject1",
},
projectId: undefined,
};
const mockAsset2 = {
id: "asset2",
userId: "test",
projectId: undefined,
user: {
id: "test",
admin: false,
defaultProjectId: "defaultProject1",
},
};

const assetList = [mockAsset, mockAsset2];

let mockReq: Request = {
user: {
id: "test",
admin: false,
defaultProjectId: "defaultProject1",
},
project: {
id: "test",
},
} as Request;

let mockRes: Response = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
} as any as Response;

// nonAdmin + singleAsset
let result = await addDefaultProjectId(mockAsset, mockReq, mockRes);
expect(result.projectId).toBe("defaultProject1");
expect(assetList[0].projectId).toBe(undefined);
// nonAdmin + multipleAssets
result = await addDefaultProjectId(assetList, mockReq, mockRes);
expect(result[0].projectId).toBe("defaultProject1");
mockReq.user.admin = true;
mockReq.user.defaultProjectId = "adminProjectId";
// admin + singleAsset
result = await addDefaultProjectId(mockAsset, mockReq, mockRes);
expect(result.projectId).toBe("defaultProject1");
// admin + multipleAssets
result = await addDefaultProjectId(assetList, mockReq, mockRes);
expect(result[0].projectId).toBe("defaultProject1");
});
});
Loading

0 comments on commit 3a9cbe0

Please sign in to comment.