Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/easy-lizards-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aws-amplify/data-schema": minor
---

Add `inGroup()` method for owner authorization rules to require group membership alongside ownership
226 changes: 226 additions & 0 deletions packages/data-schema/__tests__/ModelType.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,232 @@ describe('model auth rules', () => {
expect(graphql).toMatchSnapshot();
});

describe('owner.inGroup() - owner auth with required group membership', () => {
it('can define owner auth with inGroup requirement', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) => allow.owner().inGroup('AdminGroup')),
});

const graphql = schema.transform().schema;
expect(graphql).toMatchSnapshot();
});

it('can define owner auth with multiple inGroup requirements', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) =>
allow.owner().inGroup('AdminGroup', 'SuperUserGroup'),
),
});

const graphql = schema.transform().schema;
expect(graphql).toMatchSnapshot();
});

it('can chain owner.inGroup() with operations', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) =>
allow.owner().inGroup('AdminGroup').to(['create', 'read', 'update']),
),
});

const graphql = schema.transform().schema;
expect(graphql).toMatchSnapshot();
});

it('can chain owner.inGroup() with identityClaim', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) =>
allow.owner().inGroup('AdminGroup').identityClaim('user_id'),
),
});

const graphql = schema.transform().schema;
expect(graphql).toMatchSnapshot();
});

it('can define ownerDefinedIn with inGroup requirement', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) =>
allow.ownerDefinedIn('customOwnerField').inGroup('AdminGroup'),
),
});

const graphql = schema.transform().schema;
expect(graphql).toMatchSnapshot();
});

it('can define ownersDefinedIn with inGroup requirement', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) =>
allow.ownersDefinedIn('editors').inGroup('EditorGroup'),
),
});

const graphql = schema.transform().schema;
expect(graphql).toMatchSnapshot();
});
});

describe('owner.inGroup() combined with group() - rules should not merge', () => {
it('owner.inGroup() and group() with same group remain as separate rules', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) => [
allow.owner().inGroup('AdminGroup'),
allow.group('AdminGroup'),
]),
});

const graphql = schema.transform().schema;
// Should have two separate rules:
// 1. {allow: owner, ownerField: "owner", groups: ["AdminGroup"]}
// 2. {allow: groups, groups: ["AdminGroup"]}
expect(graphql).toContain('allow: owner');
expect(graphql).toContain('allow: groups');
expect(graphql).toMatchSnapshot();
});

it('owner.inGroup() and groups() with overlapping groups remain as separate rules', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) => [
allow.owner().inGroup('AdminGroup', 'ModeratorGroup'),
allow.groups(['AdminGroup', 'UserGroup']),
]),
});

const graphql = schema.transform().schema;
expect(graphql).toContain('allow: owner');
expect(graphql).toContain('allow: groups');
expect(graphql).toMatchSnapshot();
});

it('multiple owner rules with different inGroup requirements remain separate', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) => [
allow.owner().inGroup('AdminGroup').to(['create', 'read', 'update', 'delete']),
allow.ownerDefinedIn('reviewer').inGroup('ReviewerGroup').to(['read']),
]),
});

const graphql = schema.transform().schema;
expect(graphql).toMatchSnapshot();
});

it('owner.inGroup() combined with group() and authenticated() produces correct output', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) => [
allow.owner().inGroup('AdminGroup'),
allow.group('AdminGroup').to(['read']),
allow.authenticated().to(['read']),
]),
});

const graphql = schema.transform().schema;
expect(graphql).toContain('allow: owner');
expect(graphql).toContain('allow: groups');
expect(graphql).toContain('allow: private');
expect(graphql).toMatchSnapshot();
});

it('owner.inGroup() with different groups and different permissions remain separate', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) => [
allow.owner().inGroup('ReadersGroup').to(['read']),
allow.owner().inGroup('WritersGroup').to(['create', 'update', 'delete']),
]),
});

const graphql = schema.transform().schema;
// Should have two separate owner rules with different groups and operations
expect(graphql).toContain('groups: ["ReadersGroup"]');
expect(graphql).toContain('groups: ["WritersGroup"]');
expect(graphql).toContain('operations: [read]');
expect(graphql).toContain('operations: [create, update, delete]');
expect(graphql).toMatchSnapshot();
});

it('owner.inGroup() with overlapping groups but different permissions remain separate', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
})
.authorization((allow) => [
allow.owner().inGroup('AdminGroup', 'ModeratorGroup').to(['read', 'update']),
allow.owner().inGroup('AdminGroup', 'SuperUserGroup').to(['create', 'delete']),
]),
});

const graphql = schema.transform().schema;
expect(graphql).toMatchSnapshot();
});

it('ownerDefinedIn.inGroup() with different groups and permissions on same field', () => {
const schema = a.schema({
widget: a
.model({
title: a.string().required(),
createdBy: a.string(),
})
.authorization((allow) => [
allow.ownerDefinedIn('createdBy').inGroup('ViewerGroup').to(['read']),
allow.ownerDefinedIn('createdBy').inGroup('EditorGroup').to(['read', 'update']),
allow.ownerDefinedIn('createdBy').inGroup('AdminGroup').to(['create', 'read', 'update', 'delete']),
]),
});

const graphql = schema.transform().schema;
// All three rules should be separate
expect(graphql).toContain('ViewerGroup');
expect(graphql).toContain('EditorGroup');
expect(graphql).toContain('AdminGroup');
expect(graphql).toMatchSnapshot();
});
});

it(`includes auth from fields`, () => {
const schema = a.schema({
widget: a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ exports[`field level auth implied fields objects can be extracted 1`] = `
},
{
"identityClaim": [Function],
"inGroup": [Function],
Symbol(data): {
"groupOrOwnerField": "admin",
"groups": undefined,
Expand All @@ -46,6 +47,7 @@ exports[`field level auth implied fields objects can be extracted 1`] = `
},
},
{
"inGroup": [Function],
Symbol(data): {
"groupOrOwnerField": "admin",
"groups": undefined,
Expand Down Expand Up @@ -94,6 +96,7 @@ exports[`field level auth implied fields objects can be extracted from related m
},
{
"identityClaim": [Function],
"inGroup": [Function],
Symbol(data): {
"groupOrOwnerField": "admin",
"groups": undefined,
Expand All @@ -109,6 +112,7 @@ exports[`field level auth implied fields objects can be extracted from related m
},
},
{
"inGroup": [Function],
Symbol(data): {
"groupOrOwnerField": "admin",
"groups": undefined,
Expand Down
101 changes: 101 additions & 0 deletions packages/data-schema/__tests__/__snapshots__/ModelType.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,107 @@ type widget @model @auth(rules: [{allow: owner, ownerField: "owner"}])
}"
`;

exports[`model auth rules owner.inGroup() - owner auth with required group membership can chain owner.inGroup() with identityClaim 1`] = `
"type widget @model @auth(rules: [{allow: owner, ownerField: "owner", groups: ["AdminGroup"], identityClaim: "user_id"}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() - owner auth with required group membership can chain owner.inGroup() with operations 1`] = `
"type widget @model @auth(rules: [{allow: owner, operations: [create, read, update], ownerField: "owner", groups: ["AdminGroup"]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() - owner auth with required group membership can define owner auth with inGroup requirement 1`] = `
"type widget @model @auth(rules: [{allow: owner, ownerField: "owner", groups: ["AdminGroup"]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() - owner auth with required group membership can define owner auth with multiple inGroup requirements 1`] = `
"type widget @model @auth(rules: [{allow: owner, ownerField: "owner", groups: ["AdminGroup", "SuperUserGroup"]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() - owner auth with required group membership can define ownerDefinedIn with inGroup requirement 1`] = `
"type widget @model @auth(rules: [{allow: owner, ownerField: "customOwnerField", groups: ["AdminGroup"]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() - owner auth with required group membership can define ownersDefinedIn with inGroup requirement 1`] = `
"type widget @model @auth(rules: [{allow: owner, ownerField: "editors", groups: ["EditorGroup"]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() combined with group() - rules should not merge multiple owner rules with different inGroup requirements remain separate 1`] = `
"type widget @model @auth(rules: [{allow: owner, operations: [create, read, update, delete], ownerField: "owner", groups: ["AdminGroup"]},
{allow: owner, operations: [read], ownerField: "reviewer", groups: ["ReviewerGroup"]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() combined with group() - rules should not merge owner.inGroup() and group() with same group remain as separate rules 1`] = `
"type widget @model @auth(rules: [{allow: owner, ownerField: "owner", groups: ["AdminGroup"]},
{allow: groups, groups: ["AdminGroup"]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() combined with group() - rules should not merge owner.inGroup() and groups() with overlapping groups remain as separate rules 1`] = `
"type widget @model @auth(rules: [{allow: owner, ownerField: "owner", groups: ["AdminGroup", "ModeratorGroup"]},
{allow: groups, groups: ["AdminGroup", "UserGroup"]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() combined with group() - rules should not merge owner.inGroup() combined with group() and authenticated() produces correct output 1`] = `
"type widget @model @auth(rules: [{allow: owner, ownerField: "owner", groups: ["AdminGroup"]},
{allow: groups, operations: [read], groups: ["AdminGroup"]},
{allow: private, operations: [read]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() combined with group() - rules should not merge owner.inGroup() with different groups and different permissions remain separate 1`] = `
"type widget @model @auth(rules: [{allow: owner, operations: [read], ownerField: "owner", groups: ["ReadersGroup"]},
{allow: owner, operations: [create, update, delete], ownerField: "owner", groups: ["WritersGroup"]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() combined with group() - rules should not merge owner.inGroup() with overlapping groups but different permissions remain separate 1`] = `
"type widget @model @auth(rules: [{allow: owner, operations: [read, update], ownerField: "owner", groups: ["AdminGroup", "ModeratorGroup"]},
{allow: owner, operations: [create, delete], ownerField: "owner", groups: ["AdminGroup", "SuperUserGroup"]}])
{
title: String!
}"
`;

exports[`model auth rules owner.inGroup() combined with group() - rules should not merge ownerDefinedIn.inGroup() with different groups and permissions on same field 1`] = `
"type widget @model @auth(rules: [{allow: owner, operations: [read], ownerField: "createdBy", groups: ["ViewerGroup"]},
{allow: owner, operations: [read, update], ownerField: "createdBy", groups: ["EditorGroup"]},
{allow: owner, operations: [create, read, update, delete], ownerField: "createdBy", groups: ["AdminGroup"]}])
{
title: String!
createdBy: String
}"
`;

exports[`secondary indexes generates a primary key AND secondary index annotation with attributes 1`] = `
"type widget @model @auth(rules: [{allow: public, provider: apiKey}])
{
Expand Down
Loading