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

Fix graphql schema for m2o fields without permissions to related collection #13015

Merged
merged 6 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 5 additions & 8 deletions api/src/utils/reduce-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,14 @@ export function reduceSchema(
continue;
}

const relatedCollection: string | undefined =
schema.relations.find((relation) => relation.collection === collectionName && relation.field === fieldName)
?.related_collection ||
schema.relations.find(
(relation) => relation.related_collection === collectionName && relation.meta?.one_field === fieldName
)?.collection;
const o2mRelation = schema.relations.find(
(relation) => relation.related_collection === collectionName && relation.meta?.one_field === fieldName
);

if (
relatedCollection &&
o2mRelation &&
!permissions?.some(
(permission) => permission.collection === relatedCollection && actions.includes(permission.action)
(permission) => permission.collection === o2mRelation.collection && actions.includes(permission.action)
)
) {
continue;
Expand Down
110 changes: 110 additions & 0 deletions tests/api/items/many-to-many.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,116 @@ describe('/items', () => {
});
});
});

describe('/:collection/:id GraphQL Query', () => {
describe('retrieves an artist and an event off the artists_events table', () => {
it.each(vendors)('%s', async (vendor) => {
const artist = createArtist();
const event = createEvent();
await seedTable(databases.get(vendor)!, 1, 'artists', artist);
await seedTable(databases.get(vendor)!, 1, 'events', event);

const relation = await seedTable(
databases.get(vendor)!,
1,
'artists_events',
{
artists_id: artist.id,
events_id: event.id,
},
{
select: ['id'],
where: ['artists_id', artist.id],
}
);

const query = `
{
artists_events_by_id (id: ${relation[relation.length - 1].id}) {
artists_id {
name
}
events_id {
cost
}
}
}`;

const response = await request(getUrl(vendor))
.post('/graphql')
.send({ query })
.set('Authorization', 'Bearer AdminToken')
.expect('Content-Type', /application\/json/)
.expect(200);

const { data } = response.body;

expect(data.artists_events_by_id).toMatchObject({
artists_id: { name: expect.any(String) },
events_id: { cost: expect.any(Number) },
});
});
});

describe('should get users of the directus_roles table with read permissions to directus_users', () => {
it.each(vendors)('%s', async (vendor) => {
const query = `
{
roles {
id
users {
id
}
}
}`;

const response = await request(getUrl(vendor))
.post('/graphql/system')
.send({ query })
.set('Authorization', 'Bearer AdminToken')
.expect('Content-Type', /application\/json/)
.expect(200);

const { data } = response.body;

expect(data.roles[data.roles.length - 1]).toMatchObject({
id: expect.any(String),
users: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
}),
]),
});
});
});

describe('should not get users of the directus_roles table without read permissions to directus_users', () => {
it.each(vendors)('%s', async (vendor) => {
const query = `
{
roles {
id
users
}
}`;

const response = await request(getUrl(vendor))
.post('/graphql/system')
.send({ query })
.set('Authorization', 'Bearer UserToken')
.expect('Content-Type', /application\/json/)
.expect(400);

const { errors } = response.body;

expect(errors[0].extensions.code).toBe('GRAPHQL_VALIDATION_EXCEPTION');
expect(errors[0].extensions.graphqlErrors[0].message).toBe(
'Cannot query field "users" on type "directus_roles".'
);
});
});
});

describe('/:collection/:id PATCH', () => {
describe('updates one artists_events to a different artist', () => {
it.each(vendors)('%s', async (vendor) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,109 @@ describe('/items', () => {
});
});

describe('/:collection GraphQL Query', () => {
describe('retrieves all items from guest table with favorite_artist', () => {
it.each(vendors)('%s', async (vendor) => {
const artist = createArtist();
const name = 'test-user';
await seedTable(databases.get(vendor)!, 1, 'artists', artist);
const guests = createMany(createGuest, 10, { name, favorite_artist: artist.id });
await seedTable(databases.get(vendor)!, 1, 'guests', guests);

const query = `
{
guests (filter: { name: { _eq: "${name}" } }) {
birthday
favorite_artist {
name
}
}
}`;

const response = await request(getUrl(vendor))
.post('/graphql')
.send({ query })
.set('Authorization', 'Bearer AdminToken')
.expect('Content-Type', /application\/json/)
.expect(200);

const { data } = response.body;

expect(data.guests.length).toBeGreaterThanOrEqual(10);
expect(data.guests[data.guests.length - 1]).toMatchObject({
birthday: expect.any(String),
favorite_artist: expect.objectContaining({
name: expect.any(String),
}),
});
});
});

describe('Should get "favorite_artist" field with ID string when retrieving all items from guests without read permission to artists', () => {
it.each(vendors)('%s', async (vendor) => {
const artist = createArtist();
const name = 'test-user';
await seedTable(databases.get(vendor)!, 1, 'artists', artist);
const guests = createMany(createGuest, 10, { name, favorite_artist: artist.id });
await seedTable(databases.get(vendor)!, 1, 'guests', guests);

const query = `
{
guests (filter: { name: { _eq: "${name}" } }) {
favorite_artist
}
}`;

const response = await request(getUrl(vendor))
.post('/graphql')
.send({ query })
.set('Authorization', 'Bearer UserToken')
.expect('Content-Type', /application\/json/)
.expect(200);

const { data } = response.body;

expect(data.guests.length).toBeGreaterThanOrEqual(10);
expect(data.guests[data.guests.length - 1]).toMatchObject({
favorite_artist: expect.any(String),
});
});
});

describe('Should not get nested fields in "favourite_artist" when retrieving all items from guests without read permission to artists', () => {
it.each(vendors)('%s', async (vendor) => {
const artist = createArtist();
const name = 'test-user';
await seedTable(databases.get(vendor)!, 1, 'artists', artist);
const guests = createMany(createGuest, 10, { name, favorite_artist: artist.id });
await seedTable(databases.get(vendor)!, 1, 'guests', guests);

const query = `
{
guests (filter: { name: { _eq: "${name}" } }) {
favorite_artist {
name
}
}
}`;

const response = await request(getUrl(vendor))
.post('/graphql')
.send({ query })
.set('Authorization', 'Bearer UserToken')
.expect('Content-Type', /application\/json/)
.expect(400);

const { errors } = response.body;

expect(errors[0].extensions.code).toBe('GRAPHQL_VALIDATION_EXCEPTION');
expect(errors[0].extensions.graphqlErrors[0].message).toBe(
'Field "favorite_artist" must not have a selection since type "String" has no subfields.'
);
});
});
});

describe('/:collection POST', () => {
describe('createOne', () => {
describe('creates one guest with a favorite_artist', () => {
Expand Down
1 change: 0 additions & 1 deletion tests/setup/seeds/02_directus_relations.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ exports.seed = async function (knex) {
many_field: 'favorite_artist',
one_collection: 'artists',
},
{ many_collection: 'artists_events', many_field: 'events_id', one_collection: 'artists' },
{
many_collection: 'artists_events',
many_field: 'events_id',
Expand Down
19 changes: 19 additions & 0 deletions tests/setup/seeds/06_directus_permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
exports.seed = async function (knex) {
await knex('directus_permissions').del();

return await knex('directus_permissions').insert([
// TestUser role permissions
{
role: '214faee7-d6a6-4a4c-b1cd-f9e9bd0b6fb7',
collection: 'guests',
action: 'read',
fields: '*',
},
{
role: '214faee7-d6a6-4a4c-b1cd-f9e9bd0b6fb7',
collection: 'directus_roles',
action: 'read',
fields: '*',
},
]);
};
File renamed without changes.