diff --git a/packages/api/src/controllers/session.ts b/packages/api/src/controllers/session.ts index c82d2afc62..c32b3cd00f 100644 --- a/packages/api/src/controllers/session.ts +++ b/packages/api/src/controllers/session.ts @@ -1,8 +1,8 @@ import { Router, Request } from "express"; import sql from "sql-template-strings"; -import { authorizer, hasAccessToResource } from "../middleware"; -import { User, Project } from "../schema/types"; +import { authorizer } from "../middleware"; +import { User } from "../schema/types"; import { db } from "../store"; import { DBSession } from "../store/session-table"; import { pathJoin } from "../controllers/helpers"; @@ -36,7 +36,6 @@ const fieldsMap: FieldsMap = { userId: `session.data->>'userId'`, "user.email": { val: `users.data->>'email'`, type: "full-text" }, parentId: `session.data->>'parentId'`, - projectId: `session.data->>'projectId'`, record: { val: `session.data->'record'`, type: "boolean" }, sourceSegments: `session.data->'sourceSegments'`, transcodedSegments: { @@ -66,14 +65,10 @@ app.get("/", authorizer({}), async (req, res, next) => { limit = undefined; } - const query = parseFilters(fieldsMap, filters); - if (!req.user.admin) { userId = req.user.id; - query.push( - sql`coalesce(session.data->>'projectId', '') = ${req.project?.id || ""}` - ); } + const query = parseFilters(fieldsMap, filters); if (!all || all === "false" || !req.user.admin) { query.push(sql`(session.data->>'deleted')::boolean IS false`); } @@ -151,7 +146,8 @@ app.get("/:id", authorizer({}), async (req, res) => { let session = await db.session.get(req.params.id); if ( !session || - (!hasAccessToResource(req, session) && + ((session.userId !== req.user.id || session.deleted) && + !req.user.admin && !LVPR_SDK_EMAILS.includes(req.user.email)) ) { // do not reveal that session exists diff --git a/packages/api/src/controllers/signing-key.test.ts b/packages/api/src/controllers/signing-key.test.ts index 7f07f8d96d..203a37eeed 100644 --- a/packages/api/src/controllers/signing-key.test.ts +++ b/packages/api/src/controllers/signing-key.test.ts @@ -2,20 +2,13 @@ import serverPromise, { TestServer } from "../test-server"; import { TestClient, clearDatabase, - createApiToken, setupUsers, verifyJwt, } from "../test-helpers"; -import { - ApiToken, - SigningKey, - SigningKeyResponsePayload, - User, -} from "../schema/types"; +import { SigningKey, SigningKeyResponsePayload, User } from "../schema/types"; import { WithID } from "../store/types"; import jwt, { JsonWebTokenError, JwtPayload } from "jsonwebtoken"; import { db } from "../store"; -import { createProject } from "../test-helpers"; // includes auth file tests @@ -55,8 +48,6 @@ describe("controllers/signing-key", () => { let samplePrivateKey: string; let decodedPublicKey: string; let otherPublicKey: string; - let projectId: string; - let apiKeyWithProject: WithID; beforeEach(async () => { ({ client, adminUser, adminToken, nonAdminUser, nonAdminToken } = @@ -78,27 +69,10 @@ describe("controllers/signing-key", () => { otherSigningKey.publicKey, "base64" ).toString(); - // create a new project - client.jwtAuth = nonAdminToken; - let project = await createProject(client); - expect(project).toBeDefined(); - projectId = project.id; - apiKeyWithProject = await createApiToken({ - client: client, - projectId: project.id, - jwtAuthToken: nonAdminToken, - }); - expect(apiKeyWithProject).toMatchObject({ - id: expect.any(String), - projectId: projectId, - }); - projectId = project.id; }); it("should create a signing key and display the private key only on creation", async () => { const preCreationTime = Date.now(); - client.jwtAuth = ""; - client.apiKey = apiKeyWithProject.id; let res = await client.post("/access-control/signing-key"); expect(res.status).toBe(201); const created = (await res.json()) as SigningKeyResponsePayload; @@ -107,7 +81,6 @@ describe("controllers/signing-key", () => { privateKey: expect.any(String), publicKey: expect.any(String), createdAt: expect.any(Number), - projectId: projectId, }); expect(created.createdAt).toBeGreaterThanOrEqual(preCreationTime); res = await client.get(`/access-control/signing-key/${created.id}`); @@ -118,26 +91,11 @@ describe("controllers/signing-key", () => { }); it("should list all user signing keys", async () => { - client.jwtAuth = nonAdminToken; - client.apiKey = null; - let sigKeyWithoutProject = await client.post( - "/access-control/signing-key" - ); - expect(sigKeyWithoutProject.status).toBe(201); - client.jwtAuth = ""; - client.apiKey = apiKeyWithProject.id; - let sigkey = await client.post("/access-control/signing-key"); - expect(sigkey.status).toBe(201); const res = await client.get(`/access-control/signing-key`); expect(res.status).toBe(200); - const output = await res.json(); - expect(output).toHaveLength(1); - expect(output[0].projectId).toBe(projectId); }); it("should create a JWT using the private key and verify it with the public key", async () => { - client.jwtAuth = ""; - client.apiKey = apiKeyWithProject.id; const expiration = Math.floor(Date.now() / 1000) + 1000; const payload: JwtPayload = { sub: "b0dcxvwml48mxt2s", @@ -162,11 +120,6 @@ describe("controllers/signing-key", () => { }); it("should allow disable and enable the signing key & change the name", async () => { - client.jwtAuth = ""; - client.apiKey = apiKeyWithProject.id; - let sigkey = await client.post("/access-control/signing-key"); - expect(sigkey.status).toBe(201); - let signingKey = await sigkey.json(); let res = await client.patch( `/access-control/signing-key/${signingKey.id}`, { @@ -191,11 +144,6 @@ describe("controllers/signing-key", () => { }); it("should delete the signing key", async () => { - client.jwtAuth = ""; - client.apiKey = apiKeyWithProject.id; - let sigkey = await client.post("/access-control/signing-key"); - expect(sigkey.status).toBe(201); - let signingKey = await sigkey.json(); let res = await client.delete( `/access-control/signing-key/${signingKey.id}` ); diff --git a/packages/api/src/controllers/signing-key.ts b/packages/api/src/controllers/signing-key.ts index 07f7d953c5..ffedd4a43a 100644 --- a/packages/api/src/controllers/signing-key.ts +++ b/packages/api/src/controllers/signing-key.ts @@ -25,7 +25,6 @@ const fieldsMap: FieldsMap = { name: { val: `signing_key.data->>'name'`, type: "full-text" }, deleted: { val: `signing_key.data->'deleted'`, type: "boolean" }, createdAt: { val: `signing_key.data->'createdAt'`, type: "int" }, - projectId: `signing_key.data->>'projectId'`, userId: `signing_key.data->>'userId'`, }; @@ -72,12 +71,6 @@ signingKeyApp.get("/", authorizer({}), async (req, res) => { query.push(sql`signing_key.data->>'deleted' IS NULL`); } - query.push( - sql`coalesce(signing_key.data->>'projectId', '') = ${ - req.project?.id || "" - }` - ); - let fields = " signing_key.id as id, signing_key.data as data, users.id as usersId, users.data as usersdata"; if (count) { @@ -113,10 +106,6 @@ signingKeyApp.get("/", authorizer({}), async (req, res) => { query.push(sql`signing_key.data->>'userId' = ${req.user.id}`); query.push(sql`signing_key.data->>'deleted' IS NULL`); - query.push( - sql`coalesce(signing_key.data->>'projectId', '') = ${req.project?.id || ""}` - ); - let fields = " signing_key.id as id, signing_key.data as data"; if (count) { fields = fields + ", count(*) OVER() AS count"; @@ -148,7 +137,16 @@ signingKeyApp.get("/", authorizer({}), async (req, res) => { signingKeyApp.get("/:id", authorizer({}), async (req, res) => { const signingKey = await db.signingKey.get(req.params.id); - req.checkResourceAccess(signingKey); + if ( + !signingKey || + signingKey.deleted || + (req.user.admin !== true && req.user.id !== signingKey.userId) + ) { + res.status(404); + return res.json({ + errors: ["not found"], + }); + } res.json(signingKey); }); @@ -190,7 +188,6 @@ signingKeyApp.post( userId: req.user.id, createdAt: Date.now(), publicKey: b64PublicKey, - projectId: req.project?.id ?? "", }; await db.signingKey.create(doc); @@ -208,7 +205,9 @@ signingKeyApp.post( signingKeyApp.delete("/:id", authorizer({}), async (req, res) => { const { id } = req.params; const signingKey = await db.signingKey.get(id); - req.checkResourceAccess(signingKey); + if (!signingKey || signingKey.deleted) { + throw new NotFoundError(`signing key not found`); + } if (!req.user.admin && req.user.id !== signingKey.userId) { throw new ForbiddenError(`users may only delete their own signing keys`); } @@ -224,7 +223,9 @@ signingKeyApp.patch( async (req, res) => { const { id } = req.params; const signingKey = await db.signingKey.get(id); - req.checkResourceAccess(signingKey); + if (!signingKey || signingKey.deleted) { + return res.status(404).json({ errors: ["not found"] }); + } if (!req.user.admin && req.user.id !== signingKey.userId) { return res.status(403).json({ errors: ["users may change only their own signing key"], diff --git a/packages/api/src/controllers/stream.test.ts b/packages/api/src/controllers/stream.test.ts index 4dc5fc8b82..d15227d120 100644 --- a/packages/api/src/controllers/stream.test.ts +++ b/packages/api/src/controllers/stream.test.ts @@ -19,8 +19,6 @@ import { clearDatabase, setupUsers, startAuxTestServer, - createProject, - createApiToken, } from "../test-helpers"; import serverPromise, { TestServer } from "../test-server"; import { semaphore, sleep } from "../util"; @@ -115,7 +113,6 @@ describe("controllers/stream", () => { let nonAdminUser: User; let nonAdminToken: string; let nonAdminApiKey: string; - let projectId: string; beforeEach(async () => { await server.store.create(mockStore); @@ -130,9 +127,6 @@ describe("controllers/stream", () => { nonAdminApiKey, } = await setupUsers(server, mockAdminUser, mockNonAdminUser)); client.jwtAuth = adminToken; - - projectId = await createProject(client); - expect(projectId).toBeDefined(); }); describe("basic CRUD with JWT authorization", () => { @@ -142,7 +136,6 @@ describe("controllers/stream", () => { const document = { id: uuid(), kind: "stream", - projectId: i > 7 ? projectId : undefined, }; await server.store.create(document); const res = await client.get(`/stream/${document.id}`); @@ -158,31 +151,6 @@ describe("controllers/stream", () => { id: uuid(), kind: "stream", deleted: i > 3 ? true : undefined, - projectId: i > 2 ? projectId : undefined, - } as DBStream; - await server.store.create(document); - const res = await client.get(`/stream/${document.id}`); - const stream = await res.json(); - expect(stream).toEqual(server.db.stream.addDefaultFields(document)); - } - - const res = await client.get("/stream"); - expect(res.status).toBe(200); - const streams = await res.json(); - expect(streams.length).toEqual(4); - const resAll = await client.get("/stream?all=1"); - expect(resAll.status).toBe(200); - const streamsAll = await resAll.json(); - expect(streamsAll.length).toEqual(5); - }); - - it("should get all streams with admin authorization and specific projectId in query param", async () => { - for (let i = 0; i < 5; i += 1) { - const document = { - id: uuid(), - kind: "stream", - deleted: i > 3 ? true : undefined, - projectId: i > 2 ? projectId : undefined, } as DBStream; await server.store.create(document); const res = await client.get(`/stream/${document.id}`); @@ -1433,9 +1401,7 @@ describe("controllers/stream", () => { }); describe("stream endpoint with api key", () => { - let newApiKey; beforeEach(async () => { - // create streams without a projectId for (let i = 0; i < 5; i += 1) { const document = { id: uuid(), @@ -1446,45 +1412,7 @@ describe("controllers/stream", () => { const res = await client.get(`/stream/${document.id}`); expect(res.status).toBe(200); } - - // create a new project - client.jwtAuth = nonAdminToken; - let project = await createProject(client); - expect(project).toBeDefined(); - - // then create a new api-key under that project - newApiKey = await createApiToken({ - client: client, - projectId: project.id, - jwtAuthToken: nonAdminToken, - }); - expect(newApiKey).toMatchObject({ - id: expect.any(String), - projectId: project.id, - }); - client.jwtAuth = ""; - client.apiKey = newApiKey.id; - - // create streams with a projectId - for (let i = 0; i < 5; i += 1) { - const document = { - id: uuid(), - kind: "stream", - userId: nonAdminUser.id, - projectId: project.id, - }; - const resCreate = await client.post("/stream", { - ...postMockStream, - name: "videorec+samplePlaybackId", - }); - expect(resCreate.status).toBe(201); - const createdStream = await resCreate.json(); - const res = await client.get(`/stream/${createdStream.id}`); - expect(res.status).toBe(200); - let stream = await res.json(); - expect(stream.projectId).toEqual(project.id); - } }); it("should get own streams", async () => { @@ -1494,22 +1422,6 @@ describe("controllers/stream", () => { const streams = await res.json(); expect(streams.length).toEqual(3); expect(streams[0].userId).toEqual(nonAdminUser.id); - - client.apiKey = newApiKey.id; - let res2 = await client.get(`/stream/user/${nonAdminUser.id}`); - expect(res2.status).toBe(200); - const streams2 = await res2.json(); - expect(streams2.length).toEqual(5); - expect(streams2[0].userId).toEqual(nonAdminUser.id); - }); - - it("should get streams owned by project when using api-key for project", async () => { - client.apiKey = newApiKey.id; - let res = await client.get(`/stream/`); - expect(res.status).toBe(200); - const streams = await res.json(); - expect(streams.length).toEqual(5); - expect(streams[0].userId).toEqual(nonAdminUser.id); }); it("should delete stream", async () => { diff --git a/packages/api/src/controllers/stream.ts b/packages/api/src/controllers/stream.ts index 138005fc9a..4f7b31feb3 100644 --- a/packages/api/src/controllers/stream.ts +++ b/packages/api/src/controllers/stream.ts @@ -6,12 +6,7 @@ import { parse as parseUrl } from "url"; import { v4 as uuid } from "uuid"; import logger from "../logger"; -import { - authorizer, - geolocateMiddleware, - hasAccessToResource, - validatePost, -} from "../middleware"; +import { authorizer, geolocateMiddleware, validatePost } from "../middleware"; import { CliArgs } from "../parse-cli"; import { DetectionWebhookPayload, @@ -190,7 +185,7 @@ async function validateMultistreamOpts( async function validateStreamPlaybackPolicy( playbackPolicy: DBStream["playbackPolicy"], - req: Request + userId: string ) { if ( playbackPolicy?.type === "lit_signing_condition" || @@ -208,7 +203,7 @@ async function validateStreamPlaybackPolicy( `webhook ${playbackPolicy.webhookId} not found` ); } - if (!hasAccessToResource(req, webhook)) { + if (webhook.userId !== userId) { throw new BadRequestError( `webhook ${playbackPolicy.webhookId} not found` ); @@ -458,7 +453,6 @@ const fieldsMap: FieldsMap = { "user.email": { val: `users.data->>'email'`, type: "full-text" }, parentId: `stream.data->>'parentId'`, playbackId: `stream.data->>'playbackId'`, - projectId: `stream.data->>'projectId'`, record: { val: `stream.data->'record'`, type: "boolean" }, suspended: { val: `stream.data->'suspended'`, type: "boolean" }, sourceSegmentsDuration: { @@ -492,14 +486,11 @@ app.get("/", authorizer({}), async (req, res) => { limit = undefined; } - const query = parseFilters(fieldsMap, filters); - if (!req.user.admin) { userId = req.user.id; - query.push( - sql`coalesce(stream.data->>'projectId', '') = ${req.project?.id || ""}` - ); } + + const query = parseFilters(fieldsMap, filters); if (!all || all === "false" || !req.user.admin) { query.push(sql`stream.data->>'deleted' IS NULL`); } @@ -661,7 +652,14 @@ app.get("/:parentId/sessions", authorizer({}), async (req, res) => { const raw = req.query.raw && req.user.admin; const stream = await db.stream.get(parentId); - req.checkResourceAccess(stream, true); + if ( + !stream || + (stream.deleted && !req.isUIAdmin) || + (stream.userId !== req.user.id && !req.isUIAdmin) + ) { + res.status(404); + return res.json({ errors: ["not found"] }); + } let filterOut; const query = []; @@ -738,7 +736,14 @@ app.get("/sessions/:parentId", authorizer({}), async (req, res) => { logger.info(`cursor params ${cursor}, limit ${limit}`); const stream = await db.stream.get(parentId); - req.checkResourceAccess(stream, true); + if ( + !stream || + stream.deleted || + (stream.userId !== req.user.id && !req.isUIAdmin) + ) { + res.status(404); + return res.json({ errors: ["not found"] }); + } const { data, cursor: nextCursor } = await req.store.queryObjects({ kind: "stream", @@ -761,18 +766,16 @@ app.get("/user/:userId", authorizer({}), async (req, res) => { const { userId } = req.params; let { limit, cursor, streamsonly, sessionsonly } = toStringValues(req.query); - let projectId = req.token?.projectId; - if (req.user.admin !== true && req.user.id !== req.params.userId) { res.status(403); return res.json({ errors: ["user can only request information on their own streams"], }); } + const query = [ sql`data->>'deleted' IS NULL`, sql`data->>'userId' = ${userId}`, - sql`coalesce(data->>'projectId', '') = ${projectId || ""}`, ]; if (streamsonly) { query.push(sql`data->>'parentId' IS NULL`); @@ -803,7 +806,15 @@ app.get("/:id", authorizer({}), async (req, res) => { const raw = req.query.raw && req.user.admin; const { forceUrl } = req.query; let stream = await db.stream.get(req.params.id); - req.checkResourceAccess(stream); + if ( + !stream || + ((stream.userId !== req.user.id || stream.deleted) && !req.user.admin) + ) { + // do not reveal that stream exists + res.status(404); + return res.json({ errors: ["not found"] }); + } + // fixup 'user' session if (!raw && stream.lastSessionId) { const lastSession = await db.stream.get(stream.lastSessionId); @@ -847,7 +858,13 @@ app.get("/playback/:playbackId", authorizer({}), async (req, res) => { 5 ); - req.checkResourceAccess(stream); + if ( + !stream || + ((stream.userId !== req.user.id || stream.deleted) && !req.user.admin) + ) { + res.status(404); + return res.json({ errors: ["not found"] }); + } res.status(200); res.json( db.stream.addDefaultFields( @@ -863,7 +880,13 @@ app.get("/key/:streamKey", authorizer({}), async (req, res) => { { streamKey: req.params.streamKey }, { useReplica } ); - req.checkResourceAccess(docs[0]); + if ( + !docs.length || + ((docs[0].userId !== req.user.id || docs[0].deleted) && !req.user.admin) + ) { + res.status(404); + return res.json({ errors: ["not found"] }); + } res.status(200); res.json( db.stream.addDefaultFields( @@ -913,7 +936,15 @@ app.post( stream = await db.stream.get(req.params.streamId); } - req.checkResourceAccess(stream); + if ( + !stream || + ((stream.userId !== req.user.id || stream.deleted) && + !(req.user.admin && !stream.deleted)) + ) { + // do not reveal that stream exists + res.status(404); + return res.json({ errors: ["not found"] }); + } const sessionId = req.query.sessionId?.toString(); const region = req.config.ownRegion; @@ -926,7 +957,6 @@ app.post( id: parentId, playbackId, userId, - projectId, objectStoreId, record, recordingSpec, @@ -940,7 +970,6 @@ app.post( ...req.body, kind: "stream", userId, - projectId, renditions: {}, profiles, objectStoreId, @@ -959,7 +988,7 @@ app.post( const existingSession = await db.session.get(sessionId); if (existingSession) { logger.info( - `user session re-used for session.id=${sessionId} session.parentId=${existingSession.parentId} session.name=${existingSession.name} session.playbackId=${existingSession.playbackId} session.userId=${existingSession.userId} stream.id=${stream.id} stream.name='${stream.name}' stream.playbackId=${stream.playbackId} stream.userId=${stream.userId} stream.projectId=${stream.projectId}` + `user session re-used for session.id=${sessionId} session.parentId=${existingSession.parentId} session.name=${existingSession.name} session.playbackId=${existingSession.playbackId} session.userId=${existingSession.userId} stream.id=${stream.id} stream.name='${stream.name}' stream.playbackId=${stream.playbackId} stream.userId=${stream.userId}` ); } else { const session: DBSession = { @@ -967,7 +996,6 @@ app.post( parentId, playbackId, userId, - projectId, kind: "session", version: "v2", name: req.body.name, @@ -1009,9 +1037,9 @@ app.post( logger.info( `stream session created for stream_id=${stream.id} stream_name='${ stream.name - }' playbackid=${stream.playbackId} session_id=${id} projectid=${ - stream.projectId - } elapsed=${Date.now() - start}ms` + }' playbackid=${stream.playbackId} session_id=${id} elapsed=${ + Date.now() - start + }ms` ); } ); @@ -1163,7 +1191,6 @@ app.put( [ sql`data->>'userId' = ${req.user.id}`, sql`data->>'deleted' IS NULL`, - sql`coalesce(data->>'projectId', '') = ${req.project?.id || ""}`, ...filters, ], { useReplica: false } @@ -1335,7 +1362,6 @@ app.post( sql`data->>'userId' = ${req.user.id}`, sql`data->>'deleted' IS NULL`, sql`data->'pull'->>'source' = ${payload.pull.source}`, - sql`coalesce(data->>'projectId', '') = ${req.project?.id || ""}`, ], { useReplica: false } ); @@ -1409,7 +1435,6 @@ async function handleCreateStream(req: Request, payload: NewStreamPayload) { renditions: {}, objectStoreId, id, - projectId: req.project?.id ?? "", createdAt, streamKey, playbackId, @@ -1419,7 +1444,7 @@ async function handleCreateStream(req: Request, payload: NewStreamPayload) { }; doc = wowzaHydrate(doc); - await validateStreamPlaybackPolicy(doc.playbackPolicy, req); + await validateStreamPlaybackPolicy(doc.playbackPolicy, req.user.id); doc.profiles = hackMistSettings(req, doc.profiles); doc.multistream = await validateMultistreamOpts( @@ -1824,7 +1849,10 @@ app.post( return res.json({ errors: ["stream not found"] }); } - req.checkResourceAccess(stream); + if (stream.userId !== req.user.id) { + res.status(404); + return res.json({ errors: ["stream not found"] }); + } const newTarget = await validateMultistreamTarget( req.user.id, @@ -1860,7 +1888,15 @@ app.delete("/:id/multistream/:targetId", authorizer({}), async (req, res) => { const stream = await db.stream.get(id); - req.checkResourceAccess(stream); + if (!stream || stream.deleted) { + res.status(404); + return res.json({ errors: ["stream not found"] }); + } + + if (stream.userId !== req.user.id) { + res.status(404); + return res.json({ errors: ["stream not found"] }); + } let multistream: DBStream["multistream"] = stream.multistream ?? { targets: [], @@ -1896,7 +1932,7 @@ app.patch( const stream = await db.stream.get(id); const exists = stream && !stream.deleted; - const hasAccess = hasAccessToResource(req, stream); + const hasAccess = stream?.userId === req.user.id || req.user.admin; if (!exists || !hasAccess) { res.status(404); return res.json({ errors: ["not found"] }); @@ -1945,7 +1981,7 @@ app.patch( } if (playbackPolicy) { - await validateStreamPlaybackPolicy(playbackPolicy, req); + await validateStreamPlaybackPolicy(playbackPolicy, req.user.id); patch = { ...patch, playbackPolicy }; } @@ -1976,7 +2012,10 @@ app.patch( app.patch("/:id/record", authorizer({}), async (req, res) => { const { id } = req.params; const stream = await db.stream.get(id); - req.checkResourceAccess(stream); + if (!stream || stream.deleted) { + res.status(404); + return res.json({ errors: ["not found"] }); + } if (stream.parentId) { res.status(400); return res.json({ errors: ["can't set for session"] }); @@ -2003,7 +2042,14 @@ app.patch("/:id/record", authorizer({}), async (req, res) => { app.delete("/:id", authorizer({}), async (req, res) => { const { id } = req.params; const stream = await db.stream.get(id); - req.checkResourceAccess(stream); + if ( + !stream || + stream.deleted || + (stream.userId !== req.user.id && !req.user.admin) + ) { + res.status(404); + return res.json({ errors: ["not found"] }); + } await db.stream.update(stream.id, { deleted: true, @@ -2029,7 +2075,7 @@ app.delete("/", authorizer({}), async (req, res) => { const streams = await db.stream.getMany(ids); if ( streams.length !== ids.length || - streams.some((s) => !hasAccessToResource(req, s)) + streams.some((s) => s.userId !== req.user.id) ) { res.status(404); return res.json({ errors: ["not found"] }); @@ -2060,7 +2106,16 @@ app.get("/:id/info", authorizer({}), async (req, res) => { isSession = true; stream = await db.stream.get(stream.parentId); } - req.checkResourceAccess(stream); + if ( + !stream || + (!req.user.admin && (stream.deleted || stream.userId !== req.user.id)) + ) { + res.status(404); + return res.json({ + errors: ["not found"], + }); + } + if (!session) { // find last session session = await db.stream.getLastSession(stream.id); @@ -2121,7 +2176,13 @@ app.patch("/:id/suspended", authorizer({}), async (req, res) => { } const { suspended } = req.body; const stream = await db.stream.get(id); - req.checkResourceAccess(stream); + if ( + !stream || + (!req.user.admin && (stream.deleted || stream.userId !== req.user.id)) + ) { + res.status(404); + return res.json({ errors: ["not found"] }); + } await db.stream.update(stream.id, { suspended }); if (suspended) { // now kill live stream @@ -2138,7 +2199,13 @@ app.post( async (req, res) => { const { id } = req.params; const stream = await db.stream.get(id); - req.checkResourceAccess(stream); + if ( + !stream || + (!req.user.admin && (stream.deleted || stream.userId !== req.user.id)) + ) { + res.status(404); + return res.json({ errors: ["not found"] }); + } if (!stream.pull) { res.status(400); @@ -2155,7 +2222,13 @@ app.post( app.delete("/:id/terminate", authorizer({}), async (req, res) => { const { id } = req.params; const stream = await db.stream.get(id); - req.checkResourceAccess(stream); + if ( + !stream || + (!req.user.admin && (stream.deleted || stream.userId !== req.user.id)) + ) { + res.status(404); + return res.json({ errors: ["not found"] }); + } if (terminateDelay(stream) > 0) { throw new TooManyRequestsError(`too many terminate requests`); diff --git a/packages/api/src/controllers/webhook.ts b/packages/api/src/controllers/webhook.ts index bb3e317783..f39f6e7fa6 100644 --- a/packages/api/src/controllers/webhook.ts +++ b/packages/api/src/controllers/webhook.ts @@ -1,5 +1,5 @@ import { URL } from "url"; -import { authorizer, hasAccessToResource } from "../middleware"; +import { authorizer } from "../middleware"; import { validatePost } from "../middleware"; import Router from "express/lib/router"; import logger from "../logger"; @@ -10,7 +10,7 @@ import sql from "sql-template-strings"; import { UnprocessableEntityError, NotFoundError } from "../store/errors"; import webhookLog from "./webhook-log"; -function validateWebhookPayload(id, userId, projectId, createdAt, payload) { +function validateWebhookPayload(id, userId, createdAt, payload) { try { new URL(payload.url); } catch (e) { @@ -27,7 +27,6 @@ function validateWebhookPayload(id, userId, projectId, createdAt, payload) { return { id, userId, - projectId: projectId ?? "", createdAt, kind: "webhook", name: payload.name, @@ -51,7 +50,6 @@ const fieldsMap: FieldsMap = { createdAt: { val: `webhook.data->'createdAt'`, type: "int" }, userId: `webhook.data->>'userId'`, "user.email": { val: `users.data->>'email'`, type: "full-text" }, - projectId: `webhook.data->>'projectId'`, sharedSecret: `webhook.data->>'sharedSecret'`, }; @@ -67,9 +65,6 @@ app.get("/", authorizer({}), async (req, res) => { if (!all || all === "false") { query.push(sql`webhook.data->>'deleted' IS NULL`); } - query.push( - sql`coalesce(webhook.data->>'projectId', '') = ${req.project?.id || ""}` - ); let fields = " webhook.id as id, webhook.data as data, users.id as usersId, users.data as usersdata"; @@ -101,9 +96,6 @@ app.get("/", authorizer({}), async (req, res) => { const query = parseFilters(fieldsMap, filters); query.push(sql`webhook.data->>'userId' = ${req.user.id}`); - query.push( - sql`coalesce(webhook.data->>'projectId', '') = ${req.project?.id || ""}` - ); if (!all || all === "false" || !req.user.admin) { query.push(sql`webhook.data->>'deleted' IS NULL`); @@ -153,13 +145,7 @@ app.get("/subscribed/:event", authorizer({}), async (req, res) => { app.post("/", authorizer({}), validatePost("webhook"), async (req, res) => { const id = uuid(); - const doc = validateWebhookPayload( - id, - req.user.id, - req.project?.id, - Date.now(), - req.body - ); + const doc = validateWebhookPayload(id, req.user.id, Date.now(), req.body); try { await req.store.create(doc); } catch (e) { @@ -175,7 +161,13 @@ app.get("/:id", authorizer({}), async (req, res) => { logger.info(`webhook params ${req.params.id}`); const webhook = await db.webhook.get(req.params.id); - req.checkResourceAccess(webhook); + if ( + !webhook || + ((webhook.deleted || webhook.userId !== req.user.id) && !req.user.admin) + ) { + res.status(404); + return res.json({ errors: ["not found"] }); + } res.status(200); res.json(webhook); @@ -184,16 +176,14 @@ app.get("/:id", authorizer({}), async (req, res) => { app.put("/:id", authorizer({}), validatePost("webhook"), async (req, res) => { // modify a specific webhook const webhook = await req.store.get(`webhook/${req.body.id}`); - req.checkResourceAccess(webhook); + if ((webhook.userId !== req.user.id || webhook.deleted) && !req.user.admin) { + // do not reveal that webhooks exists + res.status(404); + return res.json({ errors: ["not found"] }); + } - const { id, userId, projectId, createdAt } = webhook; - const doc = validateWebhookPayload( - id, - userId, - projectId, - createdAt, - req.body - ); + const { id, userId, createdAt } = webhook; + const doc = validateWebhookPayload(id, userId, createdAt, req.body); try { await req.store.replace(doc); } catch (e) { @@ -215,9 +205,15 @@ app.patch( throw new NotFoundError(`webhook not found`); } - req.checkResourceAccess(webhook); + if ( + (webhook.userId !== req.user.id || webhook.deleted) && + !req.user.admin + ) { + // do not reveal that webhooks exists + throw new NotFoundError(`webhook not found`); + } - const { id, userId, projectId, createdAt, kind } = webhook; + const { id, userId, createdAt, kind } = webhook; if (req.body.streamId) { const stream = await db.stream.get(req.body.streamId); @@ -242,7 +238,14 @@ app.patch( app.delete("/:id", authorizer({}), async (req, res) => { // delete a specific webhook const webhook = await db.webhook.get(req.params.id); - req.checkResourceAccess(webhook, true); + if ( + !webhook || + ((webhook.deleted || webhook.userId !== req.user.id) && !req.isUIAdmin) + ) { + // do not reveal that webhooks exists + res.status(404); + return res.json({ errors: ["not found"] }); + } try { await db.webhook.markDeleted(webhook.id); @@ -267,7 +270,7 @@ app.delete("/", authorizer({}), async (req, res) => { const webhooks = await db.webhook.getMany(ids); if ( webhooks.length !== ids.length || - webhooks.some((s) => !hasAccessToResource(req, s)) + webhooks.some((s) => s.deleted || s.userId !== req.user.id) ) { res.status(404); return res.json({ errors: ["not found"] }); diff --git a/packages/api/src/middleware/auth.ts b/packages/api/src/middleware/auth.ts index 19ef0b958d..2d97289d49 100644 --- a/packages/api/src/middleware/auth.ts +++ b/packages/api/src/middleware/auth.ts @@ -1,7 +1,7 @@ import { URL } from "url"; import basicAuth from "basic-auth"; import corsLib, { CorsOptions } from "cors"; -import { Request, RequestHandler } from "express"; +import { Request, RequestHandler, Response } from "express"; import jwt, { JwtPayload, TokenExpiredError } from "jsonwebtoken"; import { pathJoin2, trimPathPrefix } from "../controllers/helpers"; import { ApiToken, User, Project } from "../schema/types"; @@ -10,9 +10,8 @@ import { ForbiddenError, BadRequestError, UnauthorizedError, - NotFoundError, } from "../store/errors"; -import { DBOwnedResource, WithID } from "../store/types"; +import { WithID } from "../store/types"; import { AuthRule, AuthPolicy } from "./authPolicy"; import tracking from "./tracking"; @@ -62,7 +61,7 @@ function isAuthorized( export async function getProject(req: Request, projectId: string) { const project = projectId - ? await db.project.get(projectId, { useCache: true }) + ? await db.project.get(projectId) : { id: "", name: "default", userId: req.user.id }; if (!req.user.admin && req.user.id !== project.userId) { throw new ForbiddenError(`invalid user`); @@ -71,23 +70,6 @@ export async function getProject(req: Request, projectId: string) { return project; } -export function hasAccessToResource( - { isUIAdmin, user, project }: Pick, - resource?: DBOwnedResource, - uiAdminOnly = false -) { - if (!resource || !user) { - return false; - } - const isAdmin = uiAdminOnly ? isUIAdmin : user.admin; - return ( - isAdmin || - (!resource.deleted && - resource.userId === user.id && - (resource.projectId ?? "") === (project?.id ?? "")) - ); -} - /** * Creates a middleware that parses and verifies the authentication method from * the request and populates the `express.Request` object. @@ -197,15 +179,6 @@ function authenticator(): RequestHandler { // UI admins must have a JWT req.isUIAdmin = user.admin && authScheme === "jwt"; - req.checkResourceAccess = ( - resource?: DBOwnedResource, - uiAdminOnly = false - ) => { - if (!hasAccessToResource(req, resource, uiAdminOnly)) { - throw new NotFoundError("not found"); - } - }; - return next(); }; } diff --git a/packages/api/src/schema/api-schema.yaml b/packages/api/src/schema/api-schema.yaml index c973404484..25cc9cda00 100644 --- a/packages/api/src/schema/api-schema.yaml +++ b/packages/api/src/schema/api-schema.yaml @@ -185,10 +185,6 @@ components: type: string readOnly: true deprecated: true - projectId: - type: string - description: The ID of the project - example: aac12556-4d65-4d34-9fb6-d1f0985eb0a9 createdAt: type: number readOnly: true @@ -627,10 +623,6 @@ components: height: 720 items: $ref: "#/components/schemas/ffmpeg-profile" - projectId: - type: string - description: The ID of the project - example: aac12556-4d65-4d34-9fb6-d1f0985eb0a9 record: description: | Should this stream be recorded? Uses default settings. For more @@ -901,10 +893,6 @@ components: type: string example: de7818e7-610a-4057-8f6f-b785dc1e6f88 description: Points to parent stream object - projectId: - type: string - description: The ID of the project - example: aac12556-4d65-4d34-9fb6-d1f0985eb0a9 record: description: > Whether the stream should be recorded. Uses default settings. For @@ -1326,15 +1314,6 @@ components: readOnly: true type: string description: URL to access file via HTTP through an IPFS gateway - new-signing-key-payload: - additionalProperties: false - properties: - name: - type: string - description: Name of the signing key - projectId: - type: string - description: Project ID to which this signing key belongs new-asset-payload: additionalProperties: false required: @@ -2183,10 +2162,6 @@ components: type: boolean description: Disable the signing key to allow rotation safely example: false - projectId: - type: string - description: The ID of the project - example: aac12556-4d65-4d34-9fb user: type: object required: diff --git a/packages/api/src/schema/db-schema.yaml b/packages/api/src/schema/db-schema.yaml index b46e20ba0d..76ab481e33 100644 --- a/packages/api/src/schema/db-schema.yaml +++ b/packages/api/src/schema/db-schema.yaml @@ -591,10 +591,6 @@ components: properties: userId: index: true - projectId: - type: string - index: true - example: 66E2161C-7670-4D05-B71D-DA2D6979556F events: index: true indexType: gin @@ -1043,6 +1039,12 @@ components: example: 09F8B46C-61A0-4254-9875-F71F4C605BC7 catalystPipelineStrategy: $ref: "#/components/schemas/task/properties/params/properties/upload/properties/catalystPipelineStrategy" + new-signing-key-payload: + additionalProperties: false + properties: + name: + type: string + description: Name of the signing key signing-key-response-payload: type: object required: @@ -1294,10 +1296,6 @@ components: deleted: type: boolean default: false - projectId: - type: string - index: true - example: 78df0075-b5f3-4683-a618-1086faca35dc usage: table: usage properties: diff --git a/packages/api/src/store/types.ts b/packages/api/src/store/types.ts index 4f6b40e923..8d5413dcd7 100644 --- a/packages/api/src/store/types.ts +++ b/packages/api/src/store/types.ts @@ -21,13 +21,6 @@ export interface DBLegacyObject extends DBObject { data: Object; } -export interface DBOwnedResource extends DBObject { - // these are never really optional, but we don't have them as required in some schemas - userId?: string; - projectId?: string; - deleted?: boolean; -} - export type WithID = T & { id: string }; export interface FindQuery { diff --git a/packages/api/src/test-helpers.ts b/packages/api/src/test-helpers.ts index b35d4e48b7..c426eb708a 100644 --- a/packages/api/src/test-helpers.ts +++ b/packages/api/src/test-helpers.ts @@ -3,11 +3,10 @@ import fetch, { RequestInit } from "node-fetch"; import { v4 as uuid } from "uuid"; import schema from "./schema/schema.json"; -import { ApiToken, User } from "./schema/types"; +import { User } from "./schema/types"; import { TestServer } from "./test-server"; import fs from "fs"; import jwt, { VerifyOptions } from "jsonwebtoken"; -import { WithID } from "./store/types"; const vhostUrl = (vhost: string) => `http://guest:guest@127.0.0.1:15672/api/vhosts/${vhost}`; @@ -200,32 +199,6 @@ export class TestClient { } } -export async function createProject(client: TestClient) { - let res = await client.post(`/project`); - const project = await res.json(); - return project; -} - -export async function createApiToken({ - client, - projectId, - tokenName = "test", - jwtAuthToken, -}: { - client: TestClient; - projectId: string; - tokenName?: string; - jwtAuthToken: string; -}): Promise> { - client.jwtAuth = jwtAuthToken; - let res = await client.post(`/api-token/?projectId=${projectId}`, { - name: tokenName, - }); - client.jwtAuth = null; - const apiKeyObj = await res.json(); - return apiKeyObj; -} - export async function createUser( server: TestServer, client: TestClient, diff --git a/packages/api/src/types/common.d.ts b/packages/api/src/types/common.d.ts index 718f89527a..ef20a3119f 100644 --- a/packages/api/src/types/common.d.ts +++ b/packages/api/src/types/common.d.ts @@ -49,10 +49,6 @@ declare global { isNeverExpiringJWT?: boolean; token?: WithID; - checkResourceAccess: ( - resource?: DBOwnedResource, - uiAdminOnly?: boolean - ) => void; getBroadcasters?: () => Promise; orchestratorsGetters?: Array<() => Promise>; getIngest?: () => Promise;