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

feat: Add AuthorizationCallback shared authorization type support #528

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/fair-knives-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/data-schema': minor
---

Add a.AuthorizationCallback authorization sharing support
198 changes: 198 additions & 0 deletions packages/data-schema/__tests__/Authorization.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { DefineFunction } from "@aws-amplify/data-schema-types";
import { a } from "../src";
import { AuthorizationCallback } from "../src/Authorization";

describe('AuthorizationCallback definition', () => {
test('customOperation is compatible with all types except conversations', () => {
const callback: AuthorizationCallback<'customOperation'> = (a) => {
[
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groups(['testGroup']),
a.guest(),
a.publicApiKey()
]
};

const customOpAuth = a.schema({M: a.query().authorization(callback)});
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error customOperation is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('field is compatible with all types except custom operations and conversations', () => {
// `model` is the default callback type as it is featurefull and compatable with the most
// This is the same as `AuthorizationCallback<'model'>`
const callback: AuthorizationCallback<'field'> = (a) => [
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groupDefinedIn('group'),
a.groups(['testGroup']),
a.groupsDefinedIn('groups'),
a.guest(),
a.owner(),
a.ownerDefinedIn('owner'),
a.ownersDefinedIn('owners'),
a.publicApiKey(),
];

// @ts-expect-error field is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error field is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('relationship is compatible with all types except custom operations, field, model and conversations', () => {
const callback: AuthorizationCallback<'relationship'> = (a) => [
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groupDefinedIn('group'),
a.groups(['testGroup']),
a.groupsDefinedIn('groups'),
a.guest(),
a.owner(),
a.ownerDefinedIn('owner'),
a.ownersDefinedIn('owners'),
a.publicApiKey(),
a.resource({} as DefineFunction),
];

// @ts-expect-error relationship is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
// @ts-expect-error relationship is incompatible with field
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
// @ts-expect-error relationship is incompatible with model
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error relationship is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('model is compatible with all types except custom operations and conversations', () => {
const callback: AuthorizationCallback = (a) => {
[
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groupDefinedIn('group'),
a.groups(['testGroup']),
a.groupsDefinedIn('groups'),
a.guest(),
a.owner(),
a.ownerDefinedIn('owner'),
a.ownersDefinedIn('owners'),
a.publicApiKey(),
]
};

// @ts-expect-error model is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error model is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('ref is compatible with all types except custom operations, field, model and conversations', () => {
const callback: AuthorizationCallback<'ref'> = (a) => {
[
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groupDefinedIn('group'),
a.groups(['testGroup']),
a.groupsDefinedIn('groups'),
a.guest(),
a.owner(),
a.ownerDefinedIn('owner'),
a.ownersDefinedIn('owners'),
a.publicApiKey(),
a.resource({} as DefineFunction),
]
};

// @ts-expect-error ref is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
// @ts-expect-error ref is incompatible with field
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
// @ts-expect-error ref is incompatible with model
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error ref is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('schema is compatible with all types except custom operations, field, model and conversations', () => {
const callback: AuthorizationCallback<'schema'> = (a) => {
[
a.authenticated(),
a.custom('function'),
a.group('test'),
a.groupDefinedIn('group'),
a.groups(['testGroup']),
a.groupsDefinedIn('groups'),
a.guest(),
a.owner(),
a.ownerDefinedIn('owner'),
a.ownersDefinedIn('owners'),
a.publicApiKey(),
a.resource({} as DefineFunction),
]
};

// @ts-expect-error schema is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
// @ts-expect-error schema is incompatible with field
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
// @ts-expect-error schema is incompatible with model
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
// @ts-expect-error schema is incompatible with conversation
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});

test('conversation is only compatible with conversations', () => {
// The owner behavior used elsewhere include a provider override `provider?: OwnerProviders`
// This is not supported by conversations and makes the types entirely incompatible with other owner authorization types
const callback: AuthorizationCallback<'conversation'> = (a) => {
[
a.owner(),
]
};

// @ts-expect-error conversation is incompatible with custom operations
const customOpAuth = a.schema({M: a.query().authorization(callback)});
// @ts-expect-error conversation is incompatible with field
const fieldAuth = a.schema({M: a.model({field: a.string().authorization(callback)})});
// @ts-expect-error conversation is incompatible with relationship
const relationshipAuth = a.schema({Y: a.model({}), M: a.model({ys: a.hasMany('Y', 'yid').authorization(callback)})});
// @ts-expect-error conversation is incompatible with model
const modelAuth = a.schema({M: a.model({}).authorization(callback)});
// @ts-expect-error conversation is incompatible with ref
const refAuth = a.schema({R: a.customType({t: a.string()}), M: a.model({field: a.ref('R').authorization(callback)})});
// @ts-expect-error conversation is incompatible with schema
const schemaAuth = a.schema({M: a.model({})}).authorization(callback);
const conversationAuth = a.schema({chat: a.conversation({aiModel: a.ai.model("Claude 3 Opus"), systemPrompt: ''}).authorization(callback)});
});
});
6 changes: 6 additions & 0 deletions packages/data-schema/__tests__/Authorization.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { expectTypeTestsToPassAsync } from 'jest-tsd';
import { a } from '../src/index';

// evaluates type defs in corresponding test-d.ts file
it('should not produce static type errors', async () => {
await expectTypeTestsToPassAsync(__filename);
});

describe('.authorization(allow) builder disallowed use cases', () => {
describe('allow.resource()', () => {
it('cannot be used with a.model()', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@aws-amplify/data-schema](./data-schema.md) &gt; [a](./data-schema.a.md) &gt; [AuthorizationCallback](./data-schema.a.authorizationcallback.md)

## a.AuthorizationCallback type

Define an authorization callback that can be reused for multiple authorization calls across the schema definition.

**Signature:**

```typescript
export type AuthorizationCallback<AuthorizationType extends (keyof AuthorizationCallbackMapping) = "model"> = AuthorizationCallbackMapping[AuthorizationType];
```

## Example

const authCallback: a.AuthorizationCallback = (allow) =<!-- -->&gt; \[ allow.guest().to(\["read"\]), allow.owner() \];

const schema = a.schema(<!-- -->{ Post: a.model(<!-- -->{ id: a.id(), title: a.string(), protectedField: a.string().authorization(authCallback), content: a.string(), }<!-- -->).authorization(authCallback), }<!-- -->).authorization(authCallback);

26 changes: 26 additions & 0 deletions packages/data-schema/docs/data-schema.a.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,5 +355,31 @@ Description
</td><td>


</td></tr>
</tbody></table>

## Type Aliases

<table><thead><tr><th>

Type Alias


</th><th>

Description


</th></tr></thead>
<tbody><tr><td>

[AuthorizationCallback](./data-schema.a.authorizationcallback.md)


</td><td>

Define an authorization callback that can be reused for multiple authorization calls across the schema definition.


</td></tr>
</tbody></table>
20 changes: 20 additions & 0 deletions packages/data-schema/docs/data-schema.authorizationcallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@aws-amplify/data-schema](./data-schema.md) &gt; [AuthorizationCallback](./data-schema.authorizationcallback.md)

## AuthorizationCallback type

Define an authorization callback that can be reused for multiple authorization calls across the schema definition.

**Signature:**

```typescript
export type AuthorizationCallback<AuthorizationType extends (keyof AuthorizationCallbackMapping) = "model"> = AuthorizationCallbackMapping[AuthorizationType];
```

## Example

const authCallback: a.AuthorizationCallback = (allow) =<!-- -->&gt; \[ allow.guest().to(\["read"\]), allow.owner() \];

const schema = a.schema(<!-- -->{ Post: a.model(<!-- -->{ id: a.id(), title: a.string(), protectedField: a.string().authorization(authCallback), content: a.string(), }<!-- -->).authorization(authCallback), }<!-- -->).authorization(authCallback);

2 changes: 1 addition & 1 deletion packages/data-schema/docs/data-schema.customoperation.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Custom operation definition interface
export type CustomOperation<T extends CustomOperationParamShape, K extends keyof CustomOperation<T> = never, B extends CustomOperationBrand = CustomOperationBrand> = Omit<{
arguments<Arguments extends CustomArguments>(args: Arguments): CustomOperation<SetTypeSubArg<T, 'arguments', Arguments>, K | 'arguments', B>;
returns<ReturnType extends CustomReturnType>(returnType: ReturnType): CustomOperation<SetTypeSubArg<T, 'returnType', ReturnType>, K | 'returns', B>;
authorization<AuthRuleType extends Authorization<any, any, any>>(callback: (allow: AllowModifierForCustomOperation) => AuthRuleType | AuthRuleType[]): CustomOperation<SetTypeSubArg<T, 'authorization', AuthRuleType[]>, K | 'authorization', B>;
authorization<AuthRuleType extends Authorization<any, any, any>>(callback: CustomOperationAuthorizationCallback<AuthRuleType>): CustomOperation<SetTypeSubArg<T, 'authorization', AuthRuleType[]>, K | 'authorization', B>;
handler<H extends HandlerInputType>(handlers: H): [H] extends [UltimateFunctionHandlerAsyncType] ? CustomOperation<AsyncFunctionCustomOperation<T>, K | 'handler' | 'returns', B> : CustomOperation<T, K | 'handler', B>;
for<Source extends SubscriptionSource>(source: Source | Source[]): CustomOperation<T['typeName'] extends 'Subscription' ? SetTypeSubArg<T, 'returnType', Source extends SubscriptionSource[] ? Source[number] : Source> : T, K | 'for', B>;
}, K> & Brand<B>;
Expand Down
11 changes: 11 additions & 0 deletions packages/data-schema/docs/data-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ Description
Container for authorization schema definition content.


</td></tr>
<tr><td>

[AuthorizationCallback](./data-schema.authorizationcallback.md)


</td><td>

Define an authorization callback that can be reused for multiple authorization calls across the schema definition.


</td></tr>
<tr><td>

Expand Down
2 changes: 1 addition & 1 deletion packages/data-schema/docs/data-schema.modelschema.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Model schema definition interface

```typescript
export type ModelSchema<T extends ModelSchemaParamShape, UsedMethods extends 'authorization' | 'relationships' = never> = Omit<{
authorization: <AuthRules extends SchemaAuthorization<any, any, any>>(callback: (allow: AllowModifier) => AuthRules | AuthRules[]) => ModelSchema<SetTypeSubArg<T, 'authorization', AuthRules[]>, UsedMethods | 'authorization'>;
authorization: <AuthRules extends SchemaAuthorization<any, any, any>>(callback: SchemaAuthorizationCallback<AuthRules>) => ModelSchema<SetTypeSubArg<T, 'authorization', AuthRules[]>, UsedMethods | 'authorization'>;
}, UsedMethods> & BaseSchema<T> & DDBSchemaBrand;
```
**References:** [ModelSchema](./data-schema.modelschema.md)
Expand Down
2 changes: 1 addition & 1 deletion packages/data-schema/docs/data-schema.modeltype.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type ModelType<T extends ModelTypeParamShape = ModelTypeParamShape, UsedM
identifier<PrimaryIndexFields = ExtractSecondaryIndexIRFields<T>, PrimaryIndexPool extends string = keyof PrimaryIndexFields & string, const ID extends ReadonlyArray<PrimaryIndexPool> = readonly [], const PrimaryIndexIR extends PrimaryIndexIrShape = PrimaryIndexFieldsToIR<ID, PrimaryIndexFields>>(identifier: ID): ModelType<SetTypeSubArg<T, 'identifier', PrimaryIndexIR>, UsedMethod | 'identifier'>;
secondaryIndexes<const SecondaryIndexFields = ExtractSecondaryIndexIRFields<T>, const SecondaryIndexPKPool extends string = keyof SecondaryIndexFields & string, const Indexes extends readonly ModelIndexType<string, string, unknown, readonly [], any>[] = readonly [], const IndexesIR extends readonly any[] = SecondaryIndexToIR<Indexes, SecondaryIndexFields>>(callback: (index: <PK extends SecondaryIndexPKPool>(pk: PK) => ModelIndexType<SecondaryIndexPKPool, PK, ReadonlyArray<Exclude<SecondaryIndexPKPool, PK>>>) => Indexes): ModelType<SetTypeSubArg<T, 'secondaryIndexes', IndexesIR>, UsedMethod | 'secondaryIndexes'>;
disableOperations<const Ops extends ReadonlyArray<DisableOperationsOptions>>(ops: Ops): ModelType<SetTypeSubArg<T, 'disabledOperations', Ops>, UsedMethod | 'disableOperations'>;
authorization<AuthRuleType extends AnyAuthorization>(callback: (allow: BaseAllowModifier) => AuthRuleType | AuthRuleType[]): ModelType<SetTypeSubArg<T, 'authorization', AuthRuleType[]>, UsedMethod | 'authorization'>;
authorization<AuthRuleType extends AnyAuthorization>(callback: ModelAuthorizationCallback<AuthRuleType>): ModelType<SetTypeSubArg<T, 'authorization', AuthRuleType[]>, UsedMethod | 'authorization'>;
}, UsedMethod>;
```
**References:** [ModelType](./data-schema.modeltype.md)
Expand Down
2 changes: 1 addition & 1 deletion packages/data-schema/docs/data-schema.rdsmodelschema.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type RDSModelSchema<T extends RDSModelSchemaParamShape, UsedMethods exten
addQueries: <Queries extends Record<string, QueryCustomOperation>>(types: Queries) => RDSModelSchema<SetTypeSubArg<T, 'types', T['types'] & Queries>, UsedMethods | 'addQueries'>;
addMutations: <Mutations extends Record<string, MutationCustomOperation>>(types: Mutations) => RDSModelSchema<SetTypeSubArg<T, 'types', T['types'] & Mutations>, UsedMethods | 'addMutations'>;
addSubscriptions: <Subscriptions extends Record<string, SubscriptionCustomOperation>>(types: Subscriptions) => RDSModelSchema<SetTypeSubArg<T, 'types', T['types'] & Subscriptions>, UsedMethods | 'addSubscriptions'>;
authorization: <AuthRules extends SchemaAuthorization<any, any, any>>(callback: (allow: AllowModifier) => AuthRules | AuthRules[]) => RDSModelSchema<SetTypeSubArg<T, 'authorization', AuthRules[]>, UsedMethods | 'authorization'>;
authorization: <AuthRules extends SchemaAuthorization<any, any, any>>(callback: SchemaAuthorizationCallback<AuthRules>) => RDSModelSchema<SetTypeSubArg<T, 'authorization', AuthRules[]>, UsedMethods | 'authorization'>;
setAuthorization: (callback: (models: OmitFromEach<BaseSchema<T, true>['models'], 'secondaryIndexes'>, schema: RDSModelSchema<T, UsedMethods | 'setAuthorization'>) => void) => RDSModelSchema<T>;
setRelationships: <Relationships extends ReadonlyArray<Partial<Record<keyof T['types'], RelationshipTemplate>>>>(callback: (models: OmitFromEach<BaseSchema<T, true>['models'], 'authorization' | 'fields' | 'secondaryIndexes'>) => Relationships) => RDSModelSchema<SetTypeSubArg<T, 'types', {
[ModelName in keyof T['types']]: ModelWithRelationships<T['types'], Relationships, ModelName>;
Expand Down
Loading