From 34a7c34bb939fa1fd3e37162dc93e43caae01f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 18 Jan 2019 10:49:35 +0100 Subject: [PATCH] Add interface to modify permissions for a user --- scm-ui/public/locales/en/permissions.json | 22 ++ scm-ui/public/locales/en/users.json | 6 + .../users/components/PermissionCheckbox.js | 32 +++ .../users/components/SetUserPermissions.js | 196 ++++++++++++++++++ .../navLinks/SetPermissionsNavLink.js | 28 +++ .../navLinks/SetPermissionsNavLink.test.js | 31 +++ scm-ui/src/users/components/navLinks/index.js | 1 + scm-ui/src/users/components/setPermissions.js | 13 ++ scm-ui/src/users/containers/SingleUser.js | 12 +- .../resources/META-INF/scm/permissions.xml | 4 +- 10 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 scm-ui/public/locales/en/permissions.json create mode 100644 scm-ui/src/users/components/PermissionCheckbox.js create mode 100644 scm-ui/src/users/components/SetUserPermissions.js create mode 100644 scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js create mode 100644 scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js create mode 100644 scm-ui/src/users/components/setPermissions.js diff --git a/scm-ui/public/locales/en/permissions.json b/scm-ui/public/locales/en/permissions.json new file mode 100644 index 0000000000..71c2bcc9e4 --- /dev/null +++ b/scm-ui/public/locales/en/permissions.json @@ -0,0 +1,22 @@ +{ + "repository": { + "read": { + "*": { + "displayName": "Read all repositories", + "description": "Read access to all repositories" + } + }, + "write": { + "*": { + "displayName": "Modify all repositories", + "description": "May modify/configure all repositories" + } + } + }, + "user":{ + "*": { + "displayName": "Administer users", + "description": "May administer all users" + } + } +} diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index 2a9ee7b79d..aa7ed4e3fc 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -32,6 +32,9 @@ "set-password-button": { "label": "Set password" }, + "set-permissions-button": { + "label": "Set permissions" + }, "user-form": { "submit": "Submit" }, @@ -55,6 +58,9 @@ "password": { "set-password-successful": "Password successfully set" }, + "permissions": { + "set-permissions-successful": "Permissions successfully set" + }, "help": { "usernameHelpText": "Unique name of the user.", "displayNameHelpText": "Display name of the user.", diff --git a/scm-ui/src/users/components/PermissionCheckbox.js b/scm-ui/src/users/components/PermissionCheckbox.js new file mode 100644 index 0000000000..d45128134d --- /dev/null +++ b/scm-ui/src/users/components/PermissionCheckbox.js @@ -0,0 +1,32 @@ +// @flow + +import React from "react"; +import { translate } from "react-i18next"; +import { Checkbox } from "@scm-manager/ui-components"; + +type Props = { + permission: string, + checked: boolean, + onChange: (value: boolean, name: string) => void, + disabled: boolean, + t: string => string +}; + +class PermissionCheckbox extends React.Component { + render() { + const { t, permission, checked, onChange, disabled } = this.props; + const key = permission.split(":").join("."); + return ( + + ); + } +} + +export default translate("permissions")(PermissionCheckbox); diff --git a/scm-ui/src/users/components/SetUserPermissions.js b/scm-ui/src/users/components/SetUserPermissions.js new file mode 100644 index 0000000000..204381e9e4 --- /dev/null +++ b/scm-ui/src/users/components/SetUserPermissions.js @@ -0,0 +1,196 @@ +// @flow +import React from "react"; +import type { User } from "@scm-manager/ui-types"; +import { + Notification, + ErrorNotification, + SubmitButton +} from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; +import { setPermissions } from "./setPermissions"; +import { apiClient } from "@scm-manager/ui-components"; +import PermissionCheckbox from "./PermissionCheckbox"; +import { connect } from "react-redux"; +import { getLink } from "../../modules/indexResource"; + +type Props = { + user: User, + t: string => string, + permissionLink: string +}; + +type State = { + permissions: { [string]: boolean }, + loading: boolean, + error?: Error, + permissionsChanged: boolean, + permissionsSubmitted: boolean, + modifiable: boolean +}; + +class SetUserPermissions extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + permissions: { perm1: false, perm2: false }, + loading: true, + permissionsChanged: false, + permissionsSubmitted: false, + modifiable: false + }; + } + + setLoadingState = () => { + this.setState({ + ...this.state, + loading: true + }); + }; + + setErrorState = (error: Error) => { + this.setState({ + ...this.state, + error: error, + loading: false + }); + }; + + setSuccessfulState = () => { + this.setState({ + ...this.state, + loading: false, + permissionsSubmitted: true, + permissionsChanged: false + }); + }; + + componentDidMount(): void { + apiClient + .get(this.props.permissionLink) + .then(response => { + return response.json(); + }) + .then(response => { + const availablePermissions = response.permissions; + const permissions = {}; + availablePermissions.forEach(p => { + permissions[p] = false; + }); + this.setState({ permissions }, this.loadPermissionsForUser); + }); + } + + loadPermissionsForUser = () => { + apiClient + .get(this.props.user._links.permissions.href) + .then(response => { + return response.json(); + }) + .then(response => { + const checkedPermissions = response.permissions; + const modifiable = !!response._links.overwrite; + this.setState(state => { + const newPermissions = state.permissions; + checkedPermissions.forEach(name => (newPermissions[name] = true)); + return { + loading: false, + modifiable: modifiable, + permissions: newPermissions + }; + }); + }); + }; + + submit = (event: Event) => { + event.preventDefault(); + if (this.state.permissions) { + const { user } = this.props; + const { permissions } = this.state; + this.setLoadingState(); + const selectedPermissions = Object.entries(permissions) + .filter(e => e[1]) + .map(e => e[0]); + setPermissions(user._links.permissions.href, selectedPermissions) + .then(result => { + if (result.error) { + this.setErrorState(result.error); + } else { + this.setSuccessfulState(); + } + }) + .catch(err => {}); + } + }; + + render() { + const { t } = this.props; + const { loading, permissionsSubmitted, error } = this.state; + + let message = null; + + if (permissionsSubmitted) { + message = ( + this.onClose()} + /> + ); + } else if (error) { + message = ; + } + + return ( +
+ {message} + {this.renderPermissions()} + + + ); + } + + renderPermissions = () => { + const { modifiable, permissions } = this.state; + return Object.keys(permissions).map(p => ( +
+ +
+ )); + }; + + valueChanged = (value: boolean, name: string) => { + this.setState(state => { + const newPermissions = state.permissions; + newPermissions[name] = value; + return { + permissions: newPermissions, + permissionsChanged: true + }; + }); + }; + + onClose = () => { + this.setState({ + permissionsSubmitted: false + }); + }; +} + +const mapStateToProps = state => { + const permissionLink = getLink(state, "permissions"); + return { + permissionLink + }; +}; + +export default connect(mapStateToProps)(translate("users")(SetUserPermissions)); diff --git a/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js new file mode 100644 index 0000000000..cfdb1775a3 --- /dev/null +++ b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js @@ -0,0 +1,28 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { User } from "@scm-manager/ui-types"; +import { NavLink } from "@scm-manager/ui-components"; + +type Props = { + t: string => string, + user: User, + permissionsUrl: String +}; + +class ChangePermissionNavLink extends React.Component { + render() { + const { t, permissionsUrl } = this.props; + + if (!this.hasPermissionToSetPermission()) { + return null; + } + return ; + } + + hasPermissionToSetPermission = () => { + return this.props.user._links.permissions; + }; +} + +export default translate("users")(ChangePermissionNavLink); diff --git a/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js new file mode 100644 index 0000000000..0e5fcf4125 --- /dev/null +++ b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js @@ -0,0 +1,31 @@ +import React from "react"; +import { shallow } from "enzyme"; +import "../../../tests/enzyme"; +import "../../../tests/i18n"; +import SetPermissionsNavLink from "./SetPermissionsNavLink"; + +it("should render nothing, if the permissions link is missing", () => { + const user = { + _links: {} + }; + + const navLink = shallow( + + ); + expect(navLink.text()).toBe(""); +}); + +it("should render the navLink", () => { + const user = { + _links: { + permissions: { + href: "/permissions" + } + } + }; + + const navLink = shallow( + + ); + expect(navLink.text()).not.toBe(""); +}); diff --git a/scm-ui/src/users/components/navLinks/index.js b/scm-ui/src/users/components/navLinks/index.js index a6d8370c00..eb39bb6726 100644 --- a/scm-ui/src/users/components/navLinks/index.js +++ b/scm-ui/src/users/components/navLinks/index.js @@ -1,3 +1,4 @@ export { default as DeleteUserNavLink } from "./DeleteUserNavLink"; export { default as EditUserNavLink } from "./EditUserNavLink"; export { default as SetPasswordNavLink } from "./SetPasswordNavLink"; +export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink"; diff --git a/scm-ui/src/users/components/setPermissions.js b/scm-ui/src/users/components/setPermissions.js new file mode 100644 index 0000000000..4c54036fab --- /dev/null +++ b/scm-ui/src/users/components/setPermissions.js @@ -0,0 +1,13 @@ +//@flow +import { apiClient } from "@scm-manager/ui-components"; + +export const CONTENT_TYPE_PERMISSIONS = + "application/vnd.scmm-permissionCollection+json;v=2"; + +export function setPermissions(url: string, permissions: string[]) { + return apiClient + .put(url, { permissions: permissions }, CONTENT_TYPE_PERMISSIONS) + .then(response => { + return response; + }); +} diff --git a/scm-ui/src/users/containers/SingleUser.js b/scm-ui/src/users/containers/SingleUser.js index 5f20598962..033c0aa0a2 100644 --- a/scm-ui/src/users/containers/SingleUser.js +++ b/scm-ui/src/users/containers/SingleUser.js @@ -27,11 +27,13 @@ import { import { DeleteUserNavLink, EditUserNavLink, - SetPasswordNavLink + SetPasswordNavLink, + SetPermissionsNavLink } from "./../components/navLinks"; import { translate } from "react-i18next"; import { getUsersLink } from "../../modules/indexResource"; import SetUserPassword from "../components/SetUserPassword"; +import SetUserPermissions from "../components/SetUserPermissions"; type Props = { name: string, @@ -106,6 +108,10 @@ class SingleUser extends React.Component { path={`${url}/password`} component={() => } /> + } + />
@@ -119,6 +125,10 @@ class SingleUser extends React.Component { user={user} passwordUrl={`${url}/password`} /> +
diff --git a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml index 8978b050ca..55c8cc41a6 100644 --- a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml @@ -38,11 +38,11 @@ - repository:*:WRITE + repository:write:* - repository:*:OWNER + user:*