diff --git a/cypress.json b/cypress.json index 7425c1c4de0..10ad6833212 100644 --- a/cypress.json +++ b/cypress.json @@ -4,11 +4,12 @@ "baseUrl": "http://localhost:3001", "defaultCommandTimeout": 10000, "env": { - "AWS_COGNITO_USER_POOL_ID": "eu-central-1_LGh6br2ix", - "AWS_COGNITO_CLIENT_ID": "561rp7m88m81t4jgdvq1j5eobf", - "API_URL": "https://d1cfsi9llzrdlf.cloudfront.net", - "DEFAULT_ADMIN_USER_USERNAME": "asd@net.hr", + "SITE_URL": "http://localhost:3000", + "AWS_COGNITO_USER_POOL_ID": "eu-central-1_4rzqiPNHm", + "AWS_COGNITO_CLIENT_ID": "1agul1e3ge3afpb4a0vsrqtuhb", + "API_URL": "https://d2sj4ukj5xxt5i.cloudfront.net", + "DEFAULT_ADMIN_USER_USERNAME": "admin@webiny.com", "DEFAULT_ADMIN_USER_PASSWORD": "12345678", - "GRAPHQL_API_URL": "https://d1cfsi9llzrdlf.cloudfront.net/graphql" + "GRAPHQL_API_URL": "https://d2sj4ukj5xxt5i.cloudfront.net/graphql" } } diff --git a/cypress/integration/admin/security/roles/createGroup.spec.js b/cypress/integration/admin/security/roles/createGroup.spec.js new file mode 100644 index 00000000000..7565ee786cc --- /dev/null +++ b/cypress/integration/admin/security/roles/createGroup.spec.js @@ -0,0 +1,91 @@ +import uniqid from "uniqid"; + +context("Groups Module", () => { + beforeEach(() => cy.login()); + + it("should be able to create, edit, and immediately delete a group", () => { + const id = uniqid(); + cy.visit("/groups") + .findByLabelText("Name") + .type(`Test Group ${id}`) + .findByText("Save group") + .click() + .findByText("Value is required.") + .should("exist") + .findByLabelText("Slug") + .type(`test-group-${id}`) + .findByLabelText("Description") + .type("This is a test test.") + .findByText("Save group") + .click(); + + cy.wait(500) + .findByText("Record saved successfully.") + .should("exist"); + + cy.findByLabelText("Slug") + .type("-edited") + .findByLabelText("Description") + .type(" Test test.") + .findByText("Save group") + .click(); + + cy.wait(500) + .findByTestId("default-data-list") + .within(() => { + cy.get("div") + .first() + .within(() => { + cy.findByText(`Test Group ${id}`) + .should("exist") + .findByText("This is a test test. Test test.") + .should("exist"); + cy.get("button").click({ force: true }); + }); + }); + + cy.get('[role="alertdialog"] :visible').within(() => { + cy.contains("Are you sure you want to continue?") + .next() + .within(() => cy.findByText("Confirm").click()); + }); + + cy.findByText("Record deleted successfully.").should("exist"); + cy.findByTestId("default-data-list").within(() => { + cy.findByText(`Test Group ${id}`).should("not.exist"); + }); + }); + + it("groups with the same slug should not be allowed - an error message must be shown", () => { + const id = uniqid(); + cy.visit("/groups") + .findByLabelText("Name") + .type(`Test Group ${id}`) + .findByText("Save group") + .findByLabelText("Slug") + .type(`test-group-${id}`) + .findByLabelText("Description") + .type("This is a test test.") + .findByText("Save group") + .click() + .wait(1500); + + cy.findByTestId("new-record-button") + .click() + .findByLabelText("Name") + .type(`Test Group ${id}`) + .findByText("Save group") + .findByLabelText("Slug") + .type(`test-group-${id}`) + .findByLabelText("Description") + .type("This is a test test.") + .findByText("Save group") + .click(); + + cy.wait(500) + .get('[role="alertdialog"] :visible') + .within(() => { + cy.findByText(`Group with slug "test-group-${id}" already exists.`).should("exist"); + }); + }); +}); diff --git a/cypress/integration/admin/security/roles/createRole.spec.js b/cypress/integration/admin/security/roles/createRole.spec.js new file mode 100644 index 00000000000..792d8cd3e09 --- /dev/null +++ b/cypress/integration/admin/security/roles/createRole.spec.js @@ -0,0 +1,132 @@ +import uniqid from "uniqid"; + +context("Roles Module", () => { + beforeEach(() => cy.login()); + + it("should be able to create, edit, and immediately delete a role", () => { + const id = uniqid(); + cy.visit("/roles") + .findByLabelText("Name") + .type(`Test Role ${id}`) + .findByText("Save role") + .click() + .findByText("Value is required.") + .should("exist") + .findByLabelText("Slug") + .type(`test-role-${id}`) + .findByLabelText("Description") + .type("This is a test test.") + .findByText("Save role") + .click(); + + cy.wait(500) + .findByText("Record saved successfully.") + .should("exist"); + + cy.findByLabelText("Slug") + .type("-edited") + .findByLabelText("Description") + .type(" Test test."); + + // Check if the scopes auto-complete is working. + cy.findByLabelText("Scopes") + .type(`revi`) + .findByText("Publish form revisions") + .click() + .findByLabelText("Scopes") + .type(`revi`) + .findByText("Unpublish form revisions") + .click(); + + cy.findByText("Save role").click(); + + cy.wait(500) + .findByTestId("default-data-list") + .within(() => { + cy.get("div") + .first() + .within(() => { + cy.findByText(`Test Role ${id}`) + .should("exist") + .findByText("This is a test test. Test test.") + .should("exist"); + cy.get("button").click({ force: true }); + }); + }); + + cy.get('[role="alertdialog"] :visible').within(() => { + cy.contains("Are you sure you want to continue?") + .next() + .within(() => cy.findByText("Confirm").click()); + }); + + cy.findByText("Record deleted successfully.").should("exist"); + cy.findByTestId("default-data-list").within(() => { + cy.findByText(`Test Role ${id}`).should("not.exist"); + }); + }); + + it("roles with the same slug should not be allowed - an error message must be shown", () => { + const id = uniqid(); + cy.visit("/roles") + .findByLabelText("Name") + .type(`Test Role ${id}`) + .findByText("Save role") + .findByLabelText("Slug") + .type(`test-role-${id}`) + .findByLabelText("Description") + .type("This is a test test.") + .findByText("Save role") + .click() + .wait(1500); + + cy.findByTestId("new-record-button") + .click() + .findByLabelText("Name") + .type(`Test Role ${id}`) + .findByText("Save role") + .findByLabelText("Slug") + .type(`test-role-${id}`) + .findByLabelText("Description") + .type("This is a test test.") + .findByText("Save role") + .click(); + + cy.wait(500) + .get('[role="alertdialog"] :visible') + .within(() => { + cy.findByText(`Role with slug "test-role-${id}" already exists.`).should("exist"); + }); + }); + + it("should save scopes correctly", () => { + const id = uniqid(); + cy.visit("/roles") + .findByLabelText("Name") + .type(`Test Role ${id}`) + .findByLabelText("Slug") + .type(`test-role-${id}`) + .findByLabelText("Description") + .type("This is a test test.") + .findByLabelText("Scopes") + .type(`revi`) + .findByText("Publish form revisions") + .click() + .findByLabelText("Scopes") + .type(`revi`) + .findByText("Unpublish form revisions") + .click() + .findByText("Save role") + .click(); + + cy.wait(2500) + .reload() + .findByTestId("default-form") + .within(() => { + cy.findByText("forms:form:revision:publish") + .should("exist") + .findByText("forms:form:revision:unpublish") + .should("exist"); + }); + }); +}); diff --git a/packages/api-form-builder/src/plugins/graphql.ts b/packages/api-form-builder/src/plugins/graphql.ts index b3f6b68d8e1..2fec018a8cf 100644 --- a/packages/api-form-builder/src/plugins/graphql.ts +++ b/packages/api-form-builder/src/plugins/graphql.ts @@ -93,7 +93,7 @@ const plugin: GraphQLSchemaPlugin = { security: { shield: { FormsQuery: { - getSettings: hasScope("cms:settings"), + getSettings: hasScope("forms:settings"), getForm: hasScope("forms:form:crud"), listForms: hasScope("forms:form:crud"), listFormSubmissions: hasScope("forms:form:crud") @@ -101,15 +101,15 @@ const plugin: GraphQLSchemaPlugin = { // getPublishedForms: hasScope("forms:form:crud") // Expose publicly. }, FormsMutation: { - updateSettings: hasScope("cms:settings"), + updateSettings: hasScope("forms:settings"), createForm: hasScope("forms:form:crud"), deleteForm: hasScope("forms:form:crud"), - createRevisionFrom: hasScope("forms:form:revision:create"), - updateRevision: hasScope("forms:form:revision:update"), + createRevisionFrom: hasScope("forms:form:crud"), + updateRevision: hasScope("forms:form:crud"), publishRevision: hasScope("forms:form:revision:publish"), unpublishRevision: hasScope("forms:form:revision:unpublish"), - deleteRevision: hasScope("forms:form:revision:delete"), - exportFormSubmissions: hasScope("forms:form:submission:export") + deleteRevision: hasScope("forms:form:crud"), + exportFormSubmissions: hasScope("forms:form:submissions:export") // saveFormView: hasScope("forms:form:revision:delete") // Expose publicly. // createFormSubmission: hasScope("forms:form:revision:delete") // Expose publicly. } diff --git a/packages/api-page-builder/src/plugins/graphql.ts b/packages/api-page-builder/src/plugins/graphql.ts index b5eed7e0737..749875b3c25 100644 --- a/packages/api-page-builder/src/plugins/graphql.ts +++ b/packages/api-page-builder/src/plugins/graphql.ts @@ -110,10 +110,10 @@ export default { createPage: hasScope("pb:page:crud"), deletePage: hasScope("pb:page:crud"), - createRevisionFrom: hasScope("pb:page:revision:create"), - updateRevision: hasScope("pb:page:revision:update"), - publishRevision: hasScope("pb:page:revision:publish"), - deleteRevision: hasScope("pb:page:revision:delete"), + createRevisionFrom: hasScope("pb:page:crud"), + updateRevision: hasScope("pb:page:crud"), + publishRevision: hasScope("pb:page:crud"), + deleteRevision: hasScope("pb:page:crud"), createElement: hasScope("pb:element:crud"), updateElement: hasScope("pb:element:crud"), diff --git a/packages/api-security/src/plugins/graphql.ts b/packages/api-security/src/plugins/graphql.ts index c0e306cf23a..17806f03a62 100644 --- a/packages/api-security/src/plugins/graphql.ts +++ b/packages/api-security/src/plugins/graphql.ts @@ -2,7 +2,7 @@ import { merge } from "lodash"; import gql from "graphql-tag"; import { emptyResolver } from "@webiny/commodo-graphql"; import { GraphQLSchemaPlugin } from "@webiny/api/types"; -import { getRegisteredScopes, hasScope } from "@webiny/api-security"; +import { hasScope } from "@webiny/api-security"; import role from "./graphql/Role"; import group from "./graphql/Group"; @@ -19,8 +19,7 @@ const plugin: GraphQLSchemaPlugin = { } type SecurityQuery { - # Returns all scopes that were registered throughout the schema. - scopes: [String] + _empty: String } type SecurityMutation { @@ -59,9 +58,6 @@ const plugin: GraphQLSchemaPlugin = { Mutation: { security: emptyResolver }, - SecurityQuery: { - scopes: getRegisteredScopes - } }, install.resolvers, role.resolvers, diff --git a/packages/api-security/src/scopes.ts b/packages/api-security/src/scopes.ts index 8ab006ef1bc..528691942be 100644 --- a/packages/api-security/src/scopes.ts +++ b/packages/api-security/src/scopes.ts @@ -1,24 +1,6 @@ import { rule } from "graphql-shield"; -/** - * Contains a list of all registered scopes throughout GraphQL Schema. - * @type {Array} - */ -export const __scopes = { - registered: [] -}; - -export const registerScopes = (...scopes: Array) => { - scopes.forEach(scope => { - __scopes.registered.includes(scope) === false && __scopes.registered.push(scope); - }); -}; - -export const getRegisteredScopes = () => { - return __scopes.registered; -}; export const hasScope = (scope: string) => { - registerScopes(scope); return rule()(async (parent, args, ctx) => { if (!ctx.user) { return false; diff --git a/packages/app-form-builder/src/admin/plugins/index.ts b/packages/app-form-builder/src/admin/plugins/index.ts index 82b5c26f882..9eebae1b919 100644 --- a/packages/app-form-builder/src/admin/plugins/index.ts +++ b/packages/app-form-builder/src/admin/plugins/index.ts @@ -12,6 +12,7 @@ import previewContent from "./formDetails/previewContent"; import formRevisions from "./formDetails/formRevisions"; import formSubmissions from "./formDetails/formSubmissions"; import install from "./install"; +import scopesList from "./scopesList"; export default [ install, @@ -21,6 +22,7 @@ export default [ formSubmissions, previewContent, formRevisions, + scopesList, // Editor fields, diff --git a/packages/app-form-builder/src/admin/plugins/scopesList.ts b/packages/app-form-builder/src/admin/plugins/scopesList.ts new file mode 100644 index 00000000000..5d100515a75 --- /dev/null +++ b/packages/app-form-builder/src/admin/plugins/scopesList.ts @@ -0,0 +1,38 @@ +import { i18n } from "@webiny/app/i18n"; +import { SecurityScopesListPlugin } from "@webiny/app-security/types"; + +const t = i18n.ns("app-form-builder/admin/scopesList"); + +export default [ + { + name: "security-scopes-list-form-builder", + type: "security-scopes-list", + scopes: [ + { + scope: "forms:form:crud", + title: t`Forms CRUD`, + description: t`Allows basic CRUD operations on all forms.` + }, + { + scope: "forms:settings", + title: t`Form Builder Settings`, + description: t`Allows updating Form Builder's settings.` + }, + { + scope: "forms:form:revision:publish", + title: t`Publish form revisions`, + description: t`Allows publishing form revision.` + }, + { + scope: "forms:form:revision:unpublish", + title: t`Unpublish form revisions`, + description: t`Allows unpublishing form revisions.` + }, + { + scope: "forms:form:submissions:export", + title: t`Export form submissions`, + description: t`Allows creating form submission exports.` + } + ] + } as SecurityScopesListPlugin +]; diff --git a/packages/app-i18n/src/admin/plugins/index.ts b/packages/app-i18n/src/admin/plugins/index.ts index 80d3a3bf1b5..e90ba92e877 100644 --- a/packages/app-i18n/src/admin/plugins/index.ts +++ b/packages/app-i18n/src/admin/plugins/index.ts @@ -4,6 +4,7 @@ import routes from "./routes"; import menus from "./menus"; import richTextEditor from "./richTextEditor"; import install from "./install"; +import scopesList from "./scopesList"; /** * Prevents opening global search menu when pressing "/" inside of I18N Rich Text Editor. @@ -19,4 +20,4 @@ const globalSearchHotkey: GlobalSearchPreventHotkeyPlugin = { } }; -export default [routes, menus, richTextEditor, i18nSitePlugins, globalSearchHotkey, install]; +export default [routes, menus, scopesList, richTextEditor, i18nSitePlugins, globalSearchHotkey, install]; diff --git a/packages/app-i18n/src/admin/plugins/scopesList.ts b/packages/app-i18n/src/admin/plugins/scopesList.ts new file mode 100644 index 00000000000..c2920d2944e --- /dev/null +++ b/packages/app-i18n/src/admin/plugins/scopesList.ts @@ -0,0 +1,18 @@ +import { i18n } from "@webiny/app/i18n"; +import { SecurityScopesListPlugin } from "@webiny/app-security/types"; + +const t = i18n.ns("app-i18n/admin/scopesList"); + +export default [ + { + name: "security-scopes-list-i18n", + type: "security-scopes-list", + scopes: [ + { + scope: "i18n:locale:crud", + title: t`I18N locales CRUD`, + description: t`Allows CRUD operations on all locales.` + }, + ] + } as SecurityScopesListPlugin +]; diff --git a/packages/app-page-builder/src/admin/plugins/index.ts b/packages/app-page-builder/src/admin/plugins/index.ts index be0a1aa8f57..9d73079ed51 100644 --- a/packages/app-page-builder/src/admin/plugins/index.ts +++ b/packages/app-page-builder/src/admin/plugins/index.ts @@ -8,6 +8,7 @@ import settings from "./settings"; import routes from "./routes"; import menus from "./menus"; import install from "./install"; +import scopesList from "./scopesList"; export default [ header, @@ -19,5 +20,6 @@ export default [ settings, routes, menus, + scopesList, install ]; diff --git a/packages/app-page-builder/src/admin/plugins/scopesList.ts b/packages/app-page-builder/src/admin/plugins/scopesList.ts new file mode 100644 index 00000000000..b12e6b37555 --- /dev/null +++ b/packages/app-page-builder/src/admin/plugins/scopesList.ts @@ -0,0 +1,38 @@ +import { i18n } from "@webiny/app/i18n"; +import { SecurityScopesListPlugin } from "@webiny/app-security/types"; + +const t = i18n.ns("app-page-builder/admin/scopesList"); + +export default [ + { + name: "security-scopes-list-page-builder", + type: "security-scopes-list", + scopes: [ + { + scope: "pb:page:crud", + title: t`Pages CRUD`, + description: t`Allows CRUD operations on all pages.` + }, + { + scope: "pb:menu:crud", + title: t`Menus CRUD`, + description: t`Allows CRUD operations on all menus.` + }, + { + scope: "pb:category:crud", + title: t`Categories CRUD`, + description: t`Allows CRUD operations on all categories.` + }, + { + scope: "pb:element:crud", + title: t`Elements CRUD`, + description: t`Allows CRUD operations on all elements.` + }, + { + scope: "pb:oembed:read", + title: t`Read oEmbed data`, + description: t`Allows reading oEmbed data in the Page Builder editor.` + } + ] + } as SecurityScopesListPlugin +]; diff --git a/packages/app-security/src/admin/components/ScopesMultiAutoComplete.tsx b/packages/app-security/src/admin/components/ScopesMultiAutoComplete.tsx new file mode 100644 index 00000000000..43bfea7cf96 --- /dev/null +++ b/packages/app-security/src/admin/components/ScopesMultiAutoComplete.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from "react"; +import { i18n } from "@webiny/app/i18n"; +import { MultiAutoComplete } from "@webiny/ui/AutoComplete"; +import { getPlugins } from "@webiny/plugins"; +import { SecurityScopesListPlugin } from "@webiny/app-security/types"; +import { Typography } from "@webiny/ui/Typography"; +import { css } from "emotion"; + +const t = i18n.ns("app-security/admin/roles/form"); + +const styles = { + wrapper: css({ + ".mdc-elevation--z1": { + maxHeight: 275 + } + }) +}; + +const ScopesMultiAutoComplete = props => { + const [scopesList, setScopesList] = useState([]); + + useEffect(() => { + const getScopesList = async () => { + const plugins = getPlugins("security-scopes-list"); + const scopes = []; + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (!plugin.scopes) { + throw new Error( + `Missing "scopes" key in registered "security-scopes-list" plugin. The name of the plugin is "${plugin.name || + "undefined"}"` + ); + } + + let pluginScopes = plugin.scopes; + + if (typeof pluginScopes === "function") { + pluginScopes = await pluginScopes(); + } + + scopes.push(...pluginScopes); + } + + setScopesList(scopes); + }; + + getScopesList(); + }, []); + + return ( + + scope)} + label={t`Scopes`} + description={t`Choose one or more scopes.`} + unique + renderItem={item => { + const scope = scopesList.find(current => current.scope === item); + + return ( + <> +
+ + + {scope.title} + + +
+
+ {scope.description} +
+
+ {scope.scope} +
+ + ); + }} + {...props} + /> +
+ ); +}; + +export default ScopesMultiAutoComplete; diff --git a/packages/app-security/src/admin/components/index.ts b/packages/app-security/src/admin/components/index.ts new file mode 100644 index 00000000000..95fc853062b --- /dev/null +++ b/packages/app-security/src/admin/components/index.ts @@ -0,0 +1 @@ +export { default as ScopesMultiAutoComplete } from "./ScopesMultiAutoComplete"; diff --git a/packages/app-security/src/admin/plugins/index.ts b/packages/app-security/src/admin/plugins/index.ts index 59ee115d98d..5d8db865bd8 100644 --- a/packages/app-security/src/admin/plugins/index.ts +++ b/packages/app-security/src/admin/plugins/index.ts @@ -4,6 +4,7 @@ import routes from "./routes"; import menus from "./menus"; import secureRouteError from "./secureRouteError"; import installation from "./installation"; +import scopesList from "./scopesList"; export default [ // Layout plugins @@ -11,6 +12,7 @@ export default [ globalSearchUsers, routes, menus, + scopesList, secureRouteError, installation ]; diff --git a/packages/app-security/src/admin/plugins/scopesList.ts b/packages/app-security/src/admin/plugins/scopesList.ts new file mode 100644 index 00000000000..7577b8df1b1 --- /dev/null +++ b/packages/app-security/src/admin/plugins/scopesList.ts @@ -0,0 +1,28 @@ +import { i18n } from "@webiny/app/i18n"; +import { SecurityScopesListPlugin } from "@webiny/app-security/types"; + +const t = i18n.ns("app-security/admin/scopesList"); + +export default [ + { + name: "security-scopes-list-security", + type: "security-scopes-list", + scopes: [ + { + scope: "security:group:crud", + title: t`Security groups CRUD`, + description: t`Allows CRUD operations on all groups.` + }, + { + scope: "security:role:crud", + title: t`Security roles CRUD`, + description: t`Allows CRUD operations on all roles.` + }, + { + scope: "security:user:crud", + title: t`Security users CRUD`, + description: t`Allows CRUD operations on all users.` + } + ] + } as SecurityScopesListPlugin +]; diff --git a/packages/app-security/src/admin/views/Groups/Groups.tsx b/packages/app-security/src/admin/views/Groups/Groups.tsx index b53e8a900b7..d8f31743125 100644 --- a/packages/app-security/src/admin/views/Groups/Groups.tsx +++ b/packages/app-security/src/admin/views/Groups/Groups.tsx @@ -43,7 +43,10 @@ const Groups = ({ scopes, formProps, listProps }: any) => { - + )} diff --git a/packages/app-security/src/admin/views/Groups/GroupsDataList.tsx b/packages/app-security/src/admin/views/Groups/GroupsDataList.tsx index 6843d9376da..abe62a99100 100644 --- a/packages/app-security/src/admin/views/Groups/GroupsDataList.tsx +++ b/packages/app-security/src/admin/views/Groups/GroupsDataList.tsx @@ -42,7 +42,7 @@ const GroupsDataList = () => { ]} > {({ data, select, isSelected }) => ( - + {data.map(item => ( select(item)}> @@ -56,9 +56,7 @@ const GroupsDataList = () => { {({ showConfirmation }) => ( - showConfirmation(() => - actions.deleteRecord(item) - ) + showConfirmation(() => actions.delete(item)) } /> )} diff --git a/packages/app-security/src/admin/views/Roles/Roles.tsx b/packages/app-security/src/admin/views/Roles/Roles.tsx index e6620c669a4..766ee8e685c 100644 --- a/packages/app-security/src/admin/views/Roles/Roles.tsx +++ b/packages/app-security/src/admin/views/Roles/Roles.tsx @@ -28,7 +28,10 @@ function Roles() { - + )} diff --git a/packages/app-security/src/admin/views/Roles/RolesDataList.tsx b/packages/app-security/src/admin/views/Roles/RolesDataList.tsx index 4befc119f2f..bbd2aa1fb1a 100644 --- a/packages/app-security/src/admin/views/Roles/RolesDataList.tsx +++ b/packages/app-security/src/admin/views/Roles/RolesDataList.tsx @@ -42,7 +42,7 @@ const RolesDataList = () => { ]} > {({ data, isSelected, select }) => ( - + {data.map(item => ( select(item)}> diff --git a/packages/app-security/src/admin/views/Roles/RolesForm.tsx b/packages/app-security/src/admin/views/Roles/RolesForm.tsx index 594b15b0f44..4e3da6895b6 100644 --- a/packages/app-security/src/admin/views/Roles/RolesForm.tsx +++ b/packages/app-security/src/admin/views/Roles/RolesForm.tsx @@ -1,16 +1,13 @@ -import * as React from "react"; +import React from "react"; import { i18n } from "@webiny/app/i18n"; import { Form } from "@webiny/form"; import { Grid, Cell } from "@webiny/ui/Grid"; import { Input } from "@webiny/ui/Input"; import { ButtonPrimary } from "@webiny/ui/Button"; -import { MultiAutoComplete } from "@webiny/ui/AutoComplete"; +import { ScopesMultiAutoComplete } from "@webiny/app-security/admin/components"; import { CircularProgress } from "@webiny/ui/Progress"; -import { useQuery } from "react-apollo"; import { useCrud } from "@webiny/app-admin/hooks/useCrud"; import { validation } from "@webiny/validation"; -import { get } from "lodash"; -import { LIST_SCOPES } from "./graphql"; import { SimpleForm, SimpleFormFooter, @@ -21,14 +18,12 @@ import { const t = i18n.ns("app-security/admin/roles/form"); const RoleForm = () => { - const scopesQuery = useQuery(LIST_SCOPES); - const scopes = get(scopesQuery, "data.security.scopes") || []; const { form: crudForm } = useCrud(); return (
{({ data, form, Bind }) => ( - + {crudForm.loading && } @@ -54,14 +49,7 @@ const RoleForm = () => { - + diff --git a/packages/app-security/src/admin/views/Roles/graphql.ts b/packages/app-security/src/admin/views/Roles/graphql.ts index 714ba4d3e67..426519e18c2 100644 --- a/packages/app-security/src/admin/views/Roles/graphql.ts +++ b/packages/app-security/src/admin/views/Roles/graphql.ts @@ -8,14 +8,6 @@ const fields = ` scopes `; -export const LIST_SCOPES = gql` - query loadScopes { - security { - scopes - } - } -`; - export const LIST_ROLES = gql` query listRoles( $where: JSON diff --git a/packages/app-security/src/types.ts b/packages/app-security/src/types.ts index 81abece4f64..67be1c626b7 100644 --- a/packages/app-security/src/types.ts +++ b/packages/app-security/src/types.ts @@ -34,3 +34,21 @@ export type SecurityViewUserFormPlugin = Plugin & { export type SecurityViewUserAccountFormPlugin = Plugin & { view: React.ComponentType; }; + +type SecurityScopesListPluginScope = { + title: any; + description: any; + scope: string; +}; + +/** + * Enables adding custom security scopes to the multi-select autocomplete component in the Roles form. + * @see https://docs.webiny.com/docs/webiny-apps/security/development/plugin-reference/app/#security-scopes-list + */ +export type SecurityScopesListPlugin = Plugin & { + type: "security-scopes-list"; + scopes: + | SecurityScopesListPluginScope[] + | (() => SecurityScopesListPluginScope[]) + | (() => Promise); +}; diff --git a/packages/ui/src/AutoComplete/MultiAutoComplete.tsx b/packages/ui/src/AutoComplete/MultiAutoComplete.tsx index 57f3a783696..cd344ed6704 100644 --- a/packages/ui/src/AutoComplete/MultiAutoComplete.tsx +++ b/packages/ui/src/AutoComplete/MultiAutoComplete.tsx @@ -62,17 +62,16 @@ export class MultiAutoComplete extends React.Component - getOptionValue(value, this.props) === getOptionValue(item, this.props) - ) - ) { - return false; + if (Array.isArray(values)) { + if ( + values.find( + value => + getOptionValue(value, this.props) === + getOptionValue(item, this.props) + ) + ) { + return false; + } } }