Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remix: destroy access requests #5142

Merged
merged 2 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions utopia-remix/app/models/projectAccessRequest.server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { AccessRequestStatus, UserProjectRole } from '../types'
import {
createAccessRequest,
destroyAccessRequest,
listProjectAccessRequests,
updateAccessRequestStatus,
} from './projectAccessRequest.server'
Expand Down Expand Up @@ -327,4 +328,99 @@ describe('projectAccessRequest', () => {
expect(got[3].User?.name).toBe('person4')
})
})

describe('destroyAccessRequest', () => {
let revokeAllRolesFromUserMock: jest.SpyInstance

afterEach(async () => {
await truncateTables([
prisma.projectID,
prisma.projectAccessRequest,
prisma.projectCollaborator,
prisma.project,
prisma.userDetails,
])

revokeAllRolesFromUserMock.mockRestore()
})

beforeEach(async () => {
await createTestUser(prisma, { id: 'bob' })
await createTestUser(prisma, { id: 'alice' })
await createTestUser(prisma, { id: 'carol' })
await createTestUser(prisma, { id: 'dorothy' })
await createTestProject(prisma, { id: 'one', ownerId: 'bob' })
await createTestProject(prisma, { id: 'two', ownerId: 'alice' })
await createTestProject(prisma, { id: 'three', ownerId: 'alice' })
await createTestProjectAccessRequest(prisma, {
projectId: 'two',
token: 'token1',
userId: 'carol',
status: AccessRequestStatus.PENDING,
})
await createTestProjectAccessRequest(prisma, {
projectId: 'three',
token: 'token2',
userId: 'dorothy',
status: AccessRequestStatus.PENDING,
})
await createTestProjectAccessRequest(prisma, {
projectId: 'two',
token: 'token3',
userId: 'bob',
status: AccessRequestStatus.PENDING,
})

revokeAllRolesFromUserMock = jest.spyOn(permissionsService, 'revokeAllRolesFromUser')
})
it('errors if the project is not found', async () => {
const fn = async () =>
destroyAccessRequest({ projectId: 'UNKNOWN', ownerId: 'bob', token: 'token1' })
await expect(fn).rejects.toThrow('Record to delete does not exist')
})
it('errors if the project is not owned by the user', async () => {
const fn = async () =>
destroyAccessRequest({ projectId: 'two', ownerId: 'bob', token: 'token1' })
await expect(fn).rejects.toThrow('Record to delete does not exist')
})
it('errors if the token is not found', async () => {
const fn = async () =>
destroyAccessRequest({ projectId: 'two', ownerId: 'alice', token: 'tokenWRONG' })
await expect(fn).rejects.toThrow('Record to delete does not exist')
})
it('errors if the token exists but not on that project', async () => {
const fn = async () =>
destroyAccessRequest({ projectId: 'two', ownerId: 'alice', token: 'token2' })
await expect(fn).rejects.toThrow('Record to delete does not exist')
})
it('deletes the request and revokes roles on FGA', async () => {
const existing = await prisma.projectAccessRequest.findMany({ where: { project_id: 'two' } })
expect(existing.length).toBe(2)
await destroyAccessRequest({ projectId: 'two', ownerId: 'alice', token: 'token1' })
const current = await prisma.projectAccessRequest.findMany({
where: { project_id: 'two' },
})
expect(current.length).toBe(1)
expect(current[0].user_id).toBe('bob')

expect(revokeAllRolesFromUserMock).toHaveBeenCalledWith('two', 'carol')
})
it('rolls back if the FGA revoking fails', async () => {
revokeAllRolesFromUserMock.mockImplementation(() => {
throw new Error('boom')
})

const existing = await prisma.projectAccessRequest.findMany({ where: { project_id: 'two' } })
expect(existing.length).toBe(2)

const fn = async () =>
await destroyAccessRequest({ projectId: 'two', ownerId: 'alice', token: 'token1' })
await expect(fn).rejects.toThrow('boom')

const current = await prisma.projectAccessRequest.findMany({
where: { project_id: 'two' },
})
expect(current.length).toBe(2)
})
})
})
23 changes: 23 additions & 0 deletions utopia-remix/app/models/projectAccessRequest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,26 @@ export async function listProjectAccessRequests(params: {
User: users.find((u) => u.user_id === r.user_id) ?? null,
}))
}

export async function destroyAccessRequest(params: {
projectId: string
ownerId: string
token: string
}) {
await prisma.$transaction(async (tx) => {
// 1. delete the access request from the DB
const request = await tx.projectAccessRequest.delete({
where: {
project_id_token: {
project_id: params.projectId,
token: params.token,
},
Project: {
owner_id: params.ownerId,
},
},
})
// 2. revoke access on FGA
await permissionsService.revokeAllRolesFromUser(params.projectId, request.user_id)
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { prisma } from '../db.server'
import { handleDestroyAccessRequest } from '../routes/internal.projects.$id.access.request.$token.destroy'
ruggi marked this conversation as resolved.
Show resolved Hide resolved
import {
createTestProject,
createTestProjectAccessRequest,
createTestSession,
createTestUser,
newTestRequest,
truncateTables,
} from '../test-util'
import { AccessRequestStatus } from '../types'

describe('handleDestroyAccessRequest', () => {
afterEach(async () => {
await truncateTables([
prisma.projectID,
prisma.projectAccessRequest,
prisma.projectAccess,
prisma.persistentSession,
prisma.project,
prisma.userDetails,
])
})

beforeEach(async () => {
await createTestUser(prisma, { id: 'bob' })
await createTestUser(prisma, { id: 'alice' })
await createTestUser(prisma, { id: 'carol' })
await createTestSession(prisma, { userId: 'bob', key: 'the-key' })
await createTestProject(prisma, { id: 'one', ownerId: 'bob' })
await createTestProjectAccessRequest(prisma, {
projectId: 'one',
userId: 'alice',
status: AccessRequestStatus.PENDING,
token: 'alice-token',
})
await createTestProjectAccessRequest(prisma, {
projectId: 'one',
userId: 'carol',
status: AccessRequestStatus.PENDING,
token: 'carol-token',
})
})

it('requires a user', async () => {
const fn = async () => handleDestroyAccessRequest(newTestRequest(), {})
await expect(fn).rejects.toThrow('missing session cookie')
})

it('requires a project id', async () => {
const fn = async () => handleDestroyAccessRequest(newTestRequest({ authCookie: 'the-key' }), {})
await expect(fn).rejects.toThrow('invalid project id')
})

it('requires a token', async () => {
const fn = async () =>
handleDestroyAccessRequest(newTestRequest({ authCookie: 'the-key' }), { id: 'one' })
await expect(fn).rejects.toThrow('invalid token')
})

it('destroys the access request', async () => {
await handleDestroyAccessRequest(newTestRequest({ authCookie: 'the-key' }), {
id: 'one',
token: 'alice-token',
})
const reqs = await prisma.projectAccessRequest.findMany({ where: { project_id: 'one' } })
expect(reqs.length).toBe(1)
expect(reqs[0].user_id).toBe('carol')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ActionFunctionArgs } from '@remix-run/node'
import type { Params } from '@remix-run/react'
import { validateProjectAccess } from '../handlers/validators'
import { UserProjectPermission } from '../types'
import { ensure, handle, requireUser } from '../util/api.server'
import { Status } from '../util/statusCodes'
import { destroyAccessRequest } from '../models/projectAccessRequest.server'

export async function action(args: ActionFunctionArgs) {
return handle(args, {
POST: {
handler: handleDestroyAccessRequest,
validator: validateProjectAccess(UserProjectPermission.CAN_MANAGE_PROJECT, {
getProjectId: (params) => params.id,
}),
},
})
}

export async function handleDestroyAccessRequest(req: Request, params: Params<string>) {
const user = await requireUser(req)

const projectId = params.id
ensure(projectId != null, 'invalid project id', Status.BAD_REQUEST)

const token = params.token
ensure(token != null && typeof token === 'string', 'invalid token', Status.BAD_REQUEST)

await destroyAccessRequest({
projectId: projectId,
ownerId: user.user_id,
token: token,
})

return {}
}
Loading