Skip to content

Commit

Permalink
fix: return 400 on invalid POST data to project access endpoint (#5610)
Browse files Browse the repository at this point in the history
This PR fixes the issue discussed in SR-234, where you would get a 200
OK response even if your POST request to
`/api/admin/projects/<project-name>/access` contains invalid data (and
nothing is persisted).
  • Loading branch information
thomasheartman committed Dec 12, 2023
1 parent 8961a6e commit 8e43081
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 9 deletions.
27 changes: 27 additions & 0 deletions src/lib/db/access-store.test.ts
Expand Up @@ -2,6 +2,7 @@ import dbInit from '../../test/e2e/helpers/database-init';
import getLogger from '../../test/fixtures/no-logger';
import { PermissionRef } from 'lib/services/access-service';
import { AccessStore } from './access-store';
import { BadDataError } from '../../lib/error';

let db;

Expand Down Expand Up @@ -104,3 +105,29 @@ test.each([
expect(result).toStrictEqual(expected);
},
);

describe('addAccessToProject', () => {
test.each(['roles', 'groups', 'users'])(
'should throw a bad data error if there is invalid data in the %s property of the addAccessToProject payload',
async (property) => {
const access = db.stores.accessStore as AccessStore;

const payload = {
roles: [4, 5],
groups: [],
users: [],
[property]: [123456789],
};

await expect(() =>
access.addAccessToProject(
payload.roles,
payload.groups,
payload.users,
'projectId',
'createdBy',
),
).rejects.toThrow(BadDataError);
},
);
});
54 changes: 52 additions & 2 deletions src/lib/db/access-store.ts
Expand Up @@ -26,6 +26,7 @@ import {
PermissionRef,
} from 'lib/services/access-service';
import { inTransaction } from './transaction';
import BadDataError from '../error/bad-data-error';

const T = {
ROLE_USER: 'role_user',
Expand Down Expand Up @@ -618,6 +619,18 @@ export class AccessStore implements IAccessStore {
.whereIn('type', PROJECT_ROLE_TYPES)
.pluck('id');

if (validatedProjectRoleIds.length !== roles.length) {
const invalidRoles = roles.filter(
(role) => !validatedProjectRoleIds.includes(role),
);

throw new BadDataError(
`You can't add access to a project with roles that aren't project roles or that don't exist. These roles are not valid: ${invalidRoles.join(
', ',
)}`,
);
}

const groupRows = groups.flatMap((group) =>
validatedProjectRoleIds.map((role) => ({
group_id: group,
Expand All @@ -636,17 +649,54 @@ export class AccessStore implements IAccessStore {
);

await inTransaction(this.db, async (tx) => {
const errors: string[] = [];
if (groupRows.length > 0) {
await tx(T.GROUP_ROLE)
.insert(groupRows)
.onConflict(['project', 'role_id', 'group_id'])
.merge();
.merge()
.catch((err) => {
if (
err.message.includes(
`violates foreign key constraint "group_role_group_id_fkey"`,
)
) {
errors.push(
`Your request contains one or more group IDs that do not exist. You sent these group IDs: ${groups.join(
', ',
)}.`,
);
}
});
}
if (userRows.length > 0) {
await tx(T.ROLE_USER)
.insert(userRows)
.onConflict(['project', 'role_id', 'user_id'])
.merge();
.merge()
.catch((err) => {
if (
err.message.includes(
`violates foreign key constraint "role_user_user_id_fkey"`,
)
) {
errors.push(
`Your request contains one or more user IDs that do not exist. You sent these user IDs: ${users.join(
', ',
)}.`,
);
}
});
}

if (errors.length) {
const mapped = errors.map((message) => ({
message,
}));

// because TS doesn't understand that the non-empty
// array is guaranteed to have at least one element
throw new BadDataError('', [mapped[0], ...mapped.slice(1)]);
}
});
}
Expand Down
16 changes: 16 additions & 0 deletions src/lib/services/access-service.test.ts
Expand Up @@ -19,6 +19,7 @@ import { IRole } from '../../lib/types/stores/access-store';
import { IGroup, ROLE_CREATED } from '../../lib/types';
import EventService from './event-service';
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
import BadDataError from '../../lib/error/bad-data-error';

function getSetup(customRootRolesKillSwitch: boolean = true) {
const config = createTestConfig({
Expand Down Expand Up @@ -265,3 +266,18 @@ test('throws error when trying to delete a project role in use by group', async
);
}
});

describe('addAccessToProject', () => {
test('should throw an error when you try add access with an empty list of roles', async () => {
const { accessService } = getSetup();
await expect(() =>
accessService.addAccessToProject(
[],
[1],
[1],
'projectId',
'createdBy',
),
).rejects.toThrow(BadDataError);
});
});
5 changes: 5 additions & 0 deletions src/lib/services/access-service.ts
Expand Up @@ -277,6 +277,11 @@ export class AccessService {
projectId: string,
createdBy: string,
): Promise<void> {
if (roles.length === 0) {
throw new BadDataError(
"You can't grant access without any roles. The roles array you sent was empty.",
);
}
return this.store.addAccessToProject(
roles,
groups,
Expand Down
17 changes: 10 additions & 7 deletions src/test/e2e/services/access-service.e2e.test.ts
Expand Up @@ -21,6 +21,7 @@ import {
createFeatureToggleService,
createProjectService,
} from '../../../lib/features';
import { BadDataError } from '../../../lib/error';

let db: ITestDb;
let stores: IUnleashStores;
Expand Down Expand Up @@ -1382,13 +1383,15 @@ test('calling add access with invalid project role ids should not assign those r

const adminRootRole = await accessService.getRoleByName(RoleName.ADMIN);

accessService.addAccessToProject(
[adminRootRole.id, 9999],
[],
[emptyUser.id],
projectName,
'some-admin-user',
);
await expect(() =>
accessService.addAccessToProject(
[adminRootRole.id, 9999],
[],
[emptyUser.id],
projectName,
'some-admin-user',
),
).rejects.toThrow(BadDataError);

const newAssignedPermissions =
await accessService.getPermissionsForUser(emptyUser);
Expand Down

1 comment on commit 8e43081

@vercel
Copy link

@vercel vercel bot commented on 8e43081 Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.