Explorar el Código

feat(core): Implement access control for custom fields

Relates to #85. Not full permissions-based access, but a `public` flag which can be used to hide a custom field from the Shop API.
Michael Bromley hace 6 años
padre
commit
8f763b2bc4

+ 58 - 12
packages/core/e2e/custom-fields.e2e-spec.ts

@@ -77,6 +77,18 @@ describe('Custom fields', () => {
                             type: 'string',
                             options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }],
                         },
+                        {
+                            name: 'nonPublic',
+                            type: 'string',
+                            defaultValue: 'hi!',
+                            public: false,
+                        },
+                        {
+                            name: 'public',
+                            type: 'string',
+                            defaultValue: 'ho!',
+                            public: true,
+                        },
                     ],
                 },
             },
@@ -125,6 +137,8 @@ describe('Custom fields', () => {
                 { name: 'validateFn1', type: 'string' },
                 { name: 'validateFn2', type: 'string' },
                 { name: 'stringWithOptions', type: 'string' },
+                { name: 'nonPublic', type: 'string' },
+                { name: 'public', type: 'string' },
             ],
         });
     });
@@ -223,20 +237,19 @@ describe('Custom fields', () => {
             }, `The custom field value ['tiny'] is invalid. Valid options are ['small', 'medium', 'large']`),
         );
 
-        it(
-            'valid string option', async () => {
-                const { updateProduct } = await adminClient.query(gql`
-                    mutation {
-                        updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "medium" } }) {
-                            id
-                            customFields {
-                                stringWithOptions
-                            }
+        it('valid string option', async () => {
+            const { updateProduct } = await adminClient.query(gql`
+                mutation {
+                    updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "medium" } }) {
+                        id
+                        customFields {
+                            stringWithOptions
                         }
                     }
-                `);
-                expect(updateProduct.customFields.stringWithOptions).toBe('medium');
-            });
+                }
+            `);
+            expect(updateProduct.customFields.stringWithOptions).toBe('medium');
+        });
 
         it(
             'invalid localeString',
@@ -332,4 +345,37 @@ describe('Custom fields', () => {
             }, `The value ['invalid'] is not valid`),
         );
     });
+
+    describe('public access', () => {
+        it(
+            'non-public throws for Shop API',
+            assertThrowsWithMessage(async () => {
+                await shopClient.query(gql`
+                    query {
+                        product(id: "T_1") {
+                            id
+                            customFields {
+                                nonPublic
+                            }
+                        }
+                    }
+                `);
+            }, `Cannot query field "nonPublic" on type "ProductCustomFields"`),
+        );
+
+        it('publicly accessible via Shop API', async () => {
+            const { product } = await shopClient.query(gql`
+                query {
+                    product(id: "T_1") {
+                        id
+                        customFields {
+                            public
+                        }
+                    }
+                }
+            `);
+
+            expect(product.customFields.public).toBe('ho!');
+        });
+    });
 });

+ 16 - 0
packages/core/src/api/config/__snapshots__/graphql-custom-fields.spec.ts.snap

@@ -210,6 +210,22 @@ input UpdateProductInput {
 "
 `;
 
+exports[`addGraphQLCustomFields() publicOnly = true 1`] = `
+"scalar DateTime
+
+scalar JSON
+
+type Product {
+  id: ID
+  customFields: ProductCustomFields
+}
+
+type ProductCustomFields {
+  available: Boolean
+}
+"
+`;
+
 exports[`addGraphQLCustomFields() uses JSON scalar if no custom fields defined 1`] = `
 "scalar DateTime
 

+ 1 - 1
packages/core/src/api/config/configure-graphql-module.ts

@@ -150,7 +150,7 @@ async function createGraphQLOptions(
         const customFields = configService.customFields;
         const typeDefs = await typesLoader.mergeTypesByPaths(options.typePaths);
         let schema = generateListOptions(typeDefs);
-        schema = addGraphQLCustomFields(schema, customFields);
+        schema = addGraphQLCustomFields(schema, customFields, apiType === 'shop');
         schema = addServerConfigCustomFields(schema, customFields);
         schema = addOrderLineCustomFieldsInput(schema, customFields.OrderLine || []);
         const pluginSchemaExtensions = getPluginAPIExtensions(configService.plugins, apiType).map(

+ 24 - 8
packages/core/src/api/config/graphql-custom-fields.spec.ts

@@ -14,7 +14,7 @@ describe('addGraphQLCustomFields()', () => {
         const customFieldConfig: CustomFields = {
             Product: [],
         };
-        const result = addGraphQLCustomFields(input, customFieldConfig);
+        const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
     });
 
@@ -27,7 +27,7 @@ describe('addGraphQLCustomFields()', () => {
         const customFieldConfig: CustomFields = {
             Product: [{ name: 'available', type: 'boolean' }],
         };
-        const result = addGraphQLCustomFields(input, customFieldConfig);
+        const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
     });
 
@@ -45,7 +45,7 @@ describe('addGraphQLCustomFields()', () => {
         const customFieldConfig: CustomFields = {
             Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
         };
-        const result = addGraphQLCustomFields(input, customFieldConfig);
+        const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
     });
 
@@ -62,7 +62,7 @@ describe('addGraphQLCustomFields()', () => {
         const customFieldConfig: CustomFields = {
             Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
         };
-        const result = addGraphQLCustomFields(input, customFieldConfig);
+        const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
     });
 
@@ -79,7 +79,7 @@ describe('addGraphQLCustomFields()', () => {
         const customFieldConfig: CustomFields = {
             Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
         };
-        const result = addGraphQLCustomFields(input, customFieldConfig);
+        const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
     });
 
@@ -104,7 +104,7 @@ describe('addGraphQLCustomFields()', () => {
         const customFieldConfig: CustomFields = {
             Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
         };
-        const result = addGraphQLCustomFields(input, customFieldConfig);
+        const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
     });
 
@@ -126,7 +126,7 @@ describe('addGraphQLCustomFields()', () => {
         const customFieldConfig: CustomFields = {
             Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
         };
-        const result = addGraphQLCustomFields(input, customFieldConfig);
+        const result = addGraphQLCustomFields(input, customFieldConfig, false);
         expect(printSchema(result)).toMatchSnapshot();
     });
 
@@ -165,7 +165,23 @@ describe('addGraphQLCustomFields()', () => {
                 { name: 'published', type: 'datetime' },
             ],
         };
-        const result = addGraphQLCustomFields(input, customFieldConfig);
+        const result = addGraphQLCustomFields(input, customFieldConfig, false);
+        expect(printSchema(result)).toMatchSnapshot();
+    });
+
+    it('publicOnly = true', () => {
+        const input = `
+                 type Product {
+                     id: ID
+                 }
+            `;
+        const customFieldConfig: CustomFields = {
+            Product: [
+                { name: 'available', type: 'boolean', public: true },
+                { name: 'profitMargin', type: 'float', public: false },
+            ],
+        };
+        const result = addGraphQLCustomFields(input, customFieldConfig, true);
         expect(printSchema(result)).toMatchSnapshot();
     });
 });

+ 4 - 1
packages/core/src/api/config/graphql-custom-fields.ts

@@ -12,6 +12,7 @@ import { CustomFieldConfig, CustomFields } from '../../config/custom-field/custo
 export function addGraphQLCustomFields(
     typeDefsOrSchema: string | GraphQLSchema,
     customFieldConfig: CustomFields,
+    publicOnly: boolean,
 ): GraphQLSchema {
     const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
 
@@ -30,7 +31,9 @@ export function addGraphQLCustomFields(
     }
 
     for (const entityName of Object.keys(customFieldConfig)) {
-        const customEntityFields = customFieldConfig[entityName as keyof CustomFields] || [];
+        const customEntityFields = (customFieldConfig[entityName as keyof CustomFields] || []).filter(config => {
+            return (publicOnly === true) ? config.public !== false : true;
+        });
 
         const localeStringFields = customEntityFields.filter(field => field.type === 'localeString');
         const nonLocaleStringFields = customEntityFields.filter(field => field.type !== 'localeString');

+ 5 - 0
packages/core/src/config/custom-field/custom-field-types.ts

@@ -28,6 +28,11 @@ export type TypedCustomFieldConfig<T extends CustomFieldType, C extends CustomFi
     '__typename'
 > & {
     type: T;
+    /**
+     * Whether or not the custom field is available via the Shop API.
+     * @default true
+     */
+    public?: boolean;
     defaultValue?: DefaultValueType<T>;
     nullable?: boolean;
     validate?: (value: DefaultValueType<T>) => string | LocalizedString[] | void;