From 254a0b12c4c0018ba48535c9d83381c7669db092 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Wed, 1 Sep 2021 10:11:12 -0700 Subject: [PATCH 01/19] initial commit --- ui/public/data/bundles.json | 10 +++ ui/src/app/core/components/Header.js | 12 +-- ui/src/app/metadata/Attribute.js | 4 + .../attribute/AttributeBundleDefinition.js | 29 +++++++ .../hoc/attribute/AttributeBundleApi.js | 69 +++++++++++++++ .../hoc/attribute/AttributeBundleList.js | 14 +++ .../hoc/attribute/AttributeBundleSelector.js | 14 +++ ui/src/app/metadata/new/NewBundle.js | 0 .../view/MetadataAttributeBundleEdit.js | 86 +++++++++++++++++++ .../metadata/view/MetadataAttributeBundles.js | 82 ++++++++++++++++++ 10 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 ui/public/data/bundles.json create mode 100644 ui/src/app/metadata/domain/attribute/AttributeBundleDefinition.js create mode 100644 ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js create mode 100644 ui/src/app/metadata/hoc/attribute/AttributeBundleList.js create mode 100644 ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js create mode 100644 ui/src/app/metadata/new/NewBundle.js create mode 100644 ui/src/app/metadata/view/MetadataAttributeBundleEdit.js create mode 100644 ui/src/app/metadata/view/MetadataAttributeBundles.js diff --git a/ui/public/data/bundles.json b/ui/public/data/bundles.json new file mode 100644 index 000000000..11a20d22d --- /dev/null +++ b/ui/public/data/bundles.json @@ -0,0 +1,10 @@ +[ + { + "resourceId": "abc", + "name": "Bundle 1", + "attributes": [ + "abc123", + "xyz456" + ] + } +] \ No newline at end of file diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index 0c925ac50..95047f1e0 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -4,11 +4,9 @@ import { Link } from 'react-router-dom'; import Nav from 'react-bootstrap/Nav'; import Navbar from 'react-bootstrap/Navbar'; import Dropdown from 'react-bootstrap/Dropdown'; -import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; -import Tooltip from 'react-bootstrap/Tooltip'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog, faUser, faSpinner, faUserCircle } from '@fortawesome/free-solid-svg-icons'; +import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog, faSpinner, faUserCircle, faCog, faLayerGroup, faFileArchive } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; import { useTranslator } from '../../i18n/hooks'; @@ -67,7 +65,7 @@ export function Header () { {isAdmin && - + @@ -75,6 +73,10 @@ export function Header () { + + + + @@ -84,7 +86,7 @@ export function Header () { } - + diff --git a/ui/src/app/metadata/Attribute.js b/ui/src/app/metadata/Attribute.js index c8d2aa502..a963887d1 100644 --- a/ui/src/app/metadata/Attribute.js +++ b/ui/src/app/metadata/Attribute.js @@ -4,6 +4,7 @@ import { MetadataAttributes } from './hoc/MetadataAttributes'; import { NewAttribute } from './new/NewAttribute'; import { MetadataAttributeEdit } from './view/MetadataAttributeEdit'; import { MetadataAttributeList } from './view/MetadataAttributeList'; +import { MetadataAttributeBundles } from './view/MetadataAttributeBundles'; export function Attribute() { @@ -24,6 +25,9 @@ export function Attribute() { } /> + + + } /> ); diff --git a/ui/src/app/metadata/domain/attribute/AttributeBundleDefinition.js b/ui/src/app/metadata/domain/attribute/AttributeBundleDefinition.js new file mode 100644 index 000000000..464bd1d59 --- /dev/null +++ b/ui/src/app/metadata/domain/attribute/AttributeBundleDefinition.js @@ -0,0 +1,29 @@ +import { defaultsDeep } from "lodash"; + +export const AttributeBundleDefinition = { + label: 'Metadata Attribute Bundle', + type: '@MetadataAttributeBundle', + steps: [], + schema: `/assets/schema/attribute/bundle.schema.json`, + + uiSchema: { + + }, + + parser: (data) => { + return data; + }, + + formatter: (changes) => { + return changes; + } +} + +export const CustomAttributeEditor = { + ...AttributeBundleDefinition, + uiSchema: defaultsDeep({ + attributeType: { + 'ui:disabled': true + } + }, AttributeBundleDefinition.uiSchema) +}; \ No newline at end of file diff --git a/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js b/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js new file mode 100644 index 000000000..c7ce85380 --- /dev/null +++ b/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js @@ -0,0 +1,69 @@ +import React from 'react'; +import useFetch from 'use-http'; +import API_BASE_PATH from '../../../App.constant'; + +import { DeleteConfirmation } from '../../../core/components/DeleteConfirmation'; +import { createNotificationAction, NotificationContext } from '../../../notifications/hoc/Notifications'; + +const api = '/custom/entity/bundles' + +export function AttributeBundleApi({ id, children }) { + + const { dispatch } = React.useContext(NotificationContext); + + const { get, put, post, del, response, loading } = useFetch(`/data/bundles.json`, { + cachePolicy: 'no-cache' + }); + + async function load(cb) { + const b = await get(``); + if (response.ok) { + cb && cb(b); + } + } + + async function find(id, cb) { + const b = await get(`/${id }`); + if (response.ok) { + cb && cb(b); + } + } + + async function update(id, body, cb) { + const b = await put(`/${id}`, body); + if (response.ok) { + dispatch(createNotificationAction( + `Bundle has been updated.` + )); + cb && cb(b); + } + } + + async function create(body, cb) { + const b = await post(``, body); + if (response.ok) { + dispatch(createNotificationAction( + `Bundle has been created.` + )); + cb && cb(b); + } + } + + async function remove(id, cb = () => { }) { + await del(`/${id}`); + if (response.ok) { + dispatch(createNotificationAction( + `Bundle has been deleted.` + )); + cb(); + } + } + + return ( + + {(block) => +
{children(load, find, create, update, (id) => block(() => remove(id)), loading)}
+ } +
+ ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/hoc/attribute/AttributeBundleList.js b/ui/src/app/metadata/hoc/attribute/AttributeBundleList.js new file mode 100644 index 000000000..0cf5b54d7 --- /dev/null +++ b/ui/src/app/metadata/hoc/attribute/AttributeBundleList.js @@ -0,0 +1,14 @@ +import React from 'react'; + +export function AttributeBundleList({ load, children }) { + + const [bundles, setBundles] = React.useState([]); + + React.useEffect(() => { + load((list) => setBundles(list)); + }, []); + + return ( + {children(bundles)} + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js b/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js new file mode 100644 index 000000000..2ec7ffe4d --- /dev/null +++ b/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +export function AttributeBundleSelector({ id, find, children }) { + const [bundle, setBundle] = React.useState([]); + + React.useEffect(() => { + find(id, (item) => setBundle(item)); + }, []); + + return ( + {children(bundle)} + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/new/NewBundle.js b/ui/src/app/metadata/new/NewBundle.js new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js b/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js new file mode 100644 index 000000000..e84395b89 --- /dev/null +++ b/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js @@ -0,0 +1,86 @@ +import React from 'react'; +import { faSave, faSpinner } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import Button from 'react-bootstrap/Button'; +import { Prompt, useHistory, useParams } from 'react-router'; +import Translate from '../../i18n/components/translate'; +import { MetadataAttributeEditor } from '../editor/MetadataAttributeEditor'; + +import { AttributeBundleDefinition } from '../domain/attribute/AttributeBundleDefinition'; +import MetadataSchema from '../hoc/MetadataSchema'; +import { MetadataForm } from '../hoc/MetadataFormContext'; +import { AttributeBundleSelector } from '../hoc/attribute/AttributeBundleSelector'; +import { AttributeBundleApi } from '../hoc/attribute/AttributeBundleApi'; + +export function MetadataAttributeBundleEdit() { + const { id } = useParams(); + const history = useHistory(); + + const definition = AttributeBundleDefinition; + + const [blocking, setBlocking] = React.useState(false); + + const cancel = () => { + gotoDetail(); + }; + + const gotoDetail = (state = null) => { + setBlocking(false); + history.push(`/metadata/attributes`, state); + }; + + return ( + + {(load, find, create, update, remove, loading) => + + {(bundle) =>
+ + `message.unsaved-editor` + } + /> +
+
+
+
+ Add a new metadata attribute +
+
+
+
+ + {bundle && + + + {(filter, errors) => + + + + + } + + + } + +
+
+
+ } +
+ } +
+ ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/view/MetadataAttributeBundles.js b/ui/src/app/metadata/view/MetadataAttributeBundles.js new file mode 100644 index 000000000..520080b41 --- /dev/null +++ b/ui/src/app/metadata/view/MetadataAttributeBundles.js @@ -0,0 +1,82 @@ +import React from 'react'; +import { faEdit, faPlusCircle, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import Button from 'react-bootstrap/Button'; +import { Link } from 'react-router-dom'; + +import { Translate } from '../../i18n/components/translate'; +import { useTranslator } from '../../i18n/hooks'; + +import { AttributeBundleApi } from '../hoc/attribute/AttributeBundleApi'; + +import { AttributeBundleList } from '../hoc/attribute/AttributeBundleList'; + +export function MetadataAttributeBundles({ entities, onDelete }) { + + const translator = useTranslator(); + + return ( + + {(load, find, create, update, remove, loading) => + + {(bundles) => +
+
+
+
+ + Attribute Bundles + +
+
+
+ +   + Add new bundle + +
+
+ + + + + + + + + {bundles.map((bundle, i) => + + + + + )} + +
+ Bundle Name + Actions
{bundle.name} + + + + Edit + + + +
+
+
+
+ +
+
+ } +
+ } +
+ ); +} \ No newline at end of file From 15605ecbd0eab298eb2dc278a798d16ecceac5c3 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Wed, 1 Sep 2021 10:58:10 -0700 Subject: [PATCH 02/19] Fixed missing dep --- ui/src/app/core/components/Header.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index f0bb81c3e..803bb8483 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -7,7 +7,7 @@ import Dropdown from 'react-bootstrap/Dropdown'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog, faSpinner, faSpinner, faUserCircle, faCog } from '@fortawesome/free-solid-svg-icons'; +import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog, faSpinner, faUserCircle, faCog, faFileArchive } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; import { useTranslator } from '../../i18n/hooks'; From c097f0c497a58749bd54f29d8b6346e3d9e1bbb0 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 2 Sep 2021 09:48:39 -0700 Subject: [PATCH 03/19] Updated bundle admin --- backend/src/main/resources/bundle.schema.json | 38 +++++++++ .../main/resources/i18n/messages.properties | 6 +- .../schema/attribute/bundle.schema.json | 38 +++++++++ .../assets/schema/source/metadata-source.json | 1 + ui/src/app/core/components/Header.js | 20 ++--- ui/src/app/metadata/Attribute.js | 10 ++- .../attribute/AttributeBundleDefinition.js | 4 +- .../metadata/editor/AttributeBundleEditor.js | 56 +++++++++++++ ui/src/app/metadata/new/NewBundle.js | 81 +++++++++++++++++++ .../view/MetadataAttributeBundleEdit.js | 8 +- .../metadata/view/MetadataAttributeBundles.js | 2 +- .../metadata/view/MetadataAttributeEdit.js | 4 +- .../component/NotificationList.js | 2 +- 13 files changed, 249 insertions(+), 21 deletions(-) create mode 100644 backend/src/main/resources/bundle.schema.json create mode 100644 ui/public/assets/schema/attribute/bundle.schema.json create mode 100644 ui/src/app/metadata/editor/AttributeBundleEditor.js diff --git a/backend/src/main/resources/bundle.schema.json b/backend/src/main/resources/bundle.schema.json new file mode 100644 index 000000000..3c5111e40 --- /dev/null +++ b/backend/src/main/resources/bundle.schema.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "title": "label.bundle-name", + "description": "tooltip.bundle-name", + "minLength": 1, + "maxLength": 255 + }, + "attributes": { + "type": "array", + "title": "label.attributes", + "description": "Attribute table - select the attributes you want to bundle (default unchecked)", + "items": { + "type": "string", + "enum": [ + "eduPersonPrincipalName", + "uid", + "mail", + "surname", + "givenName", + "eduPersonAffiliation", + "eduPersonScopedAffiliation", + "eduPersonPrimaryAffiliation", + "eduPersonEntitlement", + "eduPersonAssurance", + "eduPersonUniqueId", + "employeeNumber" + ] + }, + "uniqueItems": true + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index e76a6c8ee..406c587a1 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -493,7 +493,11 @@ label.by=By label.source=Metadata Source label.provider=Metadata Provider - +label.bundle-name=Bundle name +action.add-new-bundle=Add bundle +tooltip.bundle-name=A user friendly name to identify the bundle +action.attribute-bundles=Attribute bundles +label.new-attribute-bundle=New attribute bundle message.user-role-admin-group=Cannot change group for ROLE_ADMIN users. diff --git a/ui/public/assets/schema/attribute/bundle.schema.json b/ui/public/assets/schema/attribute/bundle.schema.json new file mode 100644 index 000000000..3c5111e40 --- /dev/null +++ b/ui/public/assets/schema/attribute/bundle.schema.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "title": "label.bundle-name", + "description": "tooltip.bundle-name", + "minLength": 1, + "maxLength": 255 + }, + "attributes": { + "type": "array", + "title": "label.attributes", + "description": "Attribute table - select the attributes you want to bundle (default unchecked)", + "items": { + "type": "string", + "enum": [ + "eduPersonPrincipalName", + "uid", + "mail", + "surname", + "givenName", + "eduPersonAffiliation", + "eduPersonScopedAffiliation", + "eduPersonPrimaryAffiliation", + "eduPersonEntitlement", + "eduPersonAssurance", + "eduPersonUniqueId", + "employeeNumber" + ] + }, + "uniqueItems": true + } + } +} \ No newline at end of file diff --git a/ui/public/assets/schema/source/metadata-source.json b/ui/public/assets/schema/source/metadata-source.json index dc19fe8d4..eaf89a5f0 100644 --- a/ui/public/assets/schema/source/metadata-source.json +++ b/ui/public/assets/schema/source/metadata-source.json @@ -219,6 +219,7 @@ "type": "string", "enum": [ "eduPersonPrincipalName", + {"name": "foo", "attributes": []}, "uid", "mail", "surname", diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index dc115b6af..dbade4c52 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -7,7 +7,7 @@ import Dropdown from 'react-bootstrap/Dropdown'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog, faSpinner, faUserCircle, faCog, faFileArchive, faUserTag } from '@fortawesome/free-solid-svg-icons'; +import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog, faSpinner, faUserCircle, faCog, faFileArchive, faUserTag, faBoxOpen, faTags, faIdBadge } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; import { useTranslator } from '../../i18n/hooks'; @@ -49,16 +49,16 @@ export function Header () { - + - + {isAdmin && - + } @@ -71,19 +71,19 @@ export function Header () { - + - - + + - + - + @@ -100,7 +100,7 @@ export function Header () { - + Logout
diff --git a/ui/src/app/metadata/Attribute.js b/ui/src/app/metadata/Attribute.js index a963887d1..2bb653429 100644 --- a/ui/src/app/metadata/Attribute.js +++ b/ui/src/app/metadata/Attribute.js @@ -5,6 +5,8 @@ import { NewAttribute } from './new/NewAttribute'; import { MetadataAttributeEdit } from './view/MetadataAttributeEdit'; import { MetadataAttributeList } from './view/MetadataAttributeList'; import { MetadataAttributeBundles } from './view/MetadataAttributeBundles'; +import { NewBundle } from './new/NewBundle'; +import { MetadataAttributeBundleEdit } from './view/MetadataAttributeBundleEdit'; export function Attribute() { @@ -25,9 +27,15 @@ export function Attribute() { } /> - + } /> + + + } /> + + + } /> ); diff --git a/ui/src/app/metadata/domain/attribute/AttributeBundleDefinition.js b/ui/src/app/metadata/domain/attribute/AttributeBundleDefinition.js index 464bd1d59..f94bec721 100644 --- a/ui/src/app/metadata/domain/attribute/AttributeBundleDefinition.js +++ b/ui/src/app/metadata/domain/attribute/AttributeBundleDefinition.js @@ -7,7 +7,9 @@ export const AttributeBundleDefinition = { schema: `/assets/schema/attribute/bundle.schema.json`, uiSchema: { - + attributes: { + 'ui:widget': 'AttributeReleaseWidget' + } }, parser: (data) => { diff --git a/ui/src/app/metadata/editor/AttributeBundleEditor.js b/ui/src/app/metadata/editor/AttributeBundleEditor.js new file mode 100644 index 000000000..0f04ba9ad --- /dev/null +++ b/ui/src/app/metadata/editor/AttributeBundleEditor.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { MetadataFormContext, setFormDataAction, setFormErrorAction } from '../hoc/MetadataFormContext'; +import { MetadataDefinitionContext, MetadataSchemaContext } from '../hoc/MetadataSchema'; +import { transformErrors } from '../domain/transform'; + +import Form from '@rjsf/bootstrap-4'; + +import { fields, widgets } from '../../form/component'; +import { templates } from '../../form/component'; + +function ErrorListTemplate() { + return (<>); +} + +export function AttributeBundleEditor({ children }) { + + const definition = React.useContext(MetadataDefinitionContext); + const schema = React.useContext(MetadataSchemaContext); + + const { state, dispatch } = React.useContext(MetadataFormContext); + const { metadata, errors } = state; + + const onChange = (changes) => { + dispatch(setFormDataAction(changes.formData)); + dispatch(setFormErrorAction(changes.errors)); + // setBlocking(true); + }; + + return ( +
+
+ {children(metadata, errors)} +
+
+
+
+
onChange(form)} + schema={schema} + uiSchema={definition.uiSchema} + FieldTemplate={templates.FieldTemplate} + ObjectFieldTemplate={templates.ObjectFieldTemplate} + ArrayFieldTemplate={templates.ArrayFieldTemplate} + fields={fields} + widgets={widgets} + liveValidate={true} + ErrorList={ErrorListTemplate} + transformErrors={transformErrors}> + <> +
+
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/new/NewBundle.js b/ui/src/app/metadata/new/NewBundle.js index e69de29bb..4302b4ada 100644 --- a/ui/src/app/metadata/new/NewBundle.js +++ b/ui/src/app/metadata/new/NewBundle.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { faSave, faSpinner } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import Button from 'react-bootstrap/Button'; + +import { Prompt, useHistory } from 'react-router'; +import Translate from '../../i18n/components/translate'; + +import { AttributeBundleDefinition } from '../domain/attribute/AttributeBundleDefinition'; +import MetadataSchema from '../hoc/MetadataSchema'; +import { MetadataForm } from '../hoc/MetadataFormContext'; +import { AttributeBundleEditor } from '../editor/AttributeBundleEditor'; +import { AttributeBundleApi } from '../hoc/attribute/AttributeBundleApi'; + +export function NewBundle() { + const history = useHistory(); + + console.log('hi') + + const definition = AttributeBundleDefinition; + + const [blocking, setBlocking] = React.useState(false); + + const cancel = () => { + gotoDetail(); + }; + + const gotoDetail = (state = null) => { + setBlocking(false); + history.push(`/metadata/attributes/bundles`, state); + }; + + return ( + + {(load, find, create, update, remove, loading) => +
+ + `message.unsaved-editor` + } + /> +
+
+
+
+ Add a new attribute bundle +
+
+
+
+ + + + {(bundle, errors) => + + + + + } + + + +
+
+
+ } +
+ ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js b/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js index e84395b89..914cb4347 100644 --- a/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js +++ b/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js @@ -27,7 +27,7 @@ export function MetadataAttributeBundleEdit() { const gotoDetail = (state = null) => { setBlocking(false); - history.push(`/metadata/attributes`, state); + history.push(`/metadata/attributes/bundles`, state); }; return ( @@ -58,15 +58,15 @@ export function MetadataAttributeBundleEdit() { diff --git a/ui/src/app/metadata/view/MetadataAttributeBundles.js b/ui/src/app/metadata/view/MetadataAttributeBundles.js index 520080b41..056994fcf 100644 --- a/ui/src/app/metadata/view/MetadataAttributeBundles.js +++ b/ui/src/app/metadata/view/MetadataAttributeBundles.js @@ -31,7 +31,7 @@ export function MetadataAttributeBundles({ entities, onDelete }) {
- +   Add new bundle diff --git a/ui/src/app/metadata/view/MetadataAttributeEdit.js b/ui/src/app/metadata/view/MetadataAttributeEdit.js index d0c04f287..a1297b6d4 100644 --- a/ui/src/app/metadata/view/MetadataAttributeEdit.js +++ b/ui/src/app/metadata/view/MetadataAttributeEdit.js @@ -86,13 +86,13 @@ export function MetadataAttributeEdit() { type="button" onClick={() => save(filter)} disabled={errors.length > 0 || loading} - aria-label="Save changes to the metadata source. You will return to the dashboard"> + aria-label="Save changes to the custom attribute. You will return to the attribute list">   Save diff --git a/ui/src/app/notifications/component/NotificationList.js b/ui/src/app/notifications/component/NotificationList.js index 7d621f5c3..c4ec3a6b5 100644 --- a/ui/src/app/notifications/component/NotificationList.js +++ b/ui/src/app/notifications/component/NotificationList.js @@ -9,7 +9,7 @@ export function NotificationList () { const onRemove = (id) => dispatch(removeNotificationAction(id)); return ( -
    +
      {state.notifications.map((n) => (
    • From c6ff686446d9ad2fdb9f18aeb98a8d696010f99e Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 2 Sep 2021 14:17:36 -0700 Subject: [PATCH 04/19] Changes for source attr release --- .../assets/schema/source/metadata-source.json | 74 +++++++++++-------- ui/src/app/dashboard/view/SourcesTab.js | 2 +- .../widgets/AttributeReleaseWidget.js | 27 ++++++- .../source/definition/SourceDefinition.js | 3 +- ui/src/app/metadata/wizard/Wizard.js | 2 +- 5 files changed, 72 insertions(+), 36 deletions(-) diff --git a/ui/public/assets/schema/source/metadata-source.json b/ui/public/assets/schema/source/metadata-source.json index eaf89a5f0..0a2f6baba 100644 --- a/ui/public/assets/schema/source/metadata-source.json +++ b/ui/public/assets/schema/source/metadata-source.json @@ -22,8 +22,7 @@ "serviceEnabled": { "title": "label.enable-this-service", "description": "tooltip.enable-this-service-upon-saving", - "type": "boolean", - "default": false + "type": "boolean" }, "organization": { "$ref": "#/definitions/Organization" @@ -76,8 +75,7 @@ }, "properties": { "x509CertificateAvailable": { - "type": "boolean", - "default": true + "type": "boolean" }, "authenticationRequestsSigned": { "title": "label.authentication-requests-signed", @@ -161,11 +159,16 @@ "type": "boolean", "default": false }, - "dontSignResponse": { - "title": "label.dont-sign-the-response", - "description": "tooltip.dont-sign-response", - "type": "boolean", - "default": false + "nameIdFormats": { + "$ref": "#/definitions/nameIdFormats" + }, + "responderId": { + "title": "label.responder-id", + "description": "tooltip.responder-id", + "type": "string" + }, + "authenticationMethods": { + "$ref": "#/definitions/authenticationMethods" }, "turnOffEncryption": { "title": "label.turn-off-encryption-of-response", @@ -173,9 +176,15 @@ "type": "boolean", "default": false }, - "useSha": { - "title": "label.use-sha1-signing-algorithm", - "description": "tooltip.usa-sha-algorithm", + "forceAuthn": { + "title": "label.force-authn", + "description": "tooltip.force-authn", + "type": "boolean", + "default": false + }, + "dontSignResponse": { + "title": "label.dont-sign-the-response", + "description": "tooltip.dont-sign-response", "type": "boolean", "default": false }, @@ -185,27 +194,15 @@ "type": "boolean", "default": false }, - "omitNotBefore": { - "title": "label.omit-not-before-condition", - "description": "tooltip.omit-not-before-condition", + "useSha": { + "title": "label.use-sha1-signing-algorithm", + "description": "tooltip.usa-sha-algorithm", "type": "boolean", "default": false }, - "responderId": { - "title": "label.responder-id", - "description": "tooltip.responder-id", - "type": "string", - "default": "" - }, - "nameIdFormats": { - "$ref": "#/definitions/nameIdFormats" - }, - "authenticationMethods": { - "$ref": "#/definitions/authenticationMethods" - }, - "forceAuthn": { - "title": "label.force-authn", - "description": "tooltip.force-authn", + "omitNotBefore": { + "title": "label.omit-not-before-condition", + "description": "tooltip.omit-not-before-condition", "type": "boolean", "default": false } @@ -218,8 +215,19 @@ "items": { "type": "string", "enum": [ + [ + "givenName", + "eduPersonAffiliation", + "eduPersonScopedAffiliation", + "employeeNumber" + ], + [ + "uid", + "surname", + "eduPersonAffiliation", + "employeeNumber" + ], "eduPersonPrincipalName", - {"name": "foo", "attributes": []}, "uid", "mail", "surname", @@ -231,6 +239,10 @@ "eduPersonAssurance", "eduPersonUniqueId", "employeeNumber" + ], + "enumNames": [ + "Bundle 1", + "Bundle 2" ] }, "uniqueItems": true diff --git a/ui/src/app/dashboard/view/SourcesTab.js b/ui/src/app/dashboard/view/SourcesTab.js index e69198fb6..36cdc1bdf 100644 --- a/ui/src/app/dashboard/view/SourcesTab.js +++ b/ui/src/app/dashboard/view/SourcesTab.js @@ -59,7 +59,7 @@ export function SourcesTab () { {(enable, remove) => remove(id, loadSources)} onEnable={(s, e) => enable(s, e, loadSources) } onChangeGroup={changeSourceGroup} /> diff --git a/ui/src/app/form/component/widgets/AttributeReleaseWidget.js b/ui/src/app/form/component/widgets/AttributeReleaseWidget.js index 9ff995e59..8483f7931 100644 --- a/ui/src/app/form/component/widgets/AttributeReleaseWidget.js +++ b/ui/src/app/form/component/widgets/AttributeReleaseWidget.js @@ -4,6 +4,7 @@ import Translate from "../../../i18n/components/translate"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; import Button from 'react-bootstrap/Button'; +import { useTranslator } from "../../../i18n/hooks"; const selectValue = (value, selected, all) => { const at = all.indexOf(value); @@ -59,6 +60,17 @@ const AttributeReleaseWidget = ({ onChange(update); } + + const onCheckBundle = (option) => { + const all = (enumOptions).map(({ value }) => value); + let update = [ + ...value + ]; + (option.value).forEach(v => update = selectValue(v, update, all)); + + onChange(update); + } + const onClearAll = () => { onChange([]); } @@ -78,11 +90,21 @@ const AttributeReleaseWidget = ({ const checked = value.indexOf(option.value) !== -1; const itemDisabled = enumDisabled && (enumDisabled).indexOf(option.value) !== -1; + const bundled = typeof option.value === 'string' ? false : true; return ( - - + + {bundled ? + {option.label} + : + {option.label} + }
      + {bundled ? + + :
      + }
      diff --git a/ui/src/app/metadata/domain/source/definition/SourceDefinition.js b/ui/src/app/metadata/domain/source/definition/SourceDefinition.js index 6f7276255..c650a1f95 100644 --- a/ui/src/app/metadata/domain/source/definition/SourceDefinition.js +++ b/ui/src/app/metadata/domain/source/definition/SourceDefinition.js @@ -10,7 +10,8 @@ export const SourceBase = { label: 'Metadata Source', type: '@MetadataProvider', steps: [], - schema: `${API_BASE_PATH}/ui/MetadataSources`, + //schema: `${API_BASE_PATH}/ui/MetadataSources`, + schema: `/assets/schema/source/metadata-source.json`, parser: (data) => removeNull(data, true), diff --git a/ui/src/app/metadata/wizard/Wizard.js b/ui/src/app/metadata/wizard/Wizard.js index cc52249fe..644c911a4 100644 --- a/ui/src/app/metadata/wizard/Wizard.js +++ b/ui/src/app/metadata/wizard/Wizard.js @@ -6,7 +6,7 @@ const WizardContext = React.createContext(); const { Provider, Consumer } = WizardContext; const initialState = { - current: 'common', + current: 'attribute', disabled: false, loading: false }; From 9319c71ae3e5d0bd75f9dcf2e5ed1feac36c7d9c Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Tue, 7 Sep 2021 13:46:36 -0700 Subject: [PATCH 05/19] Implemented bundles for sources --- ui/src/app/core/components/Header.js | 2 +- .../widgets/AttributeReleaseWidget.js | 55 ++++++++++++++----- .../hoc/attribute/AttributeBundleApi.js | 2 +- .../hoc/attribute/AttributeBundleList.js | 1 + .../hoc/attribute/AttributeBundleSelector.js | 2 +- .../metadata/view/MetadataAttributeBundles.js | 3 - ui/src/app/metadata/wizard/Wizard.js | 2 +- 7 files changed, 45 insertions(+), 22 deletions(-) diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index dbade4c52..4847c83f5 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -7,7 +7,7 @@ import Dropdown from 'react-bootstrap/Dropdown'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog, faSpinner, faUserCircle, faCog, faFileArchive, faUserTag, faBoxOpen, faTags, faIdBadge } from '@fortawesome/free-solid-svg-icons'; +import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog, faSpinner, faUserCircle, faCog, faBoxOpen, faTags, faIdBadge } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; import { useTranslator } from '../../i18n/hooks'; diff --git a/ui/src/app/form/component/widgets/AttributeReleaseWidget.js b/ui/src/app/form/component/widgets/AttributeReleaseWidget.js index 8483f7931..1471f0485 100644 --- a/ui/src/app/form/component/widgets/AttributeReleaseWidget.js +++ b/ui/src/app/form/component/widgets/AttributeReleaseWidget.js @@ -1,10 +1,10 @@ import React from "react"; import Form from "react-bootstrap/Form"; +import intersection from 'lodash/intersection'; import Translate from "../../../i18n/components/translate"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; import Button from 'react-bootstrap/Button'; -import { useTranslator } from "../../../i18n/hooks"; const selectValue = (value, selected, all) => { const at = all.indexOf(value); @@ -71,13 +71,49 @@ const AttributeReleaseWidget = ({ onChange(update); } + const onUncheckBundle = (option) => { + const all = (enumOptions).map(({ value }) => value); + let update = [ + ...value + ]; + (option.value).forEach(v => update = deselectValue(v, update, all)); + + onChange(update); + } + const onClearAll = () => { onChange([]); } + const attrs = React.useMemo(() => enumOptions.filter(e => !(typeof e.value === 'string' ? false : true)), [enumOptions]); + const bundles = React.useMemo(() => enumOptions.filter(e => (typeof e.value === 'string' ? false : true)), [enumOptions]); + + const bundlelist = React.useMemo(() => bundles.map((b) => ( + { + ...b, + selected: intersection(b.value, value).length === b.value.length + } + )), [bundles, value]); + return (
      + {bundles && bundles.length > 0 && +
        + {(bundlelist).map((option) => ( +
      • + {option.label} + +
      • + ))} +
      + } + @@ -86,25 +122,15 @@ const AttributeReleaseWidget = ({ - {(enumOptions).map((option, index) => { + {(attrs).map((option, index) => { const checked = value.indexOf(option.value) !== -1; const itemDisabled = enumDisabled && (enumDisabled).indexOf(option.value) !== -1; - const bundled = typeof option.value === 'string' ? false : true; return ( - - + + diff --git a/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js b/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js index c7ce85380..5abc3d103 100644 --- a/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js +++ b/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js @@ -5,7 +5,7 @@ import API_BASE_PATH from '../../../App.constant'; import { DeleteConfirmation } from '../../../core/components/DeleteConfirmation'; import { createNotificationAction, NotificationContext } from '../../../notifications/hoc/Notifications'; -const api = '/custom/entity/bundles' +const api = '/custom/entity/bundles'; export function AttributeBundleApi({ id, children }) { diff --git a/ui/src/app/metadata/hoc/attribute/AttributeBundleList.js b/ui/src/app/metadata/hoc/attribute/AttributeBundleList.js index 0cf5b54d7..447ca0d1f 100644 --- a/ui/src/app/metadata/hoc/attribute/AttributeBundleList.js +++ b/ui/src/app/metadata/hoc/attribute/AttributeBundleList.js @@ -4,6 +4,7 @@ export function AttributeBundleList({ load, children }) { const [bundles, setBundles] = React.useState([]); + /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { load((list) => setBundles(list)); }, []); diff --git a/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js b/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js index 2ec7ffe4d..7cf055b38 100644 --- a/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js +++ b/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js @@ -1,9 +1,9 @@ import React from 'react'; -import { useParams } from 'react-router-dom'; export function AttributeBundleSelector({ id, find, children }) { const [bundle, setBundle] = React.useState([]); + /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { find(id, (item) => setBundle(item)); }, []); diff --git a/ui/src/app/metadata/view/MetadataAttributeBundles.js b/ui/src/app/metadata/view/MetadataAttributeBundles.js index 056994fcf..969f25ba5 100644 --- a/ui/src/app/metadata/view/MetadataAttributeBundles.js +++ b/ui/src/app/metadata/view/MetadataAttributeBundles.js @@ -6,7 +6,6 @@ import Button from 'react-bootstrap/Button'; import { Link } from 'react-router-dom'; import { Translate } from '../../i18n/components/translate'; -import { useTranslator } from '../../i18n/hooks'; import { AttributeBundleApi } from '../hoc/attribute/AttributeBundleApi'; @@ -14,8 +13,6 @@ import { AttributeBundleList } from '../hoc/attribute/AttributeBundleList'; export function MetadataAttributeBundles({ entities, onDelete }) { - const translator = useTranslator(); - return ( {(load, find, create, update, remove, loading) => diff --git a/ui/src/app/metadata/wizard/Wizard.js b/ui/src/app/metadata/wizard/Wizard.js index 644c911a4..cc52249fe 100644 --- a/ui/src/app/metadata/wizard/Wizard.js +++ b/ui/src/app/metadata/wizard/Wizard.js @@ -6,7 +6,7 @@ const WizardContext = React.createContext(); const { Provider, Consumer } = WizardContext; const initialState = { - current: 'attribute', + current: 'common', disabled: false, loading: false }; From 67901004aa076e2003c5d6ecb7257ef083048123 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Fri, 10 Sep 2021 16:56:47 -0700 Subject: [PATCH 06/19] SHIBUI-2059 Initial add of functionality needed on the backend --- .../controller/AttributeBundleController.java | 29 +++++ .../admin/ui/domain/AttributeBundle.java | 31 ++++++ .../ui/domain/BundleableAttributeType.java | 38 +++++++ .../repository/AttributeBundleRepository.java | 15 +++ .../ui/service/AttributeBundleService.java | 18 +++ ...undleableAttributeTypeValueSerializer.java | 29 +++++ .../src/main/resources/application.properties | 1 + .../AttributeBundleControllerTests.groovy | 104 ++++++++++++++++++ .../AttributeBundleRepositoryTests.groovy | 45 ++++++++ 9 files changed, 310 insertions(+) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeBundle.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java new file mode 100644 index 000000000..ebbd057a1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java @@ -0,0 +1,29 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/custom/entity/bundles") +@Slf4j +public class AttributeBundleController { + @Autowired AttributeBundleService attributeBundleService; + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(attributeBundleService.findAll()); + } + + //POST + + //DELETE + + //PUT +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeBundle.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeBundle.java new file mode 100644 index 000000000..2df1132ac --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeBundle.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import lombok.Data; + +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.Id; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@Entity(name = "attribute_bundle_definition") +@Data +public class AttributeBundle { + @Column(nullable = false) + @ElementCollection + Set attributes = new HashSet<>(); + + @Column(name = "name", nullable = true) + String name; + + @Id + @Column(name = "resource_id", nullable = false) + String resourceId = UUID.randomUUID().toString(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java new file mode 100644 index 000000000..9977e90b1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java @@ -0,0 +1,38 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import edu.internet2.tier.shibboleth.admin.util.BundleableAttributeTypeValueSerializer; + +@JsonSerialize(using = BundleableAttributeTypeValueSerializer.class) +public enum BundleableAttributeType { + EDUPERSONPRINCIPALNAME("eduPersonPrincipalName"), + UID("uid"), + MAIL("mail"), + SURNAME("surname"), + GIVENNAME("givenName"), + EDUPERSONAFFILIATE("eduPersonAffiliation"), + EDUPERSONSCOPEDAFFILIATION("eduPersonScopedAffiliation"), + EDUPERSONPRIMARYAFFILIATION("eduPersonPrimaryAffiliation"), + EDUPERSONENTITLEMENT("eduPersonEntitlement"), + EDUPERSONASSURANCE("eduPersonAssurance"), + EDUPERSONUNIQUEID("eduPersonUniqueId"), + EMPLOYEENUMBER("employeeNumber"); + + String label; + + BundleableAttributeType(String val) { + label = val; + } + + public String label() {return label;} + + public static BundleableAttributeType valueOfLabel(String label) { + for (BundleableAttributeType e : values()) { + if (e.name().equals(label)) { + return e; + } + } + return null; + } + +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java new file mode 100644 index 000000000..c2d52bb23 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java @@ -0,0 +1,15 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +/** + * Repository to manage {@link edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle} instances. + */ +public interface AttributeBundleRepository extends JpaRepository { + List findAll(); + + AttributeBundle save(AttributeBundle target); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java new file mode 100644 index 000000000..413a14386 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java @@ -0,0 +1,18 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AttributeBundleService { + @Autowired + AttributeBundleRepository attributeBundleRepository; + + public List findAll() { + return attributeBundleRepository.findAll(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java new file mode 100644 index 000000000..32a7b7cc8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java @@ -0,0 +1,29 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import edu.internet2.tier.shibboleth.admin.ui.domain.BundleableAttributeType; + +import java.io.IOException; + +/** + * This simplifies translation to the front end. We use the ENUM on the backend, but the BundleableAttributeType + * is tagged to serialize using this helper. + * Note: The deserialize is done naturally by setting spring.jackson.mapper.accept-case-insensitive-enums=true in + * the application.properties and by the setup of the ENUM itself + */ +public class BundleableAttributeTypeValueSerializer extends StdSerializer { + public BundleableAttributeTypeValueSerializer() { + this(null); + } + + public BundleableAttributeTypeValueSerializer(Class t) { + super(t); + } + + @Override + public void serialize(BundleableAttributeType value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(value.label()); + } +} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 5b7b801f1..0556e5b45 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -24,6 +24,7 @@ spring.h2.console.settings.web-allow-others=true # spring.jackson.default-property-inclusion=non_absent spring.jackson.default-property-inclusion=NON_NULL +spring.jackson.mapper.accept-case-insensitive-enums=true # Database Configuration PostgreSQL #spring.datasource.url=jdbc:postgresql://localhost:5432/shibui diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy new file mode 100644 index 000000000..231e83601 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy @@ -0,0 +1,104 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle +import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository +import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +import spock.lang.Specification + +import static org.hamcrest.Matchers.containsInAnyOrder +import static org.springframework.http.MediaType.APPLICATION_JSON +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@DataJpaTest +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ContextConfiguration(classes = [ShibUIConfiguration, ABCTConfig]) +class AttributeBundleControllerTests extends Specification { + @Autowired + AttributeBundleController controller + + @Autowired + AttributeBundleRepository attributeBundleRepository + + ObjectMapper objectMapper = new ObjectMapper().with { + it.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + it + } + + def MockMvc + + @Transactional + def setup() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + attributeBundleRepository.deleteAll() + } + + def "GET checks" () { + expect: + attributeBundleRepository.findAll().isEmpty() + + when: "fetch for no bundles" + def result = mockMvc.perform(get('/api/custom/entity/bundles')) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(content().json('[]')) + + when: "add a bundle" + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + attributeBundleRepository.saveAndFlush(bundle) + result = mockMvc.perform(get('/api/custom/entity/bundles')) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].name").value("bundleName")) + .andExpect(jsonPath("\$.[0].resourceId").value("randomIDVal")) + .andExpect(jsonPath("\$.[0].attributes", containsInAnyOrder("eduPersonPrincipalName", "surname", "givenName"))) + } + + // can go away with merge to develop... + @TestConfiguration + private static class ABCTConfig { + @Bean + AttributeBundleController attributeBundleController(AttributeBundleService attributeBundleService) { + new AttributeBundleController().with { + it.attributeBundleService = attributeBundleService + it + } + } + + @Bean + AttributeBundleService attributeBundleService(AttributeBundleRepository repo) { + new AttributeBundleService().with { + it.attributeBundleRepository = repo + it + } + } + + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy new file mode 100644 index 000000000..148cceda8 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy @@ -0,0 +1,45 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository + +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +@DataJpaTest +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ContextConfiguration(classes = [ShibUIConfiguration]) +class AttributeBundleRepositoryTests extends Specification { + @Autowired + AttributeBundleRepository abRepo + + ObjectMapper objectMapper = new ObjectMapper().with { + it.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + it + } + + def "test create and fetch" () { + given: + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + + when: + def result = abRepo.save(bundle) + + then: + result == bundle + } +} \ No newline at end of file From de71b30815a26b53e3145264ca1659b69fea5740 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Fri, 10 Sep 2021 22:07:29 -0700 Subject: [PATCH 07/19] SHIBUI-2059 added logic for create bundles --- .../controller/AttributeBundleController.java | 16 +++++- .../AttributeBundleExceptionHandler.java | 22 ++++++++ .../EntityDescriptorController.java | 16 ++---- ...yDescriptorControllerExceptionHandler.java | 26 +++++----- .../ui/domain/BundleableAttributeType.java | 4 +- .../ui/exception/EntityIdExistsException.java | 8 --- .../ui/exception/ObjectIdExistsException.java | 8 +++ .../repository/AttributeBundleRepository.java | 3 ++ .../ui/service/AttributeBundleService.java | 8 +++ .../ui/service/EntityDescriptorService.java | 11 ++-- .../JPAEntityDescriptorServiceImpl.java | 9 ++-- .../AttributeBundleControllerTests.groovy | 50 ++++++++++++++++++- .../EntityDescriptorControllerTests.groovy | 6 +-- 13 files changed, 138 insertions(+), 49 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java delete mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ObjectIdExistsException.java diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java index ebbd057a1..ca1c18af5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java @@ -1,11 +1,19 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -21,7 +29,13 @@ public ResponseEntity getAll() { return ResponseEntity.ok(attributeBundleService.findAll()); } - //POST + @Secured("ROLE_ADMIN") + @PostMapping + @Transactional + public ResponseEntity create(@RequestBody AttributeBundle bundle) throws ObjectIdExistsException { + AttributeBundle result = attributeBundleService.create(bundle); + return ResponseEntity.status(HttpStatus.CREATED).body(result); + } //DELETE diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java new file mode 100644 index 000000000..21940d84f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java @@ -0,0 +1,22 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +@ControllerAdvice(assignableTypes = {AttributeBundleController.class}) +public class AttributeBundleExceptionHandler { + @ExceptionHandler({ ObjectIdExistsException.class }) + public ResponseEntity handleObjectIdExistsException(ObjectIdExistsException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(EntityDescriptorController.getResourceUriFor(e.getMessage())); + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers).body(new ErrorResponse( + String.valueOf(HttpStatus.CONFLICT.value()), + String.format("The attribute bundle with resource id [%s] already exists.", e.getMessage()))); + + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java index f8e28fc75..99eb079f6 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java @@ -2,14 +2,10 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.versioning.Version; -import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; -import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.model.User; -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService; import lombok.extern.slf4j.Slf4j; @@ -17,13 +13,10 @@ import org.opensaml.core.xml.io.MarshallingException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -39,8 +32,6 @@ import java.net.URI; import java.util.ConcurrentModificationException; -import java.util.List; -import java.util.stream.Collectors; @RestController @RequestMapping("/api") @@ -73,7 +64,8 @@ public EntityDescriptorController(EntityDescriptorVersionService versionService) @PostMapping("/EntityDescriptor") @Transactional - public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException { + public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, + ObjectIdExistsException { EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(edRepresentation); return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd); } @@ -171,4 +163,4 @@ public ResponseEntity upload(@RequestParam String metadataUrl, @RequestParam .body(String.format("Error fetching XML metadata from the provided URL. Error: %s", e.getMessage())); } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java index d0b18f415..fd48f68e6 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java @@ -10,7 +10,7 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; @@ -22,23 +22,23 @@ public ResponseEntity handleConcurrentModificationException(ConcurrentModific return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage())); } - @ExceptionHandler({ EntityIdExistsException.class }) - public ResponseEntity handleEntityExistsException(EntityIdExistsException e, WebRequest request) { - HttpHeaders headers = new HttpHeaders(); - headers.setLocation(EntityDescriptorController.getResourceUriFor(e.getMessage())); - return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers).body(new ErrorResponse( - String.valueOf(HttpStatus.CONFLICT.value()), - String.format("The entity descriptor with entity id [%s] already exists.", e.getMessage()))); - - } - @ExceptionHandler({ EntityNotFoundException.class }) public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); } - + @ExceptionHandler({ ForbiddenException.class }) public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest request) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, e.getMessage())); } -} + + @ExceptionHandler({ ObjectIdExistsException.class }) + public ResponseEntity handleObjectIdExistsException(ObjectIdExistsException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(EntityDescriptorController.getResourceUriFor(e.getMessage())); + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers).body(new ErrorResponse( + String.valueOf(HttpStatus.CONFLICT.value()), + String.format("The entity descriptor with entity id [%s] already exists.", e.getMessage()))); + + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java index 9977e90b1..84a80fce4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import edu.internet2.tier.shibboleth.admin.util.BundleableAttributeTypeValueSerializer; @@ -26,9 +27,10 @@ public enum BundleableAttributeType { public String label() {return label;} + @JsonCreator public static BundleableAttributeType valueOfLabel(String label) { for (BundleableAttributeType e : values()) { - if (e.name().equals(label)) { + if (e.label.equals(label)) { return e; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java deleted file mode 100644 index 990eab2c3..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java +++ /dev/null @@ -1,8 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.exception; - -public class EntityIdExistsException extends Exception { - public EntityIdExistsException(String entityId) { - super(entityId); - } - -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ObjectIdExistsException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ObjectIdExistsException.java new file mode 100644 index 000000000..f2604acfe --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ObjectIdExistsException.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class ObjectIdExistsException extends Exception { + public ObjectIdExistsException(String entityId) { + super(entityId); + } + +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java index c2d52bb23..0c9bc2aed 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; /** * Repository to manage {@link edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle} instances. @@ -11,5 +12,7 @@ public interface AttributeBundleRepository extends JpaRepository { List findAll(); + Optional findByResourceId(String resourceId); + AttributeBundle save(AttributeBundle target); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java index 413a14386..f77983e92 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service; import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -12,6 +13,13 @@ public class AttributeBundleService { @Autowired AttributeBundleRepository attributeBundleRepository; + public AttributeBundle create(AttributeBundle bundle) throws ObjectIdExistsException { + if (attributeBundleRepository.findByResourceId(bundle.getResourceId()).isPresent()) { + throw new ObjectIdExistsException(bundle.getResourceId()); + } + return attributeBundleRepository.save(bundle); + } + public List findAll() { return attributeBundleRepository.findAll(); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java index c8c39bbb3..50f29951c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java @@ -3,7 +3,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; @@ -29,17 +29,18 @@ public interface EntityDescriptorService { * @param ed - JPA EntityDescriptor to base creation on * @return EntityDescriptorRepresentation of the created object * @throws ForbiddenException If user is unauthorized to perform this operation - * @throws EntityIdExistsException If any EntityDescriptor already exists with the same EntityId + * @throws ObjectIdExistsException If any EntityDescriptor already exists with the same EntityId */ - EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException; + EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, ObjectIdExistsException; /** * @param edRepresentation Incoming representation to save * @return EntityDescriptorRepresentation * @throws ForbiddenException If user is unauthorized to perform this operation - * @throws EntityIdExistsException If the entity already exists + * @throws ObjectIdExistsException If the entity already exists */ - EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException; + EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, + ObjectIdExistsException; /** * Map from opensaml implementation of entity descriptor model to front-end data representation of entity descriptor diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index d23b16365..1b660ab9f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -2,7 +2,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.*; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.*; -import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; @@ -66,18 +66,19 @@ public EntityDescriptor createDescriptorFromRepresentation(final EntityDescripto } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException { + public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, ObjectIdExistsException { return createNew(createRepresentationFromDescriptor(ed)); } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityIdExistsException { + public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, + ObjectIdExistsException { if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } if (entityDescriptorRepository.findByEntityID(edRep.getEntityId()) != null) { - throw new EntityIdExistsException(edRep.getEntityId()); + throw new ObjectIdExistsException(edRep.getEntityId()); } EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy index 231e83601..af0b428e7 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.MapperFeature import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService import org.springframework.beans.factory.annotation.Autowired @@ -15,16 +16,20 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.transaction.annotation.Transactional +import org.springframework.web.util.NestedServletException import spock.lang.Specification +import static org.hamcrest.CoreMatchers.containsString import static org.hamcrest.Matchers.containsInAnyOrder import static org.springframework.http.MediaType.APPLICATION_JSON import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -@DataJpaTest +@DataJpaTest(properties = ["spring.jackson.mapper.accept-case-insensitive-enums=true"]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @ContextConfiguration(classes = [ShibUIConfiguration, ABCTConfig]) @@ -81,7 +86,48 @@ class AttributeBundleControllerTests extends Specification { .andExpect(jsonPath("\$.[0].attributes", containsInAnyOrder("eduPersonPrincipalName", "surname", "givenName"))) } - // can go away with merge to develop... + def "CREATE checks" () { + expect: + attributeBundleRepository.findAll().isEmpty() + + when: "add a bundle" + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + attributeBundleRepository.saveAndFlush(bundle) + + then: "bundle already exists" + try { + mockMvc.perform(post('/api/custom/entity/bundles').contentType(APPLICATION_JSON).content(json)) + false + } catch (NestedServletException expected) { + expected.getCause() instanceof ObjectIdExistsException + } + + when: "new bundle" + json = """ + { + "name": "bundle2", + "resourceId": "differentResourceId", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + + def result = mockMvc.perform(post('/api/custom/entity/bundles').contentType(APPLICATION_JSON).content(json)) + then: + result.andExpect(status().isCreated()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value("bundle2")) + .andExpect(jsonPath("\$.resourceId").value("differentResourceId")) + .andExpect(jsonPath("\$.attributes", containsInAnyOrder("eduPersonPrincipalName", "surname", "givenName"))) + } + + // can go away with merge to develop and this extends the base test class @TestConfiguration private static class ABCTConfig { @Bean diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index f7b44786a..74ca259a0 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -6,7 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.Internationalization import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor -import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects @@ -372,7 +372,7 @@ class EntityDescriptorControllerTests extends Specification { mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) } catch (Exception e) { - e instanceof EntityIdExistsException + e instanceof ObjectIdExistsException } } @@ -618,7 +618,7 @@ class EntityDescriptorControllerTests extends Specification { mockMvc.perform(post("/api/EntityDescriptor").contentType(APPLICATION_XML).content(postedBody).param("spName", spName)) } catch (Exception e) { - e instanceof EntityIdExistsException + e instanceof ObjectIdExistsException } } From 9e36ffbbaffaa48cdab88e1cb7bddd8a7fabdfc1 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Fri, 10 Sep 2021 22:36:29 -0700 Subject: [PATCH 08/19] SHIBUI-2059 added logic for delete bundles --- .../controller/AttributeBundleController.java | 12 ++++++- .../ui/service/AttributeBundleService.java | 8 +++++ ...undleableAttributeTypeValueSerializer.java | 3 +- .../AttributeBundleControllerTests.groovy | 36 +++++++++++++++++++ .../AttributeBundleRepositoryTests.groovy | 4 +-- 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java index ca1c18af5..e438b77ae 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java @@ -1,7 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService; @@ -11,7 +13,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -37,7 +41,13 @@ public ResponseEntity create(@RequestBody AttributeBundle bundle) throws Obje return ResponseEntity.status(HttpStatus.CREATED).body(result); } - //DELETE + @Secured("ROLE_ADMIN") + @DeleteMapping("/{resourceId}") + @Transactional + public ResponseEntity delete(@PathVariable String resourceId) throws EntityNotFoundException { + attributeBundleService.deleteDefinition(resourceId); + return ResponseEntity.noContent().build(); + } //PUT } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java index f77983e92..15611a8b1 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service; import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -23,4 +24,11 @@ public AttributeBundle create(AttributeBundle bundle) throws ObjectIdExistsExcep public List findAll() { return attributeBundleRepository.findAll(); } + + public void deleteDefinition(String resourceId) throws EntityNotFoundException { + if (attributeBundleRepository.findByResourceId(resourceId).isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find attribute bundle with resource id: [%s] for deletion", resourceId)); + } + attributeBundleRepository.deleteById(resourceId); + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java index 32a7b7cc8..55aa1ab44 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java @@ -10,8 +10,7 @@ /** * This simplifies translation to the front end. We use the ENUM on the backend, but the BundleableAttributeType * is tagged to serialize using this helper. - * Note: The deserialize is done naturally by setting spring.jackson.mapper.accept-case-insensitive-enums=true in - * the application.properties and by the setup of the ENUM itself + * Note: The deserialize is done by the setup of the ENUM itself */ public class BundleableAttributeTypeValueSerializer extends StdSerializer { public BundleableAttributeTypeValueSerializer() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy index af0b428e7..a9180071f 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.MapperFeature import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService @@ -22,6 +23,7 @@ import spock.lang.Specification import static org.hamcrest.CoreMatchers.containsString import static org.hamcrest.Matchers.containsInAnyOrder import static org.springframework.http.MediaType.APPLICATION_JSON +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content @@ -127,6 +129,40 @@ class AttributeBundleControllerTests extends Specification { .andExpect(jsonPath("\$.attributes", containsInAnyOrder("eduPersonPrincipalName", "surname", "givenName"))) } + def "test delete" () { + expect: + attributeBundleRepository.findAll().isEmpty() + + when: + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + attributeBundleRepository.save(bundle) + + then: + attributeBundleRepository.findAll().size() == 1 + + // Delete something doesn't exist + try { + mockMvc.perform(delete("/api/custom/entity/bundles/randomIDValdoesntexist")) + false + } catch (NestedServletException expected) { + expected instanceof EntityNotFoundException + } + + when: "Delete what does exist" + def result = mockMvc.perform(delete("/api/custom/entity/bundles/randomIDVal")) + + then: + result.andExpect(status().isNoContent()) + attributeBundleRepository.findAll().isEmpty() + } + // can go away with merge to develop and this extends the base test class @TestConfiguration private static class ABCTConfig { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy index 148cceda8..e1c23c70a 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy @@ -17,7 +17,7 @@ import spock.lang.Specification @ContextConfiguration(classes = [ShibUIConfiguration]) class AttributeBundleRepositoryTests extends Specification { @Autowired - AttributeBundleRepository abRepo + AttributeBundleRepository attributeBundleRepository ObjectMapper objectMapper = new ObjectMapper().with { it.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) @@ -37,7 +37,7 @@ class AttributeBundleRepositoryTests extends Specification { AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) when: - def result = abRepo.save(bundle) + def result = attributeBundleRepository.save(bundle) then: result == bundle From 81c34056c9d0d08e0209a28da0a49480a51b2941 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Fri, 10 Sep 2021 23:04:07 -0700 Subject: [PATCH 09/19] SHIBUI-2059 added logic for update bundles --- .../controller/AttributeBundleController.java | 21 +++++--- .../AttributeBundleExceptionHandler.java | 9 +++- .../ui/service/AttributeBundleService.java | 12 +++++ .../AttributeBundleControllerTests.groovy | 51 ++++++++++++++++--- .../AttributeBundleRepositoryTests.groovy | 5 +- 5 files changed, 80 insertions(+), 18 deletions(-) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java index e438b77ae..a9ac9160a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java @@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -27,12 +28,6 @@ public class AttributeBundleController { @Autowired AttributeBundleService attributeBundleService; - @GetMapping - @Transactional(readOnly = true) - public ResponseEntity getAll() { - return ResponseEntity.ok(attributeBundleService.findAll()); - } - @Secured("ROLE_ADMIN") @PostMapping @Transactional @@ -49,5 +44,17 @@ public ResponseEntity delete(@PathVariable String resourceId) throws EntityNo return ResponseEntity.noContent().build(); } - //PUT + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(attributeBundleService.findAll()); + } + + @Secured("ROLE_ADMIN") + @PutMapping + @Transactional + public ResponseEntity update(@RequestBody AttributeBundle bundle) throws EntityNotFoundException { + AttributeBundle result = attributeBundleService.updateBundle(bundle); + return ResponseEntity.ok(result); + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java index 21940d84f..9f5266c3c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -7,9 +8,15 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @ControllerAdvice(assignableTypes = {AttributeBundleController.class}) -public class AttributeBundleExceptionHandler { +public class AttributeBundleExceptionHandler extends ResponseEntityExceptionHandler { + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); + } + @ExceptionHandler({ ObjectIdExistsException.class }) public ResponseEntity handleObjectIdExistsException(ObjectIdExistsException e, WebRequest request) { HttpHeaders headers = new HttpHeaders(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java index 15611a8b1..f246c7d1b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Optional; @Service public class AttributeBundleService { @@ -31,4 +32,15 @@ public void deleteDefinition(String resourceId) throws EntityNotFoundException { } attributeBundleRepository.deleteById(resourceId); } + + public AttributeBundle updateBundle(AttributeBundle bundle) throws EntityNotFoundException { + Optional dbBundle = attributeBundleRepository.findByResourceId(bundle.getResourceId()); + if (dbBundle.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find attribute bundle with resource id: [%s] for update", bundle.getResourceId())); + } + AttributeBundle bundleToUpdate = dbBundle.get(); + bundleToUpdate.setName(bundle.getName()); + bundleToUpdate.setAttributes(bundle.getAttributes()); + return attributeBundleRepository.save(bundleToUpdate); + } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy index a9180071f..00e624b7e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy @@ -20,14 +20,13 @@ import org.springframework.transaction.annotation.Transactional import org.springframework.web.util.NestedServletException import spock.lang.Specification -import static org.hamcrest.CoreMatchers.containsString import static org.hamcrest.Matchers.containsInAnyOrder import static org.springframework.http.MediaType.APPLICATION_JSON import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @@ -42,10 +41,7 @@ class AttributeBundleControllerTests extends Specification { @Autowired AttributeBundleRepository attributeBundleRepository - ObjectMapper objectMapper = new ObjectMapper().with { - it.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) - it - } + ObjectMapper objectMapper = new ObjectMapper() def MockMvc @@ -163,6 +159,49 @@ class AttributeBundleControllerTests extends Specification { attributeBundleRepository.findAll().isEmpty() } + def "Update checks" () { + expect: + attributeBundleRepository.findAll().isEmpty() + + when: "add a bundle" + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + attributeBundleRepository.saveAndFlush(bundle) + + then: "bundle doesn't exist" + bundle.setResourceId("foo") + try { + mockMvc.perform(put('/api/custom/entity/bundles').contentType(APPLICATION_JSON).content(objectMapper.writeValueAsString(bundle))) + false + } catch (NestedServletException expected) { + expected.getCause() instanceof EntityNotFoundException + } + + when: "update bundle" + json = """ + { + "name": "bundle2", + "resourceId": "randomIDVal", + "attributes": ["eduPersonUniqueId", "employeeNumber", "givenName"] + } + """ + + def result = mockMvc.perform(put('/api/custom/entity/bundles').contentType(APPLICATION_JSON).content(json)) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value("bundle2")) + .andExpect(jsonPath("\$.resourceId").value("randomIDVal")) + .andExpect(jsonPath("\$.attributes", containsInAnyOrder("eduPersonUniqueId", "employeeNumber", "givenName"))) + } + // can go away with merge to develop and this extends the base test class @TestConfiguration private static class ABCTConfig { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy index e1c23c70a..0db6a9555 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy @@ -19,10 +19,7 @@ class AttributeBundleRepositoryTests extends Specification { @Autowired AttributeBundleRepository attributeBundleRepository - ObjectMapper objectMapper = new ObjectMapper().with { - it.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) - it - } + ObjectMapper objectMapper = new ObjectMapper() def "test create and fetch" () { given: From 3c3890e8f5a4cd7ec7b3e601949d785dd6ff4cb3 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Mon, 13 Sep 2021 14:20:34 -0700 Subject: [PATCH 10/19] SHIBUI-2063 intermediate commit --- .../admin/ui/controller/AttributeBundleController.java | 6 ++++++ .../admin/ui/service/AttributeBundleService.java | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java index a9ac9160a..1dbe5e026 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java @@ -50,6 +50,12 @@ public ResponseEntity getAll() { return ResponseEntity.ok(attributeBundleService.findAll()); } + @GetMapping("/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNotFoundException { + return ResponseEntity.ok(attributeBundleService.findByResourceId(resourceId)); + } + @Secured("ROLE_ADMIN") @PutMapping @Transactional diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java index f246c7d1b..916ea99b2 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java @@ -43,4 +43,12 @@ public AttributeBundle updateBundle(AttributeBundle bundle) throws EntityNotFoun bundleToUpdate.setAttributes(bundle.getAttributes()); return attributeBundleRepository.save(bundleToUpdate); } + + public AttributeBundle findByResourceId(String resourceId) throws EntityNotFoundException { + Optional result = attributeBundleRepository.findByResourceId(resourceId); + if (result.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find attribute bundle with resource id: [%s] for deletion", resourceId)); + } + return result.get(); + } } \ No newline at end of file From b70729a012335c2f2e1a8ca129c2a2b56be82961 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Mon, 13 Sep 2021 14:36:04 -0700 Subject: [PATCH 11/19] updated bundles --- ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js | 4 +--- ui/src/app/metadata/new/NewBundle.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js b/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js index 5abc3d103..fd8651840 100644 --- a/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js +++ b/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js @@ -5,13 +5,11 @@ import API_BASE_PATH from '../../../App.constant'; import { DeleteConfirmation } from '../../../core/components/DeleteConfirmation'; import { createNotificationAction, NotificationContext } from '../../../notifications/hoc/Notifications'; -const api = '/custom/entity/bundles'; - export function AttributeBundleApi({ id, children }) { const { dispatch } = React.useContext(NotificationContext); - const { get, put, post, del, response, loading } = useFetch(`/data/bundles.json`, { + const { get, put, post, del, response, loading } = useFetch(`${API_BASE_PATH}/custom/entity/bundles`, { cachePolicy: 'no-cache' }); diff --git a/ui/src/app/metadata/new/NewBundle.js b/ui/src/app/metadata/new/NewBundle.js index 4302b4ada..34bebd2e7 100644 --- a/ui/src/app/metadata/new/NewBundle.js +++ b/ui/src/app/metadata/new/NewBundle.js @@ -15,8 +15,6 @@ import { AttributeBundleApi } from '../hoc/attribute/AttributeBundleApi'; export function NewBundle() { const history = useHistory(); - console.log('hi') - const definition = AttributeBundleDefinition; const [blocking, setBlocking] = React.useState(false); @@ -56,7 +54,7 @@ export function NewBundle() { + + ); +} + +export default TruncateText; \ No newline at end of file diff --git a/ui/src/app/metadata/domain/source/definition/SourceDefinition.js b/ui/src/app/metadata/domain/source/definition/SourceDefinition.js index c650a1f95..676d97aef 100644 --- a/ui/src/app/metadata/domain/source/definition/SourceDefinition.js +++ b/ui/src/app/metadata/domain/source/definition/SourceDefinition.js @@ -10,8 +10,8 @@ export const SourceBase = { label: 'Metadata Source', type: '@MetadataProvider', steps: [], - //schema: `${API_BASE_PATH}/ui/MetadataSources`, - schema: `/assets/schema/source/metadata-source.json`, + schema: `${API_BASE_PATH}/ui/MetadataSources`, + //schema: `/assets/schema/source/metadata-source.json`, parser: (data) => removeNull(data, true), diff --git a/ui/src/app/metadata/hoc/MetadataFormContext.js b/ui/src/app/metadata/hoc/MetadataFormContext.js index 12fd598e8..2fe2d1dff 100644 --- a/ui/src/app/metadata/hoc/MetadataFormContext.js +++ b/ui/src/app/metadata/hoc/MetadataFormContext.js @@ -64,7 +64,6 @@ function MetadataForm({ children, initial = {} }) { metadata }); - const contextValue = React.useMemo(() => ({ state, dispatch }), [state, dispatch]); return ( @@ -113,6 +112,7 @@ function useFormattedMetadata(initial = {}) { const definition = React.useContext(MetadataDefinitionContext); const schema = React.useContext(MetadataSchemaContext); const obj = React.useContext(MetadataObjectContext); + return definition.formatter(initial ? initial : obj, schema); } diff --git a/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js b/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js index fd8651840..37e0dd1bf 100644 --- a/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js +++ b/ui/src/app/metadata/hoc/attribute/AttributeBundleApi.js @@ -28,7 +28,7 @@ export function AttributeBundleApi({ id, children }) { } async function update(id, body, cb) { - const b = await put(`/${id}`, body); + const b = await put(``, body); if (response.ok) { dispatch(createNotificationAction( `Bundle has been updated.` diff --git a/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js b/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js index 7cf055b38..726ef1e1e 100644 --- a/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js +++ b/ui/src/app/metadata/hoc/attribute/AttributeBundleSelector.js @@ -1,7 +1,7 @@ import React from 'react'; export function AttributeBundleSelector({ id, find, children }) { - const [bundle, setBundle] = React.useState([]); + const [bundle, setBundle] = React.useState(); /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { diff --git a/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js b/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js index 914cb4347..87eeb9db0 100644 --- a/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js +++ b/ui/src/app/metadata/view/MetadataAttributeBundleEdit.js @@ -54,11 +54,11 @@ export function MetadataAttributeBundleEdit() { {bundle && - {(filter, errors) => + {(data, errors) => + {bundles.map((bundle, i) => - + +
      {bundled ? - {option.label} - : - {option.label} - }
      {option.label}
      - {bundled ? - - :
      - }
      Bundle Name Bundled Attributes Actions
      {bundle.name}{ bundle.name } From 762fe329d28141e978dd4b60737f7a310871fc19 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 16 Sep 2021 14:08:27 -0700 Subject: [PATCH 13/19] SHIBUI-2059 Missing json for bundles with attribute release --- .../service/JsonSchemaBuilderService.groovy | 26 ++++++++++++++----- .../JsonSchemaComponentsConfiguration.java | 4 +-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy index 6ebdcf7be..a45448016 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy @@ -1,29 +1,41 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty import edu.internet2.tier.shibboleth.admin.ui.security.model.User import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import lombok.NoArgsConstructor import org.springframework.beans.factory.annotation.Autowired /** * @author Bill Smith (wsmith@unicon.net) */ +@NoArgsConstructor class JsonSchemaBuilderService { + @Autowired + AttributeBundleService attributeBundleService @Autowired CustomPropertiesConfiguration customPropertiesConfiguration + @Autowired UserService userService - JsonSchemaBuilderService(UserService userService) { - this.userService = userService - } - void addReleaseAttributesToJson(Object json) { - json['enum'] = customPropertiesConfiguration.getAttributes().collect { + List result = new ArrayList<>() + List resultNames = new ArrayList<>() + attributeBundleService.findAll().forEach({ bundle -> + result.add(bundle.getAttributes()) + resultNames.add(bundle.getName()) + }) + + result.addAll(customPropertiesConfiguration.getAttributes().collect { it['name'] - } + }) + + json['enum'] = result + json['enumNames'] = resultNames } void addRelyingPartyOverridesToJson(Object json) { @@ -79,4 +91,4 @@ class JsonSchemaBuilderService { serviceEnabled.remove('description') } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java index 97d88f5a7..d483319f3 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java @@ -100,6 +100,6 @@ public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(Res @Bean public JsonSchemaBuilderService jsonSchemaBuilderService(UserService userService) { - return new JsonSchemaBuilderService(userService); + return new JsonSchemaBuilderService(); } -} +} \ No newline at end of file From b26c7ff276323e6bbc5941455a25e9111b8ff292 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 16 Sep 2021 15:04:22 -0700 Subject: [PATCH 14/19] Added labels --- backend/src/main/resources/i18n/messages.properties | 4 +++- ui/src/app/form/component/widgets/AttributeReleaseWidget.js | 2 +- ui/src/app/metadata/view/MetadataAttributeBundleEdit.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 406c587a1..e4ba3d876 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -494,11 +494,13 @@ label.source=Metadata Source label.provider=Metadata Provider label.bundle-name=Bundle name +label.bundle-disp=Bundle - {name} action.add-new-bundle=Add bundle tooltip.bundle-name=A user friendly name to identify the bundle action.attribute-bundles=Attribute bundles label.new-attribute-bundle=New attribute bundle - +label.edit-attribute-bundle=Edit attribute bundle +label.bundled-attributes=Bundles Attributes message.user-role-admin-group=Cannot change group for ROLE_ADMIN users. label.roles-management=Role Management diff --git a/ui/src/app/form/component/widgets/AttributeReleaseWidget.js b/ui/src/app/form/component/widgets/AttributeReleaseWidget.js index 1471f0485..b1e0553a0 100644 --- a/ui/src/app/form/component/widgets/AttributeReleaseWidget.js +++ b/ui/src/app/form/component/widgets/AttributeReleaseWidget.js @@ -102,7 +102,7 @@ const AttributeReleaseWidget = ({
        {(bundlelist).map((option) => (
      • - {option.label} +