From 65ddd751cb39be60a169f8b687782ebe678207f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Thu, 9 Aug 2018 08:15:10 +0200 Subject: [PATCH 01/73] created branch From b03e058c6a004718c3016c737303e01a2665a713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Thu, 9 Aug 2018 09:38:55 +0200 Subject: [PATCH 02/73] add navigation for config --- scm-ui/public/locales/en/commons.json | 3 ++- scm-ui/src/components/navigation/PrimaryNavigation.js | 4 ++++ scm-ui/src/config/containers/Config.js | 9 +++++++++ scm-ui/src/containers/Main.js | 8 ++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 scm-ui/src/config/containers/Config.js diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index 1feaa80a9b..b1bad56993 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -32,7 +32,8 @@ "repositories": "Repositories", "users": "Users", "logout": "Logout", - "groups": "Groups" + "groups": "Groups", + "config": "Configuration" }, "paginator": { "next": "Next", diff --git a/scm-ui/src/components/navigation/PrimaryNavigation.js b/scm-ui/src/components/navigation/PrimaryNavigation.js index 530570b70a..dc81c4a4fc 100644 --- a/scm-ui/src/components/navigation/PrimaryNavigation.js +++ b/scm-ui/src/components/navigation/PrimaryNavigation.js @@ -28,6 +28,10 @@ class PrimaryNavigation extends React.Component { match="/(group|groups)" label={t("primary-navigation.groups")} /> + Here, Config will be shown; + } +} + +export default Config; diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 5272c6117b..cfc2e26f93 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -19,6 +19,8 @@ import Groups from "../groups/containers/Groups"; import SingleGroup from "../groups/containers/SingleGroup"; import AddGroup from "../groups/containers/AddGroup"; +import Config from "../config/containers/Config"; + type Props = { authenticated?: boolean }; @@ -99,6 +101,12 @@ class Main extends React.Component { component={Groups} authenticated={authenticated} /> + ); From 4edf5d6f35d42994919a17fb24299d4980f86672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Thu, 9 Aug 2018 10:18:12 +0200 Subject: [PATCH 03/73] added global navigation --- scm-ui/src/config/containers/Config.js | 52 ++++++++++++++++++-- scm-ui/src/config/containers/GlobalConfig.js | 19 +++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 scm-ui/src/config/containers/GlobalConfig.js diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 47bb8b55b0..0744ae089f 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -1,9 +1,53 @@ -import React, { Component } from "react"; +import React from "react"; +import { translate } from "react-i18next"; +import { Route } from "react-router"; + +import { Page } from "../../components/layout"; +import { Navigation, NavLink, Section } from "../../components/navigation"; +import GlobalConfig from "./GlobalConfig"; +import type { History } from "history"; + +type Props = { + // context objects + t: string => string, + match: any, + history: History +}; + +class Config extends React.Component { + stripEndingSlash = (url: string) => { + if (url.endsWith("/")) { + return url.substring(0, url.length - 2); + } + return url; + }; + + matchedUrl = () => { + return this.stripEndingSlash(this.props.match.url); + }; -class Config extends Component { render() { - return
Here, Config will be shown
; + const { t } = this.props; + + const url = this.matchedUrl(); + + return ( + +
+
+ } /> +
+
+ +
+ +
+
+
+
+
+ ); } } -export default Config; +export default translate("config")(Config); diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js new file mode 100644 index 0000000000..fde650413f --- /dev/null +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -0,0 +1,19 @@ +import React from "react"; +import { Page } from "../../components/layout"; +import type { History } from "history"; +import { translate } from "react-i18next"; + +type Props = { + // context objects + t: string => string +}; + +class GlobalConfig extends React.Component { + render() { + const { t } = this.props; + + return
Here, global config will be shown
; + } +} + +export default translate("config")(GlobalConfig); From 52eaafa549b161543e96d066ba3ec56177d7b811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Thu, 9 Aug 2018 11:33:08 +0200 Subject: [PATCH 04/73] refactoring: change title and subtitle to independent components --- scm-ui/src/components/layout/Page.js | 18 ++++++------------ scm-ui/src/components/layout/Subtitle.js | 17 +++++++++++++++++ scm-ui/src/components/layout/Title.js | 17 +++++++++++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 scm-ui/src/components/layout/Subtitle.js create mode 100644 scm-ui/src/components/layout/Title.js diff --git a/scm-ui/src/components/layout/Page.js b/scm-ui/src/components/layout/Page.js index 77d206d348..f0f2d5d971 100644 --- a/scm-ui/src/components/layout/Page.js +++ b/scm-ui/src/components/layout/Page.js @@ -2,9 +2,11 @@ import * as React from "react"; import Loading from "./../Loading"; import ErrorNotification from "./../ErrorNotification"; +import Title from "./Title"; +import Subtitle from "./Subtitle"; type Props = { - title: string, + title?: string, subtitle?: string, loading?: boolean, error?: Error, @@ -14,12 +16,12 @@ type Props = { class Page extends React.Component { render() { - const { title, error } = this.props; + const { title, error, subtitle } = this.props; return (
-

{title}

- {this.renderSubtitle()} + + <Subtitle subtitle={subtitle} /> <ErrorNotification error={error} /> {this.renderContent()} </div> @@ -27,14 +29,6 @@ class Page extends React.Component<Props> { ); } - renderSubtitle() { - const { subtitle } = this.props; - if (subtitle) { - return <h2 className="subtitle">{subtitle}</h2>; - } - return null; - } - renderContent() { const { loading, children, showContentOnError, error } = this.props; if (error && !showContentOnError) { diff --git a/scm-ui/src/components/layout/Subtitle.js b/scm-ui/src/components/layout/Subtitle.js new file mode 100644 index 0000000000..e55dbde5e4 --- /dev/null +++ b/scm-ui/src/components/layout/Subtitle.js @@ -0,0 +1,17 @@ +import React from "react"; + +type Props = { + subtitle?: string +}; + +class Subtitle extends React.Component<Props> { + render() { + const { subtitle } = this.props; + if (subtitle) { + return <h1 className="subtitle">{subtitle}</h1>; + } + return null; + } +} + +export default Subtitle; diff --git a/scm-ui/src/components/layout/Title.js b/scm-ui/src/components/layout/Title.js new file mode 100644 index 0000000000..fbb58da14c --- /dev/null +++ b/scm-ui/src/components/layout/Title.js @@ -0,0 +1,17 @@ +import React from "react"; + +type Props = { + title?: string +}; + +class Title extends React.Component<Props> { + render() { + const { title } = this.props; + if (title) { + return <h1 className="title">{title}</h1>; + } + return null; + } +} + +export default Title; From 9395b77ca2a3073ff8ecb901228423a1b3976ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 11:34:23 +0200 Subject: [PATCH 05/73] set title in global config and not in general config component --- scm-ui/src/config/containers/Config.js | 2 +- scm-ui/src/config/containers/GlobalConfig.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 0744ae089f..a5266a6680 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -32,7 +32,7 @@ class Config extends React.Component<Props> { const url = this.matchedUrl(); return ( - <Page title={t("config.title")}> + <Page> <div className="columns"> <div className="column is-three-quarters"> <Route path={url} exact component={() => <GlobalConfig />} /> diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index fde650413f..e0877e3a09 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -1,7 +1,6 @@ import React from "react"; -import { Page } from "../../components/layout"; -import type { History } from "history"; import { translate } from "react-i18next"; +import Title from "../../components/layout/Title"; type Props = { // context objects @@ -12,7 +11,7 @@ class GlobalConfig extends React.Component<Props> { render() { const { t } = this.props; - return <div>Here, global config will be shown</div>; + return <Title title={t("config.title")} />; } } From 08771624352c5c4f2c9db280246aed0b647bd49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 11:36:10 +0200 Subject: [PATCH 06/73] add endpoint for reading global config --- scm-ui/src/config/modules/config.js | 71 ++++++++++++++ scm-ui/src/config/modules/config.test.js | 116 +++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 scm-ui/src/config/modules/config.js create mode 100644 scm-ui/src/config/modules/config.test.js diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js new file mode 100644 index 0000000000..b951d33ed3 --- /dev/null +++ b/scm-ui/src/config/modules/config.js @@ -0,0 +1,71 @@ +// @flow +import { apiClient } from "../../apiclient"; +import * as types from "../../modules/types"; +import type { Action } from "../../types/Action"; + +export const FETCH_CONFIG = "scm/groups/FETCH_CONFIG"; +export const FETCH_CONFIG_PENDING = `${FETCH_CONFIG}_${types.PENDING_SUFFIX}`; +export const FETCH_CONFIG_SUCCESS = `${FETCH_CONFIG}_${types.SUCCESS_SUFFIX}`; +export const FETCH_CONFIG_FAILURE = `${FETCH_CONFIG}_${types.FAILURE_SUFFIX}`; + +const CONFIG_URL = "config"; + +//fetch config +export function fetchConfig() { + return function(dispatch: any) { + dispatch(fetchConfigPending()); + return apiClient + .get(CONFIG_URL) + .then(response => { + return response.json(); + }) + .then(data => { + dispatch(fetchConfigSuccess(data)); + }) + .catch(cause => { + const error = new Error(`could not fetch config: ${cause.message}`); + dispatch(fetchConfigFailure(error)); + }); + }; +} + +export function fetchConfigPending(): Action { + return { + type: FETCH_CONFIG_PENDING + }; +} + +export function fetchConfigSuccess(config: any): Action { + return { + type: FETCH_CONFIG_SUCCESS, + payload: config + }; +} + +export function fetchConfigFailure(error: Error): Action { + return { + type: FETCH_CONFIG_FAILURE, + payload: { + error + } + }; +} + +//reducer + +function reducer(state: any = {}, action: any = {}) { + switch (action.type) { + case FETCH_CONFIG_SUCCESS: + return { + ...state, + config: { + entries: action.payload, + configUpdatePermission: action.payload._links.update ? true : false + } + }; + default: + return state; + } +} + +export default reducer; diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js new file mode 100644 index 0000000000..f81df3d522 --- /dev/null +++ b/scm-ui/src/config/modules/config.test.js @@ -0,0 +1,116 @@ +//@flow +import configureMockStore from "redux-mock-store"; +import thunk from "redux-thunk"; +import fetchMock from "fetch-mock"; + +import reducer, { + FETCH_CONFIG_PENDING, + FETCH_CONFIG_SUCCESS, + FETCH_CONFIG_FAILURE, + fetchConfig, + fetchConfigSuccess +} from "./config"; + +const CONFIG_URL = "/scm/api/rest/v2/config"; + +const error = new Error("You have an error!"); + +const config = { + proxyPassword: null, + proxyPort: 8080, + proxyServer: "proxy.mydomain.com", + proxyUser: null, + enableProxy: false, + realmDescription: "SONIA :: SCM Manager", + enableRepositoryArchive: false, + disableGroupingGrid: false, + dateFormat: "YYYY-MM-DD HH:mm:ss", + anonymousAccessEnabled: false, + adminGroups: [], + adminUsers: [], + baseUrl: "http://localhost:8081/scm", + forceBaseUrl: false, + loginAttemptLimit: -1, + proxyExcludes: [], + skipFailedAuthenticators: false, + pluginUrl: + "http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false", + loginAttemptLimitTimeout: 300, + enabledXsrfProtection: true, + defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy", + _links: { + self: { href: "http://localhost:8081/scm/api/rest/v2/config" }, + update: { href: "http://localhost:8081/scm/api/rest/v2/config" } + } +}; + +const responseBody = { + entries: config +}; + +const response = { + headers: { "content-type": "application/json" }, + responseBody +}; + +describe("config fetch()", () => { + const mockStore = configureMockStore([thunk]); + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should successfully fetch config", () => { + fetchMock.getOnce(CONFIG_URL, response); + + const expectedActions = [ + { type: FETCH_CONFIG_PENDING }, + { + type: FETCH_CONFIG_SUCCESS, + payload: response + } + ]; + + const store = mockStore({}); + + return store.dispatch(fetchConfig()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it("should fail getting config on HTTP 500", () => { + fetchMock.getOnce(CONFIG_URL, { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(fetchConfig()).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_CONFIG_PENDING); + expect(actions[1].type).toEqual(FETCH_CONFIG_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); +}); + +describe("config reducer", () => { + it("should update state correctly according to FETCH_CONFIG_SUCCESS action", () => { + const newState = reducer({}, fetchConfigSuccess(config)); + + expect(newState.config).toEqual({ + entries: config, + configUpdatePermission: true + }); + }); + + it("should set configUpdatePermission to true if update link is present", () => { + const newState = reducer({}, fetchConfigSuccess(config)); + + expect(newState.config.configUpdatePermission).toBeTruthy(); + }); + + it("should update state according to FETCH_GROUP_SUCCESS action", () => { + const newState = reducer({}, fetchConfigSuccess(config)); + expect(newState.config.entries).toBe(config); + }); +}); From 4f8101a8dd860b8ae3a0b1497890e49f69ccf29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 11:36:29 +0200 Subject: [PATCH 07/73] add translation file for config --- scm-ui/public/locales/en/config.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 scm-ui/public/locales/en/config.json diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json new file mode 100644 index 0000000000..c6bd8c6db7 --- /dev/null +++ b/scm-ui/public/locales/en/config.json @@ -0,0 +1,7 @@ +{ + "config": { + "title": "Configuration", + "navigation-title": "Navigation", + "globalConfig-label": "Global Configuration" + } +} From 866a70b816804ad2b1b2f85390c47341e5d8671c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 12:09:50 +0200 Subject: [PATCH 08/73] handle error and loading state --- scm-ui/public/locales/en/config.json | 8 ++- scm-ui/src/config/containers/Config.js | 2 +- scm-ui/src/config/containers/GlobalConfig.js | 61 ++++++++++++++++++-- scm-ui/src/config/modules/config.js | 12 ++++ scm-ui/src/config/modules/config.test.js | 33 ++++++++++- 5 files changed, 107 insertions(+), 9 deletions(-) diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index c6bd8c6db7..dd87bdcae2 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -1,7 +1,11 @@ { "config": { + "navigation-title": "Navigation" + }, + "global-config": { "title": "Configuration", - "navigation-title": "Navigation", - "globalConfig-label": "Global Configuration" + "navigation-label": "Global Configuration", + "error-title": "Error", + "error-subtitle": "Unknown Config Error" } } diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index a5266a6680..0a7d112192 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -40,7 +40,7 @@ class Config extends React.Component<Props> { <div className="column"> <Navigation> <Section label={t("config.navigation-title")}> - <NavLink to={`${url}`} label={t("config.globalConfig-label")} /> + <NavLink to={`${url}`} label={t("global-config.navigation-label")} /> </Section> </Navigation> </div> diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index e0877e3a09..a3fcca70d5 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -1,18 +1,69 @@ import React from "react"; import { translate } from "react-i18next"; import Title from "../../components/layout/Title"; +import { + fetchConfig, + getFetchConfigFailure, + isFetchConfigPending +} from "../modules/config"; +import connect from "react-redux/es/connect/connect"; +import ErrorPage from "../../components/ErrorPage"; +import Loading from "../../components/Loading"; type Props = { + loading: boolean, + error: Error, + // context objects - t: string => string + t: string => string, + fetchConfig: void => void }; class GlobalConfig extends React.Component<Props> { - render() { - const { t } = this.props; + componentDidMount() { + this.props.fetchConfig(); + } - return <Title title={t("config.title")} />; + render() { + const { t, error, loading } = this.props; + + if (error) { + return ( + <ErrorPage + title={t("global-config.error-title")} + subtitle={t("global-config.error-subtitle")} + error={error} + /> + ); + } + + if (loading) { + return <Loading />; + } + + return <Title title={t("global-config.title")} />; } } -export default translate("config")(GlobalConfig); +const mapDispatchToProps = dispatch => { + return { + fetchConfig: () => { + dispatch(fetchConfig()); + } + }; +}; + +const mapStateToProps = state => { + const loading = isFetchConfigPending(state); + const error = getFetchConfigFailure(state); + + return { + loading, + error + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("config")(GlobalConfig)); diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index b951d33ed3..062be6fc95 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -2,6 +2,8 @@ import { apiClient } from "../../apiclient"; import * as types from "../../modules/types"; import type { Action } from "../../types/Action"; +import { isPending } from "../../modules/pending"; +import { getFailure } from "../../modules/failure"; export const FETCH_CONFIG = "scm/groups/FETCH_CONFIG"; export const FETCH_CONFIG_PENDING = `${FETCH_CONFIG}_${types.PENDING_SUFFIX}`; @@ -69,3 +71,13 @@ function reducer(state: any = {}, action: any = {}) { } export default reducer; + +// selectors + +export function isFetchConfigPending(state: Object) { + return isPending(state, FETCH_CONFIG); +} + +export function getFetchConfigFailure(state: Object) { + return getFailure(state, FETCH_CONFIG); +} diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index f81df3d522..86b5db7cbf 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -4,11 +4,14 @@ import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; import reducer, { + FETCH_CONFIG, FETCH_CONFIG_PENDING, FETCH_CONFIG_SUCCESS, FETCH_CONFIG_FAILURE, fetchConfig, - fetchConfigSuccess + fetchConfigSuccess, + getFetchConfigFailure, + isFetchConfigPending } from "./config"; const CONFIG_URL = "/scm/api/rest/v2/config"; @@ -114,3 +117,31 @@ describe("config reducer", () => { expect(newState.config.entries).toBe(config); }); }); + +describe("selector tests", () => { + it("should return true, when fetch config is pending", () => { + const state = { + pending: { + [FETCH_CONFIG]: true + } + }; + expect(isFetchConfigPending(state)).toEqual(true); + }); + + it("should return false, when fetch config is not pending", () => { + expect(isFetchConfigPending({})).toEqual(false); + }); + + it("should return error when fetch config did fail", () => { + const state = { + failure: { + [FETCH_CONFIG]: error + } + }; + expect(getFetchConfigFailure(state)).toEqual(error); + }); + + it("should return undefined when fetch config did not fail", () => { + expect(getFetchConfigFailure({})).toBe(undefined); + }); +}); From e0063c5119356166fde2e21e242d9df6e21aa938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 13:05:34 +0200 Subject: [PATCH 09/73] add config to state --- scm-ui/src/createReduxStore.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index 55e0d3f514..8411326b53 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -11,6 +11,7 @@ import groups from "./groups/modules/groups"; import auth from "./modules/auth"; import pending from "./modules/pending"; import failure from "./modules/failure"; +import config from "./config/modules/config"; import type { BrowserHistory } from "history/createBrowserHistory"; @@ -26,7 +27,8 @@ function createReduxStore(history: BrowserHistory) { repos, repositoryTypes, groups, - auth + auth, + config }); return createStore( From b0c1b64c43ac6e0cced533e4165d97629226bb18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 13:05:59 +0200 Subject: [PATCH 10/73] correct structure of config state part --- scm-ui/src/config/modules/config.js | 6 ++---- scm-ui/src/config/modules/config.test.js | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 062be6fc95..521dd94495 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -60,10 +60,8 @@ function reducer(state: any = {}, action: any = {}) { case FETCH_CONFIG_SUCCESS: return { ...state, - config: { - entries: action.payload, - configUpdatePermission: action.payload._links.update ? true : false - } + entries: action.payload, + configUpdatePermission: action.payload._links.update ? true : false }; default: return state; diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index 86b5db7cbf..837e7d126f 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -100,7 +100,7 @@ describe("config reducer", () => { it("should update state correctly according to FETCH_CONFIG_SUCCESS action", () => { const newState = reducer({}, fetchConfigSuccess(config)); - expect(newState.config).toEqual({ + expect(newState).toEqual({ entries: config, configUpdatePermission: true }); @@ -109,12 +109,12 @@ describe("config reducer", () => { it("should set configUpdatePermission to true if update link is present", () => { const newState = reducer({}, fetchConfigSuccess(config)); - expect(newState.config.configUpdatePermission).toBeTruthy(); + expect(newState.configUpdatePermission).toBeTruthy(); }); - it("should update state according to FETCH_GROUP_SUCCESS action", () => { + it("should update state according to FETCH_CONFIG_SUCCESS action", () => { const newState = reducer({}, fetchConfigSuccess(config)); - expect(newState.config.entries).toBe(config); + expect(newState.entries).toBe(config); }); }); From 00e078e3ab6630fe7ca4c3502904b8d581ca3eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 16:33:28 +0200 Subject: [PATCH 11/73] remove function because otherwise component is mounted three times instead of one --- scm-ui/src/config/containers/Config.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 0a7d112192..e41d3cc653 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -35,12 +35,15 @@ class Config extends React.Component<Props> { <Page> <div className="columns"> <div className="column is-three-quarters"> - <Route path={url} exact component={() => <GlobalConfig />} /> + <Route path={url} exact component={GlobalConfig} /> </div> <div className="column"> <Navigation> <Section label={t("config.navigation-title")}> - <NavLink to={`${url}`} label={t("global-config.navigation-label")} /> + <NavLink + to={`${url}`} + label={t("global-config.navigation-label")} + /> </Section> </Navigation> </div> From a4e1c8b023f58a5584efe0e68123c0a8cd420fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 16:34:08 +0200 Subject: [PATCH 12/73] renaming --- scm-ui/src/config/modules/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 521dd94495..e7a8e87e98 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -5,7 +5,7 @@ import type { Action } from "../../types/Action"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; -export const FETCH_CONFIG = "scm/groups/FETCH_CONFIG"; +export const FETCH_CONFIG = "scm/config/FETCH_CONFIG"; export const FETCH_CONFIG_PENDING = `${FETCH_CONFIG}_${types.PENDING_SUFFIX}`; export const FETCH_CONFIG_SUCCESS = `${FETCH_CONFIG}_${types.SUCCESS_SUFFIX}`; export const FETCH_CONFIG_FAILURE = `${FETCH_CONFIG}_${types.FAILURE_SUFFIX}`; From fa1b82f9102f3dcd5fa7520e07296d4f2dc87bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 16:44:24 +0200 Subject: [PATCH 13/73] added config modify --- scm-ui/src/config/modules/config.js | 54 +++++++++++++++++++++++ scm-ui/src/config/modules/config.test.js | 55 +++++++++++++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index e7a8e87e98..2e51693719 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -4,13 +4,20 @@ import * as types from "../../modules/types"; import type { Action } from "../../types/Action"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; +import { Dispatch } from "redux"; export const FETCH_CONFIG = "scm/config/FETCH_CONFIG"; export const FETCH_CONFIG_PENDING = `${FETCH_CONFIG}_${types.PENDING_SUFFIX}`; export const FETCH_CONFIG_SUCCESS = `${FETCH_CONFIG}_${types.SUCCESS_SUFFIX}`; export const FETCH_CONFIG_FAILURE = `${FETCH_CONFIG}_${types.FAILURE_SUFFIX}`; +export const MODIFY_CONFIG = "scm/config/FETCH_CONFIG"; +export const MODIFY_CONFIG_PENDING = `${MODIFY_CONFIG}_${types.PENDING_SUFFIX}`; +export const MODIFY_CONFIG_SUCCESS = `${MODIFY_CONFIG}_${types.SUCCESS_SUFFIX}`; +export const MODIFY_CONFIG_FAILURE = `${MODIFY_CONFIG}_${types.FAILURE_SUFFIX}`; + const CONFIG_URL = "config"; +const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2"; //fetch config export function fetchConfig() { @@ -53,6 +60,53 @@ export function fetchConfigFailure(error: Error): Action { }; } +// modify config +export function modifyConfig(config: any, callback?: () => void) { + return function(dispatch: Dispatch) { + dispatch(modifyConfigPending(config)); + return apiClient + .put(config._links.update.href, config, CONTENT_TYPE_CONFIG) + .then(() => { + dispatch(modifyConfigSuccess(config)); + if (callback) { + callback(); + } + }) + .catch(cause => { + dispatch( + modifyConfigFailure( + config, + new Error(`could not modify config: ${cause.message}`) + ) + ); + }); + }; +} + +export function modifyConfigPending(config: any): Action { + return { + type: MODIFY_CONFIG_PENDING, + payload: config + }; +} + +export function modifyConfigSuccess(config: any): Action { + return { + type: MODIFY_CONFIG_SUCCESS, + payload: config + }; +} + +export function modifyConfigFailure(config: any, error: Error): Action { + return { + type: MODIFY_CONFIG_FAILURE, + payload: { + error, + config + } + }; +} + //reducer function reducer(state: any = {}, action: any = {}) { diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index 837e7d126f..b62ed50bf8 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -8,10 +8,14 @@ import reducer, { FETCH_CONFIG_PENDING, FETCH_CONFIG_SUCCESS, FETCH_CONFIG_FAILURE, + MODIFY_CONFIG_PENDING, + MODIFY_CONFIG_SUCCESS, + MODIFY_CONFIG_FAILURE, fetchConfig, fetchConfigSuccess, getFetchConfigFailure, - isFetchConfigPending + isFetchConfigPending, + modifyConfig } from "./config"; const CONFIG_URL = "/scm/api/rest/v2/config"; @@ -94,6 +98,55 @@ describe("config fetch()", () => { expect(actions[1].payload).toBeDefined(); }); }); + + it("should successfully modify config", () => { + fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/config", { + status: 204 + }); + + const store = mockStore({}); + + return store.dispatch(modifyConfig(config)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_CONFIG_PENDING); + expect(actions[1].type).toEqual(MODIFY_CONFIG_SUCCESS); + expect(actions[1].payload).toEqual(config); + }); + }); + + it("should call the callback after modifying config", () => { + fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/config", { + status: 204 + }); + + let called = false; + const callback = () => { + called = true; + }; + const store = mockStore({}); + + return store.dispatch(modifyConfig(config, callback)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_CONFIG_PENDING); + expect(actions[1].type).toEqual(MODIFY_CONFIG_SUCCESS); + expect(called).toBe(true); + }); + }); + + it("should fail modifying config on HTTP 500", () => { + fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/config", { + status: 500 + }); + + const store = mockStore({}); + + return store.dispatch(modifyConfig(config)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(MODIFY_CONFIG_PENDING); + expect(actions[1].type).toEqual(MODIFY_CONFIG_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); }); describe("config reducer", () => { From 10822fa4e0b37b95cc9d04f4fdab5976267fbe75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 08:33:44 +0200 Subject: [PATCH 14/73] add selectors for config content --- scm-ui/src/config/modules/config.js | 12 ++++++++++++ scm-ui/src/config/modules/config.test.js | 22 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 2e51693719..df04e53235 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -133,3 +133,15 @@ export function isFetchConfigPending(state: Object) { export function getFetchConfigFailure(state: Object) { return getFailure(state, FETCH_CONFIG); } + +export function getConfig(state: Object) { + if (state.config && state.config.entries) { + return state.config.entries; + } +} + +export function getConfigUpdatePermission(state: Object) { + if (state.config && state.config.configUpdatePermission) { + return state.config.configUpdatePermission; + } +} diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index b62ed50bf8..76f8f1a2b2 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -15,7 +15,9 @@ import reducer, { fetchConfigSuccess, getFetchConfigFailure, isFetchConfigPending, - modifyConfig + modifyConfig, + getConfig, + getConfigUpdatePermission } from "./config"; const CONFIG_URL = "/scm/api/rest/v2/config"; @@ -197,4 +199,22 @@ describe("selector tests", () => { it("should return undefined when fetch config did not fail", () => { expect(getFetchConfigFailure({})).toBe(undefined); }); + + it("should return config", () => { + const state = { + config: { + entries: config + } + }; + expect(getConfig(state)).toEqual(config); + }); + + it("should return configUpdatePermission", () => { + const state = { + config: { + configUpdatePermission: true + } + }; + expect(getConfigUpdatePermission(state)).toEqual(true); + }); }); From bb6f076178afbffed3259466e3639bf20b400ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 08:43:13 +0200 Subject: [PATCH 15/73] add selectors for modifyGroupPending and Failure --- scm-ui/src/config/modules/config.js | 8 ++++++ scm-ui/src/config/modules/config.test.js | 31 +++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index df04e53235..0c231ea1ac 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -134,6 +134,14 @@ export function getFetchConfigFailure(state: Object) { return getFailure(state, FETCH_CONFIG); } +export function isModifyConfigPending(state: Object) { + return isPending(state, MODIFY_CONFIG); +} + +export function getModifyConfigFailure(state: Object) { + return getFailure(state, MODIFY_CONFIG); +} + export function getConfig(state: Object) { if (state.config && state.config.entries) { return state.config.entries; diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index 76f8f1a2b2..78b19f10c7 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -8,6 +8,7 @@ import reducer, { FETCH_CONFIG_PENDING, FETCH_CONFIG_SUCCESS, FETCH_CONFIG_FAILURE, + MODIFY_CONFIG, MODIFY_CONFIG_PENDING, MODIFY_CONFIG_SUCCESS, MODIFY_CONFIG_FAILURE, @@ -16,6 +17,8 @@ import reducer, { getFetchConfigFailure, isFetchConfigPending, modifyConfig, + isModifyConfigPending, + getModifyConfigFailure, getConfig, getConfigUpdatePermission } from "./config"; @@ -200,6 +203,32 @@ describe("selector tests", () => { expect(getFetchConfigFailure({})).toBe(undefined); }); + it("should return true, when modify group is pending", () => { + const state = { + pending: { + [MODIFY_CONFIG]: true + } + }; + expect(isModifyConfigPending(state)).toEqual(true); + }); + + it("should return false, when modify config is not pending", () => { + expect(isModifyConfigPending({})).toEqual(false); + }); + + it("should return error when modify config did fail", () => { + const state = { + failure: { + [MODIFY_CONFIG]: error + } + }; + expect(getModifyConfigFailure(state)).toEqual(error); + }); + + it("should return undefined when modify config did not fail", () => { + expect(getModifyConfigFailure({})).toBe(undefined); + }); + it("should return config", () => { const state = { config: { @@ -209,7 +238,7 @@ describe("selector tests", () => { expect(getConfig(state)).toEqual(config); }); - it("should return configUpdatePermission", () => { + it("should return configUpdatePermission", () => { const state = { config: { configUpdatePermission: true From 5767000eb5957e5df242e653b3b9b1ef2bfdc57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:33:27 +0200 Subject: [PATCH 16/73] added Config type --- scm-ui/src/config/types/Config.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scm-ui/src/config/types/Config.js diff --git a/scm-ui/src/config/types/Config.js b/scm-ui/src/config/types/Config.js new file mode 100644 index 0000000000..6c64cd1db7 --- /dev/null +++ b/scm-ui/src/config/types/Config.js @@ -0,0 +1,27 @@ +//@flow +import type { Links } from "../../types/hal"; + +export type Config = { + proxyPassword: string | null, + proxyPort: number, + proxyServer: string, + proxyUser: string | null, + enableProxy: boolean, + realmDescription: string, + enableRepositoryArchive: boolean, + disableGroupingGrid: boolean, + dateFormat: string, + anonymousAccessEnabled: boolean, + adminGroups: string[], + adminUsers: string[], + baseUrl: string, + forceBaseUrl: boolean, + loginAttemptLimit: number, + proxyExcludes: string[], + skipFailedAuthenticators: boolean, + pluginUrl: string, + loginAttemptLimitTimeout: number, + enabledXsrfProtection: boolean, + defaultNamespaceStrategy: string, + _links: Links +}; From 27ae4dac1a192c67ef4f7db609c881245fa110c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:33:56 +0200 Subject: [PATCH 17/73] show settings and let them modify --- .../config/components/form/AdminSettings.js | 29 ++++ .../config/components/form/BaseUrlSettings.js | 43 ++++++ .../src/config/components/form/ConfigForm.js | 126 ++++++++++++++++ .../config/components/form/GeneralSettings.js | 135 ++++++++++++++++++ .../config/components/form/ProxySettings.js | 82 +++++++++++ 5 files changed, 415 insertions(+) create mode 100644 scm-ui/src/config/components/form/AdminSettings.js create mode 100644 scm-ui/src/config/components/form/BaseUrlSettings.js create mode 100644 scm-ui/src/config/components/form/ConfigForm.js create mode 100644 scm-ui/src/config/components/form/GeneralSettings.js create mode 100644 scm-ui/src/config/components/form/ProxySettings.js diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js new file mode 100644 index 0000000000..5079addca1 --- /dev/null +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -0,0 +1,29 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Checkbox, InputField } from "../../../components/forms/index"; +import Subtitle from "../../../components/layout/Subtitle"; + +type Props = { + adminGroups: string[], + adminUsers: string[], + t: string => string, + onChange: (boolean, any, string) => void +}; +//TODO: Einbauen! +class AdminSettings extends React.Component<Props> { + render() { + const { + t, + adminGroups, + adminUsers + } = this.props; + + return ( + null + ); + } + +} + +export default translate("config")(AdminSettings); diff --git a/scm-ui/src/config/components/form/BaseUrlSettings.js b/scm-ui/src/config/components/form/BaseUrlSettings.js new file mode 100644 index 0000000000..cde4680b48 --- /dev/null +++ b/scm-ui/src/config/components/form/BaseUrlSettings.js @@ -0,0 +1,43 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Checkbox, InputField } from "../../../components/forms/index"; +import Subtitle from "../../../components/layout/Subtitle"; + +type Props = { + baseUrl: string, + forceBaseUrl: boolean, + t: string => string, + onChange: (boolean, any, string) => void +}; + +class BaseUrlSettings extends React.Component<Props> { + render() { + const { t, baseUrl, forceBaseUrl } = this.props; + + return ( + <div> + <Subtitle subtitle={t("base-url-settings.name")} /> + <Checkbox + checked={forceBaseUrl} + label={t("base-url-settings.force-base-url")} + onChange={this.handleForceBaseUrlChange} + /> + <InputField + label={t("base-url-settings.base-url")} + onChange={this.handleBaseUrlChange} + value={baseUrl} + /> + </div> + ); + } + + handleBaseUrlChange = (value: string) => { + this.props.onChange(true, value, "baseUrl"); + }; + handleForceBaseUrlChange = (value: boolean) => { + this.props.onChange(true, value, "forceBaseUrl"); + }; +} + +export default translate("config")(BaseUrlSettings); diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js new file mode 100644 index 0000000000..ac7325e6fc --- /dev/null +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -0,0 +1,126 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { SubmitButton } from "../../../components/buttons/index"; +import type { Config } from "../../types/Config"; +import ProxySettings from "./ProxySettings"; +import GeneralSettings from "./GeneralSettings"; +import BaseUrlSettings from "./BaseUrlSettings"; + +type Props = { + submitForm: Config => void, + config?: Config, + loading?: boolean, + t: string => string +}; + +type State = { + config: Config +}; + +class ConfigForm extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + config: { + proxyPassword: null, + proxyPort: 0, + proxyServer: "", + proxyUser: null, + enableProxy: false, + realmDescription: "", + enableRepositoryArchive: false, + disableGroupingGrid: false, + dateFormat: "", + anonymousAccessEnabled: false, + adminGroups: [], + adminUsers: [], + baseUrl: "", + forceBaseUrl: false, + loginAttemptLimit: 0, + proxyExcludes: [], + skipFailedAuthenticators: false, + pluginUrl: "", + loginAttemptLimitTimeout: 0, + enabledXsrfProtection: true, + defaultNamespaceStrategy: "", + _links: {} + } + }; + } + + componentDidMount() { + const { config } = this.props; + console.log(config); + if (config) { + this.setState({ config: { ...config } }); + } + } + + submit = (event: Event) => { + event.preventDefault(); + this.props.submitForm(this.state.config); + }; + + render() { + const { loading, t } = this.props; + let config = this.state.config; + + return ( + <form onSubmit={this.submit}> + <GeneralSettings + realmDescription={config.realmDescription} + enableRepositoryArchive={config.enableRepositoryArchive} + disableGroupingGrid={config.disableGroupingGrid} + dateFormat={config.dateFormat} + anonymousAccessEnabled={config.anonymousAccessEnabled} + loginAttemptLimit={config.loginAttemptLimit} + skipFailedAuthenticators={config.skipFailedAuthenticators} + pluginUrl={config.pluginUrl} + loginAttemptLimitTimeout={config.loginAttemptLimitTimeout} + enabledXsrfProtection={config.enabledXsrfProtection} + defaultNamespaceStrategy={config.defaultNamespaceStrategy} + onChange={(isValid, changedValue, name) => + this.onChange(isValid, changedValue, name) + } + /> + <BaseUrlSettings + baseUrl={config.baseUrl} + forceBaseUrl={config.forceBaseUrl} + onChange={(isValid, changedValue, name) => + this.onChange(isValid, changedValue, name) + } + /> + <ProxySettings + proxyPassword={config.proxyPassword ? config.proxyPassword : ""} + proxyPort={config.proxyPort} + proxyServer={config.proxyServer ? config.proxyServer : ""} + proxyUser={config.proxyUser ? config.proxyUser : ""} + enableProxy={config.enableProxy} + onChange={(isValid, changedValue, name) => + this.onChange(isValid, changedValue, name) + } + /> + <SubmitButton + // disabled={!this.isValid()} + loading={loading} + label={t("config-form.submit")} + /> + </form> + ); + } + + onChange = (isValid: boolean, changedValue: any, name: string) => { + if (isValid) { + this.setState({ + config: { + ...this.state.config, + [name]: changedValue + } + }); + } + }; +} + +export default translate("config")(ConfigForm); diff --git a/scm-ui/src/config/components/form/GeneralSettings.js b/scm-ui/src/config/components/form/GeneralSettings.js new file mode 100644 index 0000000000..29db1e5288 --- /dev/null +++ b/scm-ui/src/config/components/form/GeneralSettings.js @@ -0,0 +1,135 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Checkbox, InputField } from "../../../components/forms/index"; + +type Props = { + realmDescription: string, + enableRepositoryArchive: boolean, + disableGroupingGrid: boolean, + dateFormat: string, + anonymousAccessEnabled: boolean, + loginAttemptLimit: number, + skipFailedAuthenticators: boolean, + pluginUrl: string, + loginAttemptLimitTimeout: number, + enabledXsrfProtection: boolean, + defaultNamespaceStrategy: string, + t: string => string, + onChange: (boolean, any, string) => void +}; + +class GeneralSettings extends React.Component<Props> { + render() { + const { + t, + realmDescription, + enableRepositoryArchive, + disableGroupingGrid, + dateFormat, + anonymousAccessEnabled, + loginAttemptLimit, + skipFailedAuthenticators, + pluginUrl, + loginAttemptLimitTimeout, + enabledXsrfProtection, + defaultNamespaceStrategy + } = this.props; + + return ( + <div> + <InputField + label={t("general-settings.realm-description")} + onChange={this.handleRealmDescriptionChange} + value={realmDescription} + /> + <Checkbox + checked={enableRepositoryArchive} + label={t("general-settings.enable-repository-archive")} + onChange={this.handleEnableRepositoryArchiveChange} + /> + <Checkbox + checked={disableGroupingGrid} + label={t("general-settings.disable-grouping-grid")} + onChange={this.handleDisableGroupingGridChange} + /> + <InputField + label={t("general-settings.date-format")} + onChange={this.handleDateFormatChange} + value={dateFormat} + /> + <Checkbox + checked={anonymousAccessEnabled} + label={t("general-settings.anonymous-access-enabled")} + onChange={this.handleAnonymousAccessEnabledChange} + /> + <InputField + label={t("general-settings.login-attempt-limit")} + onChange={this.handleLoginAttemptLimitChange} + value={loginAttemptLimit} + /> + <InputField + label={t("general-settings.login-attempt-limit-timeout")} + onChange={this.handleLoginAttemptLimitTimeoutChange} + value={loginAttemptLimitTimeout} + /> + <Checkbox + checked={skipFailedAuthenticators} + label={t("general-settings.skip-failed-authenticators")} + onChange={this.handleSkipFailedAuthenticatorsChange} + /> + <InputField + label={t("general-settings.plugin-url")} + onChange={this.handlePluginUrlChange} + value={pluginUrl} + /> + <Checkbox + checked={enabledXsrfProtection} + label={t("general-settings.enabled-xsrf-protection")} + onChange={this.handleEnabledXsrfProtectionChange} + /> + <InputField + label={t("general-settings.default-namespace-strategy")} + onChange={this.handleDefaultNamespaceStrategyChange} + value={defaultNamespaceStrategy} + /> + </div> + ); + } + + handleRealmDescriptionChange = (value: string) => { + this.props.onChange(true, value, "realmDescription"); + }; + handleEnableRepositoryArchiveChange = (value: boolean) => { + this.props.onChange(true, value, "enableRepositoryArchive"); + }; + handleDisableGroupingGridChange = (value: boolean) => { + this.props.onChange(true, value, "disableGroupingGrid"); + }; + handleDateFormatChange = (value: string) => { + this.props.onChange(true, value, "dateFormat"); + }; + handleAnonymousAccessEnabledChange = (value: string) => { + this.props.onChange(true, value, "anonymousAccessEnabled"); + }; + handleLoginAttemptLimitChange = (value: string) => { + this.props.onChange(true, value, "loginAttemptLimit"); + }; + handleSkipFailedAuthenticatorsChange = (value: string) => { + this.props.onChange(true, value, "skipFailedAuthenticators"); + }; + handlePluginUrlChange = (value: string) => { + this.props.onChange(true, value, "pluginUrl"); + }; + handleLoginAttemptLimitTimeoutChange = (value: string) => { + this.props.onChange(true, value, "loginAttemptLimitTimeout"); + }; + handleEnabledXsrfProtectionChange = (value: boolean) => { + this.props.onChange(true, value, "enabledXsrfProtection"); + }; + handleDefaultNamespaceStrategyChange = (value: string) => { + this.props.onChange(true, value, "defaultNamespaceStrategy"); + }; +} + +export default translate("config")(GeneralSettings); diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js new file mode 100644 index 0000000000..2938c08a71 --- /dev/null +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -0,0 +1,82 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Checkbox, InputField } from "../../../components/forms/index"; +import Subtitle from "../../../components/layout/Subtitle"; + +type Props = { + proxyPassword: string, + proxyPort: number, + proxyServer: string, + proxyUser: string, + enableProxy: boolean, + proxyExcludes: string[], //TODO: einbauen! + t: string => string, + onChange: (boolean, any, string) => void +}; + +class ProxySettings extends React.Component<Props> { + render() { + const { + t, + proxyPassword, + proxyPort, + proxyServer, + proxyUser, + enableProxy + } = this.props; + + return ( + <div> + <Subtitle subtitle={t("proxy-settings.name")} /> + <Checkbox + checked={enableProxy} + label={t("proxy-settings.enable-proxy")} + onChange={this.handleEnableProxyChange} + /> + <InputField + label={t("proxy-settings.proxy-password")} + onChange={this.handleProxyPasswordChange} + value={proxyPassword} + disable={!enableProxy} + /> + <InputField + label={t("proxy-settings.proxy-port")} + value={proxyPort} + onChange={this.handleProxyPortChange} + disable={!enableProxy} + /> + <InputField + label={t("proxy-settings.proxy-server")} + value={proxyServer} + onChange={this.handleProxyServerChange} + disable={!enableProxy} + /> + <InputField + label={t("proxy-settings.proxy-user")} + value={proxyUser} + onChange={this.handleProxyUserChange} + disable={!enableProxy} + /> + </div> + ); + } + + handleProxyPasswordChange = (value: string) => { + this.props.onChange(true, value, "proxyPassword"); + }; + handleProxyPortChange = (value: string) => { + this.props.onChange(true, value, "proxyPort"); + }; + handleProxyServerChange = (value: string) => { + this.props.onChange(true, value, "proxyServer"); + }; + handleProxyUserChange = (value: string) => { + this.props.onChange(true, value, "proxyUser"); + }; + handleEnableProxyChange = (value: string) => { + this.props.onChange(true, value, "enableProxy"); + }; +} + +export default translate("config")(ProxySettings); From 5acd1ba90a03f99263e2ec80cd6c6703c6494d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:34:12 +0200 Subject: [PATCH 18/73] add translations --- scm-ui/public/locales/en/config.json | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index dd87bdcae2..df2a12a6dc 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -7,5 +7,39 @@ "navigation-label": "Global Configuration", "error-title": "Error", "error-subtitle": "Unknown Config Error" + }, + "config-form": { + "submit": "Submit" + }, + "proxy-settings": { + "name": "Proxy Settings", + "proxy-password": "Proxy Password", + "proxy-port": "Proxy Port", + "proxy-server": "Proxy Server", + "proxy-user": "Proxy User", + "enable-proxy": "Enable Proxy", + "proxy-excludes": "Proxy Excludes" + }, + "base-url-settings": { + "name": "Base URL Settings", + "base-url": "Base URL", + "force-base-url": "Force Base URL" + }, + "admin-settings": { + "admin-groups": "Admin Groups", + "admin-user": "Admin Users" + }, + "general-settings": { + "realm-description": "Realm Description", + "enable-repository-archive": "Enable Repository Archive", + "disable-grouping-grid": "Disable Grouping Grid", + "date-format": "Date Format", + "anonymous-access-enabled": "Anonymous Access Enabled", + "login-attempt-limit": "Login Attempt Limit", + "skip-failed-authenticators": "Skip Failed Authenticators", + "plugin-url": "Plugin URL", + "login-attempt-limit-timeout": "Login Attempt Limit Timeout", + "enabled-xsrf-protection": "Enabled XSRF Protection", + "default-namespace-strategy": "Default Namespace Strategy" } } From 0df136da19660c1d8f971e0da0495f352c3ce2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:34:31 +0200 Subject: [PATCH 19/73] add disabled option to input field --- scm-ui/src/components/forms/InputField.js | 29 ++++++++++++++++------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/scm-ui/src/components/forms/InputField.js b/scm-ui/src/components/forms/InputField.js index be657348e5..9833739461 100644 --- a/scm-ui/src/components/forms/InputField.js +++ b/scm-ui/src/components/forms/InputField.js @@ -11,7 +11,8 @@ type Props = { onChange: string => void, onReturnPressed?: () => void, validationError: boolean, - errorMessage: string + errorMessage: string, + disable?: boolean }; class InputField extends React.Component<Props> { @@ -40,21 +41,33 @@ class InputField extends React.Component<Props> { return ""; }; + handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => { const onReturnPressed = this.props.onReturnPressed; if (!onReturnPressed) { - return + return; } if (event.key === "Enter") { event.preventDefault(); onReturnPressed(); } - } + }; render() { - const { type, placeholder, value, validationError, errorMessage } = this.props; + const { + type, + placeholder, + value, + validationError, + errorMessage, + disable + } = this.props; const errorView = validationError ? "is-danger" : ""; - const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : ""; + const helper = validationError ? ( + <p className="help is-danger">{errorMessage}</p> + ) : ( + "" + ); return ( <div className="field"> {this.renderLabel()} @@ -63,15 +76,13 @@ class InputField extends React.Component<Props> { ref={input => { this.field = input; }} - className={ classNames( - "input", - errorView - )} + className={classNames("input", errorView)} type={type} placeholder={placeholder} value={value} onChange={this.handleInput} onKeyPress={this.handleKeyPress} + disabled={disable} /> </div> {helper} From 335ac2df248a3ec37a3a4a8e040e49a16644489e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:35:19 +0200 Subject: [PATCH 20/73] use Config type --- scm-ui/src/config/modules/config.js | 13 +++++++------ scm-ui/src/config/modules/config.test.js | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 0c231ea1ac..4534dbaeb6 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -5,13 +5,14 @@ import type { Action } from "../../types/Action"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; import { Dispatch } from "redux"; +import type { Config } from "../types/Config"; export const FETCH_CONFIG = "scm/config/FETCH_CONFIG"; export const FETCH_CONFIG_PENDING = `${FETCH_CONFIG}_${types.PENDING_SUFFIX}`; export const FETCH_CONFIG_SUCCESS = `${FETCH_CONFIG}_${types.SUCCESS_SUFFIX}`; export const FETCH_CONFIG_FAILURE = `${FETCH_CONFIG}_${types.FAILURE_SUFFIX}`; -export const MODIFY_CONFIG = "scm/config/FETCH_CONFIG"; +export const MODIFY_CONFIG = "scm/config/MODIFY_CONFIG"; export const MODIFY_CONFIG_PENDING = `${MODIFY_CONFIG}_${types.PENDING_SUFFIX}`; export const MODIFY_CONFIG_SUCCESS = `${MODIFY_CONFIG}_${types.SUCCESS_SUFFIX}`; export const MODIFY_CONFIG_FAILURE = `${MODIFY_CONFIG}_${types.FAILURE_SUFFIX}`; @@ -44,7 +45,7 @@ export function fetchConfigPending(): Action { }; } -export function fetchConfigSuccess(config: any): Action { +export function fetchConfigSuccess(config: Config): Action { return { type: FETCH_CONFIG_SUCCESS, payload: config @@ -61,7 +62,7 @@ export function fetchConfigFailure(error: Error): Action { } // modify config -export function modifyConfig(config: any, callback?: () => void) { +export function modifyConfig(config: Config, callback?: () => void) { return function(dispatch: Dispatch) { dispatch(modifyConfigPending(config)); return apiClient @@ -83,21 +84,21 @@ export function modifyConfig(config: any, callback?: () => void) { }; } -export function modifyConfigPending(config: any): Action { +export function modifyConfigPending(config: Config): Action { return { type: MODIFY_CONFIG_PENDING, payload: config }; } -export function modifyConfigSuccess(config: any): Action { +export function modifyConfigSuccess(config: Config): Action { return { type: MODIFY_CONFIG_SUCCESS, payload: config }; } -export function modifyConfigFailure(config: any, error: Error): Action { +export function modifyConfigFailure(config: Config, error: Error): Action { return { type: MODIFY_CONFIG_FAILURE, payload: { diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index 78b19f10c7..d991bd929a 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -57,7 +57,8 @@ const config = { }; const responseBody = { - entries: config + entries: config, + configUpdatePermission: false }; const response = { From f2850370dd9967f1c8f874dc32ffe0c78fd1ee93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:35:43 +0200 Subject: [PATCH 21/73] add modification of config --- scm-ui/src/config/containers/GlobalConfig.js | 47 ++++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index a3fcca70d5..a6da5f41bf 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -4,28 +4,46 @@ import Title from "../../components/layout/Title"; import { fetchConfig, getFetchConfigFailure, - isFetchConfigPending + isFetchConfigPending, + getConfig, + modifyConfig, + isModifyConfigPending } from "../modules/config"; import connect from "react-redux/es/connect/connect"; import ErrorPage from "../../components/ErrorPage"; +import type { Config } from "../types/Config"; +import ConfigForm from "../components/form/ConfigForm"; import Loading from "../../components/Loading"; +import type { User } from "../../users/types/User"; +import type { History } from "history"; type Props = { loading: boolean, error: Error, - + config: Config, + // dispatch functions + modifyConfig: (config: User, callback?: () => void) => void, // context objects t: string => string, - fetchConfig: void => void + fetchConfig: void => void, + history: History }; class GlobalConfig extends React.Component<Props> { + configModified = (config: Config) => () => { + this.props.history.push(`/config`); + }; + componentDidMount() { this.props.fetchConfig(); } + modifyConfig = (config: Config) => { + this.props.modifyConfig(config, this.configModified(config)); + }; + render() { - const { t, error, loading } = this.props; + const { t, error, loading, config } = this.props; if (error) { return ( @@ -36,12 +54,20 @@ class GlobalConfig extends React.Component<Props> { /> ); } - if (loading) { return <Loading />; } - return <Title title={t("global-config.title")} />; + return ( + <div> + <Title title={t("global-config.title")} /> + <ConfigForm + submitForm={config => this.modifyConfig(config)} + config={config} + loading={loading} + /> + </div> + ); } } @@ -49,17 +75,22 @@ const mapDispatchToProps = dispatch => { return { fetchConfig: () => { dispatch(fetchConfig()); + }, + modifyConfig: (config: Config, callback?: () => void) => { + dispatch(modifyConfig(config, callback)); } }; }; const mapStateToProps = state => { - const loading = isFetchConfigPending(state); + const loading = isFetchConfigPending(state) || isModifyConfigPending(state); //TODO: Button lädt so nicht, sondern gesamte Seite const error = getFetchConfigFailure(state); + const config = getConfig(state); return { loading, - error + error, + config }; }; From 012ac34a6accac5ce22aa6284decbfc11bbd5a76 Mon Sep 17 00:00:00 2001 From: Mohamed Karray <mohamed.karray.sr@gmail.com> Date: Tue, 14 Aug 2018 16:44:42 +0200 Subject: [PATCH 22/73] #8771 create the Permission DTO --- .../java/sonia/scm/repository/Permission.java | 81 ++++++++----------- .../scm/api/v2/resources/PermissionDto.java | 26 ++++++ .../api/v2/resources/PermissionTypeDto.java | 26 ++++++ 3 files changed, 85 insertions(+), 48 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java diff --git a/scm-core/src/main/java/sonia/scm/repository/Permission.java b/scm-core/src/main/java/sonia/scm/repository/Permission.java index b5de810f75..2a1f34aa85 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Permission.java +++ b/scm-core/src/main/java/sonia/scm/repository/Permission.java @@ -1,32 +1,32 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * +/* + Copyright (c) 2010, Sebastian Sdorra + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. Neither the name of SCM-Manager; nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + */ @@ -57,10 +57,11 @@ import javax.xml.bind.annotation.XmlRootElement; public class Permission implements PermissionObject, Serializable { - /** Field description */ private static final long serialVersionUID = -2915175031430884040L; - //~--- constructors --------------------------------------------------------- + private boolean groupPermission = false; + private String name; + private PermissionType type = PermissionType.READ; /** * Constructs a new {@link Permission}. @@ -153,12 +154,7 @@ public class Permission implements PermissionObject, Serializable return Objects.hashCode(name, type, groupPermission); } - /** - * Method description - * - * - * @return - */ + @Override public String toString() { @@ -242,15 +238,4 @@ public class Permission implements PermissionObject, Serializable { this.type = type; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private boolean groupPermission = false; - - /** Field description */ - private String name; - - /** Field description */ - private PermissionType type = PermissionType.READ; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java new file mode 100644 index 0000000000..ebd49423ab --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class PermissionDto extends HalRepresentation { + + @JsonInclude(JsonInclude.Include.NON_NULL) + private PermissionTypeDto type = PermissionTypeDto.READ; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String name; + + private boolean groupPermission = false; + + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java new file mode 100644 index 0000000000..7b280f9f1f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +/** + * Type of permissionPrefix for a {@link RepositoryDto}. + * + * @author mkarray + */ + +public enum PermissionTypeDto { + + /** + * read permission + */ + READ, + + /** + * read and write permission + */ + WRITE, + + /** + * read, write and manage the properties and permissions + */ + OWNER + +} From 41589d785d61a2c143b12e57500d747c3d090dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 10:55:39 +0200 Subject: [PATCH 23/73] added view and edit of admin user and groups --- scm-ui/public/locales/en/config.json | 11 ++- .../buttons/RemoveAdminGroupButton.js | 34 +++++++++ .../buttons/RemoveAdminUserButton.js | 34 +++++++++ .../components/fields/AddAdminGroupField.js | 71 +++++++++++++++++++ .../components/fields/AddAdminUserField.js | 71 +++++++++++++++++++ .../config/components/form/AdminSettings.js | 59 +++++++++++++-- .../src/config/components/form/ConfigForm.js | 15 +++- .../components/table/AdminGroupTable.js | 47 ++++++++++++ .../config/components/table/AdminUserTable.js | 47 ++++++++++++ scm-ui/src/config/containers/GlobalConfig.js | 13 +++- 10 files changed, 390 insertions(+), 12 deletions(-) create mode 100644 scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js create mode 100644 scm-ui/src/config/components/buttons/RemoveAdminUserButton.js create mode 100644 scm-ui/src/config/components/fields/AddAdminGroupField.js create mode 100644 scm-ui/src/config/components/fields/AddAdminUserField.js create mode 100644 scm-ui/src/config/components/table/AdminGroupTable.js create mode 100644 scm-ui/src/config/components/table/AdminUserTable.js diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index df2a12a6dc..53d41e682d 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -26,8 +26,17 @@ "force-base-url": "Force Base URL" }, "admin-settings": { + "name": "Administration Settings", "admin-groups": "Admin Groups", - "admin-user": "Admin Users" + "admin-users": "Admin Users", + "remove-group-button": "Remove Admin Group", + "remove-user-button": "Remove Admin User", + "add-group-error": "The group name you want to add is not valid", + "add-group-textfield": "Add group you want to add to admin groups here", + "add-group-button": "Add Admin Group", + "add-user-error": "The user name you want to add is not valid", + "add-user-textfield": "Add user you want to add to admin users here", + "add-user-button": "Add Admin User" }, "general-settings": { "realm-description": "Realm Description", diff --git a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js new file mode 100644 index 0000000000..c7d8f8d85a --- /dev/null +++ b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js @@ -0,0 +1,34 @@ +//@flow +import React from "react"; +import { DeleteButton } from "../../../components/buttons"; +import { translate } from "react-i18next"; +import classNames from "classnames"; + +type Props = { + t: string => string, + groupname: string, + removeGroup: string => void +}; + +type State = {}; + + + +class RemoveAdminGroupButton extends React.Component<Props, State> { + render() { + const { t , groupname, removeGroup} = this.props; + return ( + <div className={classNames("is-pulled-right")}> + <DeleteButton + label={t("admin-settings.remove-group-button")} + action={(event: Event) => { + event.preventDefault(); + removeGroup(groupname); + }} + /> + </div> + ); + } +} + +export default translate("config")(RemoveAdminGroupButton); diff --git a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js new file mode 100644 index 0000000000..6bdf06cd28 --- /dev/null +++ b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js @@ -0,0 +1,34 @@ +//@flow +import React from "react"; +import { DeleteButton } from "../../../components/buttons"; +import { translate } from "react-i18next"; +import classNames from "classnames"; + +type Props = { + t: string => string, + username: string, + removeUser: string => void +}; + +type State = {}; + + + +class RemoveAdminUserButton extends React.Component<Props, State> { + render() { + const { t , username, removeUser} = this.props; + return ( + <div className={classNames("is-pulled-right")}> + <DeleteButton + label={t("admin-settings.remove-user-button")} + action={(event: Event) => { + event.preventDefault(); + removeUser(username); + }} + /> + </div> + ); + } +} + +export default translate("config")(RemoveAdminUserButton); diff --git a/scm-ui/src/config/components/fields/AddAdminGroupField.js b/scm-ui/src/config/components/fields/AddAdminGroupField.js new file mode 100644 index 0000000000..ed694355d8 --- /dev/null +++ b/scm-ui/src/config/components/fields/AddAdminGroupField.js @@ -0,0 +1,71 @@ +//@flow +import React from "react"; + +import { translate } from "react-i18next"; +import { AddButton } from "../../../components/buttons"; +import InputField from "../../../components/forms/InputField"; + +type Props = { + t: string => string, + addGroup: string => void +}; + +type State = { + groupToAdd: string, + //validationError: boolean +}; + +class AddAdminGroupField extends React.Component<Props, State> { + constructor(props) { + super(props); + this.state = { + groupToAdd: "", + //validationError: false + }; + } + + render() { + const { t } = this.props; + return ( + <div className="field"> + <InputField + + label={t("admin-settings.add-group-textfield")} + errorMessage={t("admin-settings.add-group-error")} + onChange={this.handleAddGroupChange} + validationError={false} + value={this.state.groupToAdd} + onReturnPressed={this.appendGroup} + /> + <AddButton + label={t("admin-settings.add-group-button")} + action={this.addButtonClicked} + //disabled={!isMemberNameValid(this.state.memberToAdd)} + /> + </div> + ); + } + + addButtonClicked = (event: Event) => { + event.preventDefault(); + this.appendGroup(); + }; + + appendGroup = () => { + const { groupToAdd } = this.state; + //if (isMemberNameValid(memberToAdd)) { + this.props.addGroup(groupToAdd); + this.setState({ ...this.state, groupToAdd: "" }); + // } + }; + + handleAddGroupChange = (groupname: string) => { + this.setState({ + ...this.state, + groupToAdd: groupname, + //validationError: membername.length > 0 && !isMemberNameValid(membername) + }); + }; +} + +export default translate("config")(AddAdminGroupField); diff --git a/scm-ui/src/config/components/fields/AddAdminUserField.js b/scm-ui/src/config/components/fields/AddAdminUserField.js new file mode 100644 index 0000000000..116144ff3c --- /dev/null +++ b/scm-ui/src/config/components/fields/AddAdminUserField.js @@ -0,0 +1,71 @@ +//@flow +import React from "react"; + +import { translate } from "react-i18next"; +import { AddButton } from "../../../components/buttons"; +import InputField from "../../../components/forms/InputField"; + +type Props = { + t: string => string, + addUser: string => void +}; + +type State = { + userToAdd: string, + //validationError: boolean +}; + +class AddAdminUserField extends React.Component<Props, State> { + constructor(props) { + super(props); + this.state = { + userToAdd: "", + //validationError: false + }; + } + + render() { + const { t } = this.props; + return ( + <div className="field"> + <InputField + + label={t("admin-settings.add-user-textfield")} + errorMessage={t("admin-settings.add-user-error")} + onChange={this.handleAddUserChange} + validationError={false} + value={this.state.userToAdd} + onReturnPressed={this.appendUser} + /> + <AddButton + label={t("admin-settings.add-user-button")} + action={this.addButtonClicked} + //disabled={!isMemberNameValid(this.state.memberToAdd)} + /> + </div> + ); + } + + addButtonClicked = (event: Event) => { + event.preventDefault(); + this.appendUser(); + }; + + appendUser = () => { + const { userToAdd } = this.state; + //if (isMemberNameValid(memberToAdd)) { + this.props.addUser(userToAdd); + this.setState({ ...this.state, userToAdd: "" }); + // } + }; + + handleAddUserChange = (username: string) => { + this.setState({ + ...this.state, + userToAdd: username, + //validationError: membername.length > 0 && !isMemberNameValid(membername) + }); + }; +} + +export default translate("config")(AddAdminUserField); diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index 5079addca1..bcc6efbbfe 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -1,8 +1,12 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import { Checkbox, InputField } from "../../../components/forms/index"; import Subtitle from "../../../components/layout/Subtitle"; +import AdminGroupTable from "../table/AdminGroupTable"; +import ProxySettings from "./ProxySettings"; +import AdminUserTable from "../table/AdminUserTable"; +import AddAdminGroupField from "../fields/AddAdminGroupField"; +import AddAdminUserField from "../fields/AddAdminUserField"; type Props = { adminGroups: string[], @@ -13,17 +17,58 @@ type Props = { //TODO: Einbauen! class AdminSettings extends React.Component<Props> { render() { - const { - t, - adminGroups, - adminUsers - } = this.props; + const { t, adminGroups, adminUsers } = this.props; return ( - null + <div> + <Subtitle subtitle={t("admin-settings.name")} /> + <AdminGroupTable + adminGroups={adminGroups} + onChange={(isValid, changedValue, name) => + this.props.onChange(isValid, changedValue, name) + } + /> + <AddAdminGroupField addGroup={this.addGroup} /> + <AdminUserTable + adminUsers={adminUsers} + onChange={(isValid, changedValue, name) => + this.props.onChange(isValid, changedValue, name) + } + /> + <AddAdminUserField addUser={this.addUser} /> + </div> ); } + addGroup = (groupname: string) => { + if (this.isAdminGroupMember(groupname)) { + return; + } + this.props.onChange( + true, + [...this.props.adminGroups, groupname], + "adminGroups" + ); + }; + + isAdminGroupMember = (groupname: string) => { + return this.props.adminGroups.includes(groupname); + }; + + addUser = (username: string) => { + if (this.isAdminUserMember(username)) { + return; + } + this.props.onChange( + true, + [...this.props.adminUsers, username], + "adminUsers" + ); + }; + + isAdminUserMember = (username: string) => { + return this.props.adminUsers.includes(username); + }; } export default translate("config")(AdminSettings); diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index ac7325e6fc..ec75f3f579 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -6,12 +6,14 @@ import type { Config } from "../../types/Config"; import ProxySettings from "./ProxySettings"; import GeneralSettings from "./GeneralSettings"; import BaseUrlSettings from "./BaseUrlSettings"; +import AdminSettings from "./AdminSettings"; type Props = { submitForm: Config => void, config?: Config, loading?: boolean, - t: string => string + t: string => string, + configUpdatePermission: boolean }; type State = { @@ -85,6 +87,7 @@ class ConfigForm extends React.Component<Props, State> { this.onChange(isValid, changedValue, name) } /> + <hr /> <BaseUrlSettings baseUrl={config.baseUrl} forceBaseUrl={config.forceBaseUrl} @@ -92,6 +95,15 @@ class ConfigForm extends React.Component<Props, State> { this.onChange(isValid, changedValue, name) } /> + <hr /> + <AdminSettings + adminGroups={config.adminGroups} + adminUsers={config.adminUsers} + onChange={(isValid, changedValue, name) => + this.onChange(isValid, changedValue, name) + } + /> + <hr /> <ProxySettings proxyPassword={config.proxyPassword ? config.proxyPassword : ""} proxyPort={config.proxyPort} @@ -102,6 +114,7 @@ class ConfigForm extends React.Component<Props, State> { this.onChange(isValid, changedValue, name) } /> + <hr /> <SubmitButton // disabled={!this.isValid()} loading={loading} diff --git a/scm-ui/src/config/components/table/AdminGroupTable.js b/scm-ui/src/config/components/table/AdminGroupTable.js new file mode 100644 index 0000000000..94e54ba747 --- /dev/null +++ b/scm-ui/src/config/components/table/AdminGroupTable.js @@ -0,0 +1,47 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import RemoveAdminGroupButton from "../buttons/RemoveAdminGroupButton"; + +type Props = { + adminGroups: string[], + t: string => string, + onChange: (boolean, any, string) => void +}; + +type State = {}; + +class AdminGroupTable extends React.Component<Props, State> { + render() { + const { t } = this.props; + return ( + <div> + <label className="label">{t("admin-settings.admin-groups")}</label> + <table className="table is-hoverable is-fullwidth"> + <tbody> + {this.props.adminGroups.map(group => { + return ( + <tr key={group}> + <td key={group}>{group}</td> + <td> + <RemoveAdminGroupButton + groupname={group} + removeGroup={this.removeGroup} + /> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); + } + + removeGroup = (groupname: string) => { + const newGroups = this.props.adminGroups.filter(name => name !== groupname); + this.props.onChange(true, newGroups, "adminGroups"); + }; +} + +export default translate("config")(AdminGroupTable); diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js new file mode 100644 index 0000000000..8f1cbe11c4 --- /dev/null +++ b/scm-ui/src/config/components/table/AdminUserTable.js @@ -0,0 +1,47 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import RemoveAdminUserButton from "../buttons/RemoveAdminUserButton"; + +type Props = { + adminUsers: string[], + t: string => string, + onChange: (boolean, any, string) => void +}; + +type State = {}; + +class AdminUserTable extends React.Component<Props, State> { + render() { + const { t } = this.props; + return ( + <div> + <label className="label">{t("admin-settings.admin-users")}</label> + <table className="table is-hoverable is-fullwidth"> + <tbody> + {this.props.adminUsers.map(user => { + return ( + <tr key={user}> + <td key={user}>{user}</td> + <td> + <RemoveAdminUserButton + username={user} + removeUser={this.removeUser} + /> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); + } + + removeUser = (username: string) => { + const newUsers = this.props.adminUsers.filter(name => name !== username); + this.props.onChange(true, newUsers, "adminUsers"); + }; +} + +export default translate("config")(AdminUserTable); diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index a6da5f41bf..56e0a06c9b 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -7,7 +7,8 @@ import { isFetchConfigPending, getConfig, modifyConfig, - isModifyConfigPending + isModifyConfigPending, + getConfigUpdatePermission } from "../modules/config"; import connect from "react-redux/es/connect/connect"; import ErrorPage from "../../components/ErrorPage"; @@ -21,6 +22,7 @@ type Props = { loading: boolean, error: Error, config: Config, + configUpdatePermission: boolean, // dispatch functions modifyConfig: (config: User, callback?: () => void) => void, // context objects @@ -31,6 +33,7 @@ type Props = { class GlobalConfig extends React.Component<Props> { configModified = (config: Config) => () => { + this.props.fetchConfig(); this.props.history.push(`/config`); }; @@ -39,11 +42,12 @@ class GlobalConfig extends React.Component<Props> { } modifyConfig = (config: Config) => { + console.log(config); this.props.modifyConfig(config, this.configModified(config)); }; render() { - const { t, error, loading, config } = this.props; + const { t, error, loading, config, configUpdatePermission } = this.props; if (error) { return ( @@ -51,6 +55,7 @@ class GlobalConfig extends React.Component<Props> { title={t("global-config.error-title")} subtitle={t("global-config.error-subtitle")} error={error} + configUpdatePermission={configUpdatePermission} /> ); } @@ -86,11 +91,13 @@ const mapStateToProps = state => { const loading = isFetchConfigPending(state) || isModifyConfigPending(state); //TODO: Button lädt so nicht, sondern gesamte Seite const error = getFetchConfigFailure(state); const config = getConfig(state); + const configUpdatePermission = getConfigUpdatePermission(state); return { loading, error, - config + config, + configUpdatePermission }; }; From 998595ccd969c76529cab0f59cba218c246753a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 11:18:10 +0200 Subject: [PATCH 24/73] remove to do since it is done --- scm-ui/src/config/components/form/AdminSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index bcc6efbbfe..1486ce56af 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -14,7 +14,7 @@ type Props = { t: string => string, onChange: (boolean, any, string) => void }; -//TODO: Einbauen! + class AdminSettings extends React.Component<Props> { render() { const { t, adminGroups, adminUsers } = this.props; From 0452b95d105d2c4b3362d50ca039582493f095fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 11:18:28 +0200 Subject: [PATCH 25/73] add proxy exclude view and edit --- scm-ui/public/locales/en/config.json | 6 +- .../buttons/RemoveProxyExcludeButton.js | 34 +++++++++ .../components/fields/AddProxyExcludeField.js | 71 +++++++++++++++++++ .../src/config/components/form/ConfigForm.js | 1 + .../config/components/form/ProxySettings.js | 27 ++++++- .../components/table/ProxyExcludesTable.js | 47 ++++++++++++ 6 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js create mode 100644 scm-ui/src/config/components/fields/AddProxyExcludeField.js create mode 100644 scm-ui/src/config/components/table/ProxyExcludesTable.js diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 53d41e682d..5f1db2a549 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -18,7 +18,11 @@ "proxy-server": "Proxy Server", "proxy-user": "Proxy User", "enable-proxy": "Enable Proxy", - "proxy-excludes": "Proxy Excludes" + "proxy-excludes": "Proxy Excludes", + "remove-proxy-exclude-button": "Remove Proxy Exclude", + "add-proxy-exclude-error": "The proxy exclude you want to add is not valid", + "add-proxy-exclude-textfield": "Add proxy exclude you want to add to proxy excludes here", + "add-proxy-exclude-button": "Add Proxy Exclude" }, "base-url-settings": { "name": "Base URL Settings", diff --git a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js new file mode 100644 index 0000000000..254f8c8225 --- /dev/null +++ b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js @@ -0,0 +1,34 @@ +//@flow +import React from "react"; +import { DeleteButton } from "../../../components/buttons"; +import { translate } from "react-i18next"; +import classNames from "classnames"; + +type Props = { + t: string => string, + proxyExcludeName: string, + removeProxyExclude: string => void +}; + +type State = {}; + + + +class RemoveProxyExcludeButton extends React.Component<Props, State> { + render() { + const { t , proxyExcludeName, removeProxyExclude} = this.props; + return ( + <div className={classNames("is-pulled-right")}> + <DeleteButton + label={t("proxy-settings.remove-proxy-exclude-button")} + action={(event: Event) => { + event.preventDefault(); + removeProxyExclude(proxyExcludeName); + }} + /> + </div> + ); + } +} + +export default translate("config")(RemoveProxyExcludeButton); diff --git a/scm-ui/src/config/components/fields/AddProxyExcludeField.js b/scm-ui/src/config/components/fields/AddProxyExcludeField.js new file mode 100644 index 0000000000..a5c8b3370d --- /dev/null +++ b/scm-ui/src/config/components/fields/AddProxyExcludeField.js @@ -0,0 +1,71 @@ +//@flow +import React from "react"; + +import { translate } from "react-i18next"; +import { AddButton } from "../../../components/buttons"; +import InputField from "../../../components/forms/InputField"; + +type Props = { + t: string => string, + addProxyExclude: string => void +}; + +type State = { + proxyExcludeToAdd: string, + //validationError: boolean +}; + +class AddProxyExcludeField extends React.Component<Props, State> { + constructor(props) { + super(props); + this.state = { + proxyExcludeToAdd: "", + //validationError: false + }; + } + + render() { + const { t } = this.props; + return ( + <div className="field"> + <InputField + + label={t("proxy-settings.add-proxy-exclude-textfield")} + errorMessage={t("proxy-settings.add-proxy-exclude-error")} + onChange={this.handleAddProxyExcludeChange} + validationError={false} + value={this.state.proxyExcludeToAdd} + onReturnPressed={this.appendProxyExclude} + /> + <AddButton + label={t("proxy-settings.add-proxy-exclude-button")} + action={this.addButtonClicked} + //disabled={!isMemberNameValid(this.state.memberToAdd)} + /> + </div> + ); + } + + addButtonClicked = (event: Event) => { + event.preventDefault(); + this.appendProxyExclude(); + }; + + appendProxyExclude = () => { + const { proxyExcludeToAdd } = this.state; + //if (isMemberNameValid(memberToAdd)) { + this.props.addProxyExclude(proxyExcludeToAdd); + this.setState({ ...this.state, proxyExcludeToAdd: "" }); + // } + }; + + handleAddProxyExcludeChange = (username: string) => { + this.setState({ + ...this.state, + proxyExcludeToAdd: username, + //validationError: membername.length > 0 && !isMemberNameValid(membername) + }); + }; +} + +export default translate("config")(AddProxyExcludeField); diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index ec75f3f579..4d05a816a4 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -110,6 +110,7 @@ class ConfigForm extends React.Component<Props, State> { proxyServer={config.proxyServer ? config.proxyServer : ""} proxyUser={config.proxyUser ? config.proxyUser : ""} enableProxy={config.enableProxy} + proxyExcludes={config.proxyExcludes} onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index 2938c08a71..9904c739c0 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -3,6 +3,8 @@ import React from "react"; import { translate } from "react-i18next"; import { Checkbox, InputField } from "../../../components/forms/index"; import Subtitle from "../../../components/layout/Subtitle"; +import ProxyExcludesTable from "../table/ProxyExcludesTable"; +import AddProxyExcludeField from "../fields/AddProxyExcludeField"; type Props = { proxyPassword: string, @@ -23,7 +25,8 @@ class ProxySettings extends React.Component<Props> { proxyPort, proxyServer, proxyUser, - enableProxy + enableProxy, + proxyExcludes } = this.props; return ( @@ -58,6 +61,13 @@ class ProxySettings extends React.Component<Props> { onChange={this.handleProxyUserChange} disable={!enableProxy} /> + <ProxyExcludesTable + proxyExcludes={proxyExcludes} + onChange={(isValid, changedValue, name) => + this.props.onChange(isValid, changedValue, name) + } + /> + <AddProxyExcludeField addProxyExclude={this.addProxyExclude} /> </div> ); } @@ -77,6 +87,21 @@ class ProxySettings extends React.Component<Props> { handleEnableProxyChange = (value: string) => { this.props.onChange(true, value, "enableProxy"); }; + + addProxyExclude = (proxyExcludeName: string) => { + if (this.isProxyExcludeMember(proxyExcludeName)) { + return; + } + this.props.onChange( + true, + [...this.props.proxyExcludes, proxyExcludeName], + "proxyExcludes" + ); + }; + + isProxyExcludeMember = (proxyExcludeName: string) => { + return this.props.proxyExcludes.includes(proxyExcludeName); + }; } export default translate("config")(ProxySettings); diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js new file mode 100644 index 0000000000..04f3126425 --- /dev/null +++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js @@ -0,0 +1,47 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import RemoveProxyExcludeButton from "../buttons/RemoveProxyExcludeButton"; + +type Props = { + proxyExcludes: string[], + t: string => string, + onChange: (boolean, any, string) => void +}; + +type State = {}; + +class ProxyExcludesTable extends React.Component<Props, State> { + render() { + const { t } = this.props; + return ( + <div> + <label className="label">{t("proxy-settings.proxy-excludes")}</label> + <table className="table is-hoverable is-fullwidth"> + <tbody> + {this.props.proxyExcludes.map(excludes => { + return ( + <tr key={excludes}> + <td key={excludes}>{excludes}</td> + <td> + <RemoveProxyExcludeButton + proxyExcludeName={excludes} + removeProxyExclude={this.removeProxyExclude} + /> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); + } + + removeProxyExclude = (excludename: string) => { + const newExcludes = this.props.proxyExcludes.filter(name => name !== excludename); + this.props.onChange(true, newExcludes, "proxyExcludes"); + }; +} + +export default translate("config")(ProxyExcludesTable); From d2c85606090c4d1d826636316a3705f6e834b313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 11:26:45 +0200 Subject: [PATCH 26/73] remove unsued imports and disable editing if proxy is disabled --- .../buttons/RemoveProxyExcludeButton.js | 6 ++-- .../components/fields/AddProxyExcludeField.js | 7 ++-- .../config/components/form/AdminSettings.js | 1 - .../config/components/form/ProxySettings.js | 3 +- .../components/table/ProxyExcludesTable.js | 34 +++++++++++-------- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js index 254f8c8225..ba177024fe 100644 --- a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js +++ b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js @@ -1,13 +1,14 @@ //@flow import React from "react"; -import { DeleteButton } from "../../../components/buttons"; +import {DeleteButton} from "../../../components/buttons"; import { translate } from "react-i18next"; import classNames from "classnames"; type Props = { t: string => string, proxyExcludeName: string, - removeProxyExclude: string => void + removeProxyExclude: string => void, + disable: boolean }; type State = {}; @@ -25,6 +26,7 @@ class RemoveProxyExcludeButton extends React.Component<Props, State> { event.preventDefault(); removeProxyExclude(proxyExcludeName); }} + disabled={this.props.disable} /> </div> ); diff --git a/scm-ui/src/config/components/fields/AddProxyExcludeField.js b/scm-ui/src/config/components/fields/AddProxyExcludeField.js index a5c8b3370d..c76d8ae807 100644 --- a/scm-ui/src/config/components/fields/AddProxyExcludeField.js +++ b/scm-ui/src/config/components/fields/AddProxyExcludeField.js @@ -2,12 +2,13 @@ import React from "react"; import { translate } from "react-i18next"; -import { AddButton } from "../../../components/buttons"; +import {AddButton} from "../../../components/buttons"; import InputField from "../../../components/forms/InputField"; type Props = { t: string => string, - addProxyExclude: string => void + addProxyExclude: string => void, + disable: boolean }; type State = { @@ -36,10 +37,12 @@ class AddProxyExcludeField extends React.Component<Props, State> { validationError={false} value={this.state.proxyExcludeToAdd} onReturnPressed={this.appendProxyExclude} + disable={this.props.disable} /> <AddButton label={t("proxy-settings.add-proxy-exclude-button")} action={this.addButtonClicked} + disabled={this.props.disable} //disabled={!isMemberNameValid(this.state.memberToAdd)} /> </div> diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index 1486ce56af..dcde12f91f 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -3,7 +3,6 @@ import React from "react"; import { translate } from "react-i18next"; import Subtitle from "../../../components/layout/Subtitle"; import AdminGroupTable from "../table/AdminGroupTable"; -import ProxySettings from "./ProxySettings"; import AdminUserTable from "../table/AdminUserTable"; import AddAdminGroupField from "../fields/AddAdminGroupField"; import AddAdminUserField from "../fields/AddAdminUserField"; diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index 9904c739c0..0a8666f805 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -66,8 +66,9 @@ class ProxySettings extends React.Component<Props> { onChange={(isValid, changedValue, name) => this.props.onChange(isValid, changedValue, name) } + disable={!enableProxy} /> - <AddProxyExcludeField addProxyExclude={this.addProxyExclude} /> + <AddProxyExcludeField addProxyExclude={this.addProxyExclude} disable={!enableProxy}/> </div> ); } diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js index 04f3126425..b8102c4f63 100644 --- a/scm-ui/src/config/components/table/ProxyExcludesTable.js +++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js @@ -6,7 +6,8 @@ import RemoveProxyExcludeButton from "../buttons/RemoveProxyExcludeButton"; type Props = { proxyExcludes: string[], t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + disable: boolean }; type State = {}; @@ -19,19 +20,20 @@ class ProxyExcludesTable extends React.Component<Props, State> { <label className="label">{t("proxy-settings.proxy-excludes")}</label> <table className="table is-hoverable is-fullwidth"> <tbody> - {this.props.proxyExcludes.map(excludes => { - return ( - <tr key={excludes}> - <td key={excludes}>{excludes}</td> - <td> - <RemoveProxyExcludeButton - proxyExcludeName={excludes} - removeProxyExclude={this.removeProxyExclude} - /> - </td> - </tr> - ); - })} + {this.props.proxyExcludes.map(excludes => { + return ( + <tr key={excludes}> + <td key={excludes}>{excludes}</td> + <td> + <RemoveProxyExcludeButton + proxyExcludeName={excludes} + removeProxyExclude={this.removeProxyExclude} + disable={this.props.disable} + /> + </td> + </tr> + ); + })} </tbody> </table> </div> @@ -39,7 +41,9 @@ class ProxyExcludesTable extends React.Component<Props, State> { } removeProxyExclude = (excludename: string) => { - const newExcludes = this.props.proxyExcludes.filter(name => name !== excludename); + const newExcludes = this.props.proxyExcludes.filter( + name => name !== excludename + ); this.props.onChange(true, newExcludes, "proxyExcludes"); }; } From fbc3b458317239bf66fc8e11c29f82579e1ed39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 11:37:21 +0200 Subject: [PATCH 27/73] add error handling if modify config failed --- scm-ui/src/config/containers/GlobalConfig.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index 56e0a06c9b..5d18f86226 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -8,7 +8,8 @@ import { getConfig, modifyConfig, isModifyConfigPending, - getConfigUpdatePermission + getConfigUpdatePermission, + getModifyConfigFailure } from "../modules/config"; import connect from "react-redux/es/connect/connect"; import ErrorPage from "../../components/ErrorPage"; @@ -89,7 +90,7 @@ const mapDispatchToProps = dispatch => { const mapStateToProps = state => { const loading = isFetchConfigPending(state) || isModifyConfigPending(state); //TODO: Button lädt so nicht, sondern gesamte Seite - const error = getFetchConfigFailure(state); + const error = getFetchConfigFailure(state) || getModifyConfigFailure(state); const config = getConfig(state); const configUpdatePermission = getConfigUpdatePermission(state); From 08328bb95f1998dced1ec295f26055929f3490f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 12:12:59 +0200 Subject: [PATCH 28/73] disable everything if user has no permission to edit config --- scm-ui/src/components/forms/Checkbox.js | 6 +++-- scm-ui/src/components/forms/InputField.js | 6 ++--- .../buttons/RemoveAdminGroupButton.js | 7 ++++-- .../buttons/RemoveAdminUserButton.js | 9 ++++---- .../buttons/RemoveProxyExcludeButton.js | 4 ++-- .../components/fields/AddAdminGroupField.js | 7 ++++-- .../components/fields/AddAdminUserField.js | 14 +++++++----- .../components/fields/AddProxyExcludeField.js | 15 ++++++------- .../config/components/form/AdminSettings.js | 13 +++++++---- .../config/components/form/BaseUrlSettings.js | 7 ++++-- .../src/config/components/form/ConfigForm.js | 8 +++++-- .../config/components/form/GeneralSettings.js | 17 ++++++++++++-- .../config/components/form/ProxySettings.js | 22 ++++++++++++------- .../components/table/AdminGroupTable.js | 6 +++-- .../config/components/table/AdminUserTable.js | 7 ++++-- .../components/table/ProxyExcludesTable.js | 4 ++-- scm-ui/src/config/containers/GlobalConfig.js | 1 + 17 files changed, 100 insertions(+), 53 deletions(-) diff --git a/scm-ui/src/components/forms/Checkbox.js b/scm-ui/src/components/forms/Checkbox.js index b37f951e2b..72dfbf4af8 100644 --- a/scm-ui/src/components/forms/Checkbox.js +++ b/scm-ui/src/components/forms/Checkbox.js @@ -4,7 +4,8 @@ import React from "react"; type Props = { label?: string, checked: boolean, - onChange?: boolean => void + onChange?: boolean => void, + disabled?: boolean }; class Checkbox extends React.Component<Props> { onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => { @@ -17,11 +18,12 @@ class Checkbox extends React.Component<Props> { return ( <div className="field"> <div className="control"> - <label className="checkbox"> + <label className="checkbox" disabled={this.props.disabled}> <input type="checkbox" checked={this.props.checked} onChange={this.onCheckboxChange} + disabled={this.props.disabled} /> {this.props.label} </label> diff --git a/scm-ui/src/components/forms/InputField.js b/scm-ui/src/components/forms/InputField.js index 9833739461..6f87683939 100644 --- a/scm-ui/src/components/forms/InputField.js +++ b/scm-ui/src/components/forms/InputField.js @@ -12,7 +12,7 @@ type Props = { onReturnPressed?: () => void, validationError: boolean, errorMessage: string, - disable?: boolean + disabled?: boolean }; class InputField extends React.Component<Props> { @@ -60,7 +60,7 @@ class InputField extends React.Component<Props> { value, validationError, errorMessage, - disable + disabled } = this.props; const errorView = validationError ? "is-danger" : ""; const helper = validationError ? ( @@ -82,7 +82,7 @@ class InputField extends React.Component<Props> { value={value} onChange={this.handleInput} onKeyPress={this.handleKeyPress} - disabled={disable} + disabled={disabled} /> </div> {helper} diff --git a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js index c7d8f8d85a..c593483409 100644 --- a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js +++ b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js @@ -3,11 +3,13 @@ import React from "react"; import { DeleteButton } from "../../../components/buttons"; import { translate } from "react-i18next"; import classNames from "classnames"; +import {InputField} from "../../../components/forms"; type Props = { t: string => string, groupname: string, - removeGroup: string => void + removeGroup: string => void, + disabled: boolean }; type State = {}; @@ -16,7 +18,7 @@ type State = {}; class RemoveAdminGroupButton extends React.Component<Props, State> { render() { - const { t , groupname, removeGroup} = this.props; + const { t , groupname, removeGroup, disabled} = this.props; return ( <div className={classNames("is-pulled-right")}> <DeleteButton @@ -25,6 +27,7 @@ class RemoveAdminGroupButton extends React.Component<Props, State> { event.preventDefault(); removeGroup(groupname); }} + disabled={disabled} /> </div> ); diff --git a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js index 6bdf06cd28..bd4a02a18a 100644 --- a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js +++ b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js @@ -3,20 +3,20 @@ import React from "react"; import { DeleteButton } from "../../../components/buttons"; import { translate } from "react-i18next"; import classNames from "classnames"; +import { InputField } from "../../../components/forms"; type Props = { t: string => string, username: string, - removeUser: string => void + removeUser: string => void, + disabled: boolean }; type State = {}; - - class RemoveAdminUserButton extends React.Component<Props, State> { render() { - const { t , username, removeUser} = this.props; + const { t, username, removeUser, disabled } = this.props; return ( <div className={classNames("is-pulled-right")}> <DeleteButton @@ -25,6 +25,7 @@ class RemoveAdminUserButton extends React.Component<Props, State> { event.preventDefault(); removeUser(username); }} + disabled={disabled} /> </div> ); diff --git a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js index ba177024fe..acdfd6292a 100644 --- a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js +++ b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js @@ -8,7 +8,7 @@ type Props = { t: string => string, proxyExcludeName: string, removeProxyExclude: string => void, - disable: boolean + disabled: boolean }; type State = {}; @@ -26,7 +26,7 @@ class RemoveProxyExcludeButton extends React.Component<Props, State> { event.preventDefault(); removeProxyExclude(proxyExcludeName); }} - disabled={this.props.disable} + disabled={this.props.disabled} /> </div> ); diff --git a/scm-ui/src/config/components/fields/AddAdminGroupField.js b/scm-ui/src/config/components/fields/AddAdminGroupField.js index ed694355d8..0991056b66 100644 --- a/scm-ui/src/config/components/fields/AddAdminGroupField.js +++ b/scm-ui/src/config/components/fields/AddAdminGroupField.js @@ -7,7 +7,8 @@ import InputField from "../../../components/forms/InputField"; type Props = { t: string => string, - addGroup: string => void + addGroup: string => void, + disabled: boolean }; type State = { @@ -25,7 +26,7 @@ class AddAdminGroupField extends React.Component<Props, State> { } render() { - const { t } = this.props; + const { t, disabled } = this.props; return ( <div className="field"> <InputField @@ -36,10 +37,12 @@ class AddAdminGroupField extends React.Component<Props, State> { validationError={false} value={this.state.groupToAdd} onReturnPressed={this.appendGroup} + disabled={disabled} /> <AddButton label={t("admin-settings.add-group-button")} action={this.addButtonClicked} + disabled={disabled} //disabled={!isMemberNameValid(this.state.memberToAdd)} /> </div> diff --git a/scm-ui/src/config/components/fields/AddAdminUserField.js b/scm-ui/src/config/components/fields/AddAdminUserField.js index 116144ff3c..025ed6cb4d 100644 --- a/scm-ui/src/config/components/fields/AddAdminUserField.js +++ b/scm-ui/src/config/components/fields/AddAdminUserField.js @@ -7,11 +7,12 @@ import InputField from "../../../components/forms/InputField"; type Props = { t: string => string, - addUser: string => void + addUser: string => void, + disabled: boolean }; type State = { - userToAdd: string, + userToAdd: string //validationError: boolean }; @@ -19,27 +20,28 @@ class AddAdminUserField extends React.Component<Props, State> { constructor(props) { super(props); this.state = { - userToAdd: "", + userToAdd: "" //validationError: false }; } render() { - const { t } = this.props; + const { t, disabled } = this.props; return ( <div className="field"> <InputField - label={t("admin-settings.add-user-textfield")} errorMessage={t("admin-settings.add-user-error")} onChange={this.handleAddUserChange} validationError={false} value={this.state.userToAdd} onReturnPressed={this.appendUser} + disabled={disabled} /> <AddButton label={t("admin-settings.add-user-button")} action={this.addButtonClicked} + disabled={disabled} //disabled={!isMemberNameValid(this.state.memberToAdd)} /> </div> @@ -62,7 +64,7 @@ class AddAdminUserField extends React.Component<Props, State> { handleAddUserChange = (username: string) => { this.setState({ ...this.state, - userToAdd: username, + userToAdd: username //validationError: membername.length > 0 && !isMemberNameValid(membername) }); }; diff --git a/scm-ui/src/config/components/fields/AddProxyExcludeField.js b/scm-ui/src/config/components/fields/AddProxyExcludeField.js index c76d8ae807..bb7e005b02 100644 --- a/scm-ui/src/config/components/fields/AddProxyExcludeField.js +++ b/scm-ui/src/config/components/fields/AddProxyExcludeField.js @@ -2,17 +2,17 @@ import React from "react"; import { translate } from "react-i18next"; -import {AddButton} from "../../../components/buttons"; +import { AddButton } from "../../../components/buttons"; import InputField from "../../../components/forms/InputField"; type Props = { t: string => string, addProxyExclude: string => void, - disable: boolean + disabled: boolean }; type State = { - proxyExcludeToAdd: string, + proxyExcludeToAdd: string //validationError: boolean }; @@ -20,7 +20,7 @@ class AddProxyExcludeField extends React.Component<Props, State> { constructor(props) { super(props); this.state = { - proxyExcludeToAdd: "", + proxyExcludeToAdd: "" //validationError: false }; } @@ -30,19 +30,18 @@ class AddProxyExcludeField extends React.Component<Props, State> { return ( <div className="field"> <InputField - label={t("proxy-settings.add-proxy-exclude-textfield")} errorMessage={t("proxy-settings.add-proxy-exclude-error")} onChange={this.handleAddProxyExcludeChange} validationError={false} value={this.state.proxyExcludeToAdd} onReturnPressed={this.appendProxyExclude} - disable={this.props.disable} + disabled={this.props.disabled} /> <AddButton label={t("proxy-settings.add-proxy-exclude-button")} action={this.addButtonClicked} - disabled={this.props.disable} + disabled={this.props.disabled} //disabled={!isMemberNameValid(this.state.memberToAdd)} /> </div> @@ -65,7 +64,7 @@ class AddProxyExcludeField extends React.Component<Props, State> { handleAddProxyExcludeChange = (username: string) => { this.setState({ ...this.state, - proxyExcludeToAdd: username, + proxyExcludeToAdd: username //validationError: membername.length > 0 && !isMemberNameValid(membername) }); }; diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index dcde12f91f..b7a39f234c 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -11,12 +11,13 @@ type Props = { adminGroups: string[], adminUsers: string[], t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + hasUpdatePermission: boolean }; class AdminSettings extends React.Component<Props> { render() { - const { t, adminGroups, adminUsers } = this.props; + const { t, adminGroups, adminUsers, hasUpdatePermission } = this.props; return ( <div> @@ -26,15 +27,19 @@ class AdminSettings extends React.Component<Props> { onChange={(isValid, changedValue, name) => this.props.onChange(isValid, changedValue, name) } + disabled={!hasUpdatePermission} + /> + <AddAdminGroupField addGroup={this.addGroup} disabled={!hasUpdatePermission} /> - <AddAdminGroupField addGroup={this.addGroup} /> <AdminUserTable adminUsers={adminUsers} onChange={(isValid, changedValue, name) => this.props.onChange(isValid, changedValue, name) } + disabled={!hasUpdatePermission} + /> + <AddAdminUserField addUser={this.addUser} disabled={!hasUpdatePermission} /> - <AddAdminUserField addUser={this.addUser} /> </div> ); } diff --git a/scm-ui/src/config/components/form/BaseUrlSettings.js b/scm-ui/src/config/components/form/BaseUrlSettings.js index cde4680b48..e7b80ed924 100644 --- a/scm-ui/src/config/components/form/BaseUrlSettings.js +++ b/scm-ui/src/config/components/form/BaseUrlSettings.js @@ -8,12 +8,13 @@ type Props = { baseUrl: string, forceBaseUrl: boolean, t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + hasUpdatePermission: boolean }; class BaseUrlSettings extends React.Component<Props> { render() { - const { t, baseUrl, forceBaseUrl } = this.props; + const { t, baseUrl, forceBaseUrl, hasUpdatePermission } = this.props; return ( <div> @@ -22,11 +23,13 @@ class BaseUrlSettings extends React.Component<Props> { checked={forceBaseUrl} label={t("base-url-settings.force-base-url")} onChange={this.handleForceBaseUrlChange} + disabled={!hasUpdatePermission} /> <InputField label={t("base-url-settings.base-url")} onChange={this.handleBaseUrlChange} value={baseUrl} + disabled={!hasUpdatePermission} /> </div> ); diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index 4d05a816a4..db0a79e7bf 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -66,9 +66,8 @@ class ConfigForm extends React.Component<Props, State> { }; render() { - const { loading, t } = this.props; + const { loading, t, configUpdatePermission } = this.props; let config = this.state.config; - return ( <form onSubmit={this.submit}> <GeneralSettings @@ -86,6 +85,7 @@ class ConfigForm extends React.Component<Props, State> { onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } + hasUpdatePermission={configUpdatePermission} /> <hr /> <BaseUrlSettings @@ -94,6 +94,7 @@ class ConfigForm extends React.Component<Props, State> { onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } + hasUpdatePermission={configUpdatePermission} /> <hr /> <AdminSettings @@ -102,6 +103,7 @@ class ConfigForm extends React.Component<Props, State> { onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } + hasUpdatePermission={configUpdatePermission} /> <hr /> <ProxySettings @@ -114,12 +116,14 @@ class ConfigForm extends React.Component<Props, State> { onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } + hasUpdatePermission={configUpdatePermission} /> <hr /> <SubmitButton // disabled={!this.isValid()} loading={loading} label={t("config-form.submit")} + disabled={!configUpdatePermission} /> </form> ); diff --git a/scm-ui/src/config/components/form/GeneralSettings.js b/scm-ui/src/config/components/form/GeneralSettings.js index 29db1e5288..321f247a2a 100644 --- a/scm-ui/src/config/components/form/GeneralSettings.js +++ b/scm-ui/src/config/components/form/GeneralSettings.js @@ -16,7 +16,8 @@ type Props = { enabledXsrfProtection: boolean, defaultNamespaceStrategy: string, t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + hasUpdatePermission: boolean }; class GeneralSettings extends React.Component<Props> { @@ -33,7 +34,8 @@ class GeneralSettings extends React.Component<Props> { pluginUrl, loginAttemptLimitTimeout, enabledXsrfProtection, - defaultNamespaceStrategy + defaultNamespaceStrategy, + hasUpdatePermission } = this.props; return ( @@ -42,56 +44,67 @@ class GeneralSettings extends React.Component<Props> { label={t("general-settings.realm-description")} onChange={this.handleRealmDescriptionChange} value={realmDescription} + disabled={!hasUpdatePermission} /> <Checkbox checked={enableRepositoryArchive} label={t("general-settings.enable-repository-archive")} onChange={this.handleEnableRepositoryArchiveChange} + disabled={!hasUpdatePermission} /> <Checkbox checked={disableGroupingGrid} label={t("general-settings.disable-grouping-grid")} onChange={this.handleDisableGroupingGridChange} + disabled={!hasUpdatePermission} /> <InputField label={t("general-settings.date-format")} onChange={this.handleDateFormatChange} value={dateFormat} + disabled={!hasUpdatePermission} /> <Checkbox checked={anonymousAccessEnabled} label={t("general-settings.anonymous-access-enabled")} onChange={this.handleAnonymousAccessEnabledChange} + disabled={!hasUpdatePermission} /> <InputField label={t("general-settings.login-attempt-limit")} onChange={this.handleLoginAttemptLimitChange} value={loginAttemptLimit} + disabled={!hasUpdatePermission} /> <InputField label={t("general-settings.login-attempt-limit-timeout")} onChange={this.handleLoginAttemptLimitTimeoutChange} value={loginAttemptLimitTimeout} + disabled={!hasUpdatePermission} /> <Checkbox checked={skipFailedAuthenticators} label={t("general-settings.skip-failed-authenticators")} onChange={this.handleSkipFailedAuthenticatorsChange} + disabled={!hasUpdatePermission} /> <InputField label={t("general-settings.plugin-url")} onChange={this.handlePluginUrlChange} value={pluginUrl} + disabled={!hasUpdatePermission} /> <Checkbox checked={enabledXsrfProtection} label={t("general-settings.enabled-xsrf-protection")} onChange={this.handleEnabledXsrfProtectionChange} + disabled={!hasUpdatePermission} /> <InputField label={t("general-settings.default-namespace-strategy")} onChange={this.handleDefaultNamespaceStrategyChange} value={defaultNamespaceStrategy} + disabled={!hasUpdatePermission} /> </div> ); diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index 0a8666f805..c21d4e807f 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -14,7 +14,8 @@ type Props = { enableProxy: boolean, proxyExcludes: string[], //TODO: einbauen! t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + hasUpdatePermission: boolean }; class ProxySettings extends React.Component<Props> { @@ -26,7 +27,8 @@ class ProxySettings extends React.Component<Props> { proxyServer, proxyUser, enableProxy, - proxyExcludes + proxyExcludes, + hasUpdatePermission } = this.props; return ( @@ -36,39 +38,43 @@ class ProxySettings extends React.Component<Props> { checked={enableProxy} label={t("proxy-settings.enable-proxy")} onChange={this.handleEnableProxyChange} + disabled={!hasUpdatePermission} /> <InputField label={t("proxy-settings.proxy-password")} onChange={this.handleProxyPasswordChange} value={proxyPassword} - disable={!enableProxy} + disabled={!enableProxy || !hasUpdatePermission} /> <InputField label={t("proxy-settings.proxy-port")} value={proxyPort} onChange={this.handleProxyPortChange} - disable={!enableProxy} + disabled={!enableProxy || !hasUpdatePermission} /> <InputField label={t("proxy-settings.proxy-server")} value={proxyServer} onChange={this.handleProxyServerChange} - disable={!enableProxy} + disabled={!enableProxy || !hasUpdatePermission} /> <InputField label={t("proxy-settings.proxy-user")} value={proxyUser} onChange={this.handleProxyUserChange} - disable={!enableProxy} + disabled={!enableProxy || !hasUpdatePermission} /> <ProxyExcludesTable proxyExcludes={proxyExcludes} onChange={(isValid, changedValue, name) => this.props.onChange(isValid, changedValue, name) } - disable={!enableProxy} + disabled={!enableProxy || !hasUpdatePermission} + /> + <AddProxyExcludeField + addProxyExclude={this.addProxyExclude} + disabled={!enableProxy || !hasUpdatePermission} /> - <AddProxyExcludeField addProxyExclude={this.addProxyExclude} disable={!enableProxy}/> </div> ); } diff --git a/scm-ui/src/config/components/table/AdminGroupTable.js b/scm-ui/src/config/components/table/AdminGroupTable.js index 94e54ba747..d5566e7ce1 100644 --- a/scm-ui/src/config/components/table/AdminGroupTable.js +++ b/scm-ui/src/config/components/table/AdminGroupTable.js @@ -6,14 +6,15 @@ import RemoveAdminGroupButton from "../buttons/RemoveAdminGroupButton"; type Props = { adminGroups: string[], t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + disabled: boolean }; type State = {}; class AdminGroupTable extends React.Component<Props, State> { render() { - const { t } = this.props; + const { t, disabled } = this.props; return ( <div> <label className="label">{t("admin-settings.admin-groups")}</label> @@ -27,6 +28,7 @@ class AdminGroupTable extends React.Component<Props, State> { <RemoveAdminGroupButton groupname={group} removeGroup={this.removeGroup} + disabled={disabled} /> </td> </tr> diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js index 8f1cbe11c4..9a840351e0 100644 --- a/scm-ui/src/config/components/table/AdminUserTable.js +++ b/scm-ui/src/config/components/table/AdminUserTable.js @@ -2,18 +2,20 @@ import React from "react"; import { translate } from "react-i18next"; import RemoveAdminUserButton from "../buttons/RemoveAdminUserButton"; +import { InputField } from "../../../components/forms"; type Props = { adminUsers: string[], t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + disabled: boolean }; type State = {}; class AdminUserTable extends React.Component<Props, State> { render() { - const { t } = this.props; + const { t, disabled } = this.props; return ( <div> <label className="label">{t("admin-settings.admin-users")}</label> @@ -27,6 +29,7 @@ class AdminUserTable extends React.Component<Props, State> { <RemoveAdminUserButton username={user} removeUser={this.removeUser} + disabled={disabled} /> </td> </tr> diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js index b8102c4f63..ccee16cea5 100644 --- a/scm-ui/src/config/components/table/ProxyExcludesTable.js +++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js @@ -7,7 +7,7 @@ type Props = { proxyExcludes: string[], t: string => string, onChange: (boolean, any, string) => void, - disable: boolean + disabled: boolean }; type State = {}; @@ -28,7 +28,7 @@ class ProxyExcludesTable extends React.Component<Props, State> { <RemoveProxyExcludeButton proxyExcludeName={excludes} removeProxyExclude={this.removeProxyExclude} - disable={this.props.disable} + disabled={this.props.disabled} /> </td> </tr> diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index 5d18f86226..f76a71c11b 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -71,6 +71,7 @@ class GlobalConfig extends React.Component<Props> { submitForm={config => this.modifyConfig(config)} config={config} loading={loading} + configUpdatePermission={configUpdatePermission} /> </div> ); From d9aa04e6206a39e76d135dc3b76163d9ecc8a26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 13:28:46 +0200 Subject: [PATCH 29/73] add notification message if user has no permission to edit config --- scm-ui/public/locales/en/config.json | 3 +- .../src/config/components/form/ConfigForm.js | 39 ++++++++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 5f1db2a549..2779b3c28c 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -9,7 +9,8 @@ "error-subtitle": "Unknown Config Error" }, "config-form": { - "submit": "Submit" + "submit": "Submit", + "no-permission-notification": "Please note: You do not have the permission to edit the config!" }, "proxy-settings": { "name": "Proxy Settings", diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index db0a79e7bf..bc4a94b501 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -7,6 +7,7 @@ import ProxySettings from "./ProxySettings"; import GeneralSettings from "./GeneralSettings"; import BaseUrlSettings from "./BaseUrlSettings"; import AdminSettings from "./AdminSettings"; +import Notification from "../../../components/Notification"; type Props = { submitForm: Config => void, @@ -17,7 +18,8 @@ type Props = { }; type State = { - config: Config + config: Config, + showNotification: boolean }; class ConfigForm extends React.Component<Props, State> { @@ -48,15 +50,18 @@ class ConfigForm extends React.Component<Props, State> { enabledXsrfProtection: true, defaultNamespaceStrategy: "", _links: {} - } + }, + showNotification: false }; } componentDidMount() { - const { config } = this.props; - console.log(config); + const { config, configUpdatePermission } = this.props; if (config) { - this.setState({ config: { ...config } }); + this.setState({ ...this.state, config: { ...config } }); + } + if (!configUpdatePermission) { + this.setState({ ...this.state, showNotification: true }); } } @@ -67,9 +72,23 @@ class ConfigForm extends React.Component<Props, State> { render() { const { loading, t, configUpdatePermission } = this.props; - let config = this.state.config; + const config = this.state.config; + + let noPermissionNotification = null; + + if (this.state.showNotification) { + noPermissionNotification = ( + <Notification + type={"info"} + children={t("config-form.no-permission-notification")} + onClose={() => this.onClose()} + /> + ); + } + return ( <form onSubmit={this.submit}> + {noPermissionNotification} <GeneralSettings realmDescription={config.realmDescription} enableRepositoryArchive={config.enableRepositoryArchive} @@ -132,6 +151,7 @@ class ConfigForm extends React.Component<Props, State> { onChange = (isValid: boolean, changedValue: any, name: string) => { if (isValid) { this.setState({ + ...this.state, config: { ...this.state.config, [name]: changedValue @@ -139,6 +159,13 @@ class ConfigForm extends React.Component<Props, State> { }); } }; + + onClose = () => { + this.setState({ + ...this.state, + showNotification: false + }); + }; } export default translate("config")(ConfigForm); From ce700cde1376bd197ca476e6fd5bd0978b75c3da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 13:30:25 +0200 Subject: [PATCH 30/73] remove unneccesary imports --- .../src/config/components/buttons/RemoveAdminGroupButton.js | 5 +---- .../src/config/components/buttons/RemoveAdminUserButton.js | 1 - scm-ui/src/config/components/table/AdminUserTable.js | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js index c593483409..6a819b6972 100644 --- a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js +++ b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js @@ -3,7 +3,6 @@ import React from "react"; import { DeleteButton } from "../../../components/buttons"; import { translate } from "react-i18next"; import classNames from "classnames"; -import {InputField} from "../../../components/forms"; type Props = { t: string => string, @@ -14,11 +13,9 @@ type Props = { type State = {}; - - class RemoveAdminGroupButton extends React.Component<Props, State> { render() { - const { t , groupname, removeGroup, disabled} = this.props; + const { t, groupname, removeGroup, disabled } = this.props; return ( <div className={classNames("is-pulled-right")}> <DeleteButton diff --git a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js index bd4a02a18a..7d79a84030 100644 --- a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js +++ b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js @@ -3,7 +3,6 @@ import React from "react"; import { DeleteButton } from "../../../components/buttons"; import { translate } from "react-i18next"; import classNames from "classnames"; -import { InputField } from "../../../components/forms"; type Props = { t: string => string, diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js index 9a840351e0..caf58f2cc0 100644 --- a/scm-ui/src/config/components/table/AdminUserTable.js +++ b/scm-ui/src/config/components/table/AdminUserTable.js @@ -2,7 +2,6 @@ import React from "react"; import { translate } from "react-i18next"; import RemoveAdminUserButton from "../buttons/RemoveAdminUserButton"; -import { InputField } from "../../../components/forms"; type Props = { adminUsers: string[], From 756da5db1cd67156629dd5f3b387337828227d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 13:31:04 +0200 Subject: [PATCH 31/73] remove not needed to do --- scm-ui/src/config/containers/GlobalConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index f76a71c11b..a23ef047a6 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -90,7 +90,7 @@ const mapDispatchToProps = dispatch => { }; const mapStateToProps = state => { - const loading = isFetchConfigPending(state) || isModifyConfigPending(state); //TODO: Button lädt so nicht, sondern gesamte Seite + const loading = isFetchConfigPending(state) || isModifyConfigPending(state); const error = getFetchConfigFailure(state) || getModifyConfigFailure(state); const config = getConfig(state); const configUpdatePermission = getConfigUpdatePermission(state); From 322e82380abb66ca7a7fcd21efed3d905c78b8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 13:33:50 +0200 Subject: [PATCH 32/73] refactoring --- scm-ui/src/config/containers/GlobalConfig.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index a23ef047a6..2f4aa3be22 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -16,7 +16,6 @@ import ErrorPage from "../../components/ErrorPage"; import type { Config } from "../types/Config"; import ConfigForm from "../components/form/ConfigForm"; import Loading from "../../components/Loading"; -import type { User } from "../../users/types/User"; import type { History } from "history"; type Props = { @@ -25,7 +24,7 @@ type Props = { config: Config, configUpdatePermission: boolean, // dispatch functions - modifyConfig: (config: User, callback?: () => void) => void, + modifyConfig: (config: Config, callback?: () => void) => void, // context objects t: string => string, fetchConfig: void => void, @@ -33,7 +32,7 @@ type Props = { }; class GlobalConfig extends React.Component<Props> { - configModified = (config: Config) => () => { + configModified = () => () => { this.props.fetchConfig(); this.props.history.push(`/config`); }; @@ -43,8 +42,7 @@ class GlobalConfig extends React.Component<Props> { } modifyConfig = (config: Config) => { - console.log(config); - this.props.modifyConfig(config, this.configModified(config)); + this.props.modifyConfig(config, this.configModified()); }; render() { From fb56cf4d912c7f588f98089f92e36612ac7ef660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 14:04:27 +0200 Subject: [PATCH 33/73] use password field for proxy password --- scm-ui/src/config/components/form/ProxySettings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index c21d4e807f..eb434513fe 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -44,6 +44,7 @@ class ProxySettings extends React.Component<Props> { label={t("proxy-settings.proxy-password")} onChange={this.handleProxyPasswordChange} value={proxyPassword} + type="password" disabled={!enableProxy || !hasUpdatePermission} /> <InputField From fb28677a61489e90a74942ed3099f4aac2d8a2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 16:05:24 +0200 Subject: [PATCH 34/73] add validation if number is really a number --- scm-ui/public/locales/en/config.json | 6 ++++ scm-ui/src/components/validation.js | 18 +++++++++++ .../config/components/form/GeneralSettings.js | 31 ++++++++++++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 2779b3c28c..8a19b01026 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -55,5 +55,11 @@ "login-attempt-limit-timeout": "Login Attempt Limit Timeout", "enabled-xsrf-protection": "Enabled XSRF Protection", "default-namespace-strategy": "Default Namespace Strategy" + }, + "validation": { + "date-format-invalid": "The date format is not valid", + "login-attempt-limit-timeout-invalid": "This is not a number", + "login-attempt-limit-invalid": "This is not a number", + "plugin-url-invalid": "This is not a valid url" } } diff --git a/scm-ui/src/components/validation.js b/scm-ui/src/components/validation.js index fd61da57a5..0afa016a1e 100644 --- a/scm-ui/src/components/validation.js +++ b/scm-ui/src/components/validation.js @@ -10,3 +10,21 @@ const mailRegex = /^[A-z0-9][\w.-]*@[A-z0-9][\w\-.]*\.[A-z0-9][A-z0-9-]+$/; export const isMailValid = (mail: string) => { return mailRegex.test(mail); }; + +export const isNumberValid = (number: string) => { + return !isNaN(number); +}; + +const urlRegex = new RegExp( + "^(https?:\\/\\/)?" + // protocol + "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|" + // domain name + "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address + "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path + "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string + "(\\#[-a-z\\d_]*)?$", + "i" +); // fragment locator + +export const isUrlValid = (url: string) => { + return urlRegex.test(url); +}; diff --git a/scm-ui/src/config/components/form/GeneralSettings.js b/scm-ui/src/config/components/form/GeneralSettings.js index 321f247a2a..cb4e4e72b9 100644 --- a/scm-ui/src/config/components/form/GeneralSettings.js +++ b/scm-ui/src/config/components/form/GeneralSettings.js @@ -2,6 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import { Checkbox, InputField } from "../../../components/forms/index"; +import * as validator from "../../../components/validation"; type Props = { realmDescription: string, @@ -20,7 +21,23 @@ type Props = { hasUpdatePermission: boolean }; -class GeneralSettings extends React.Component<Props> { +type State = { + loginAttemptLimitError: boolean, + loginAttemptLimitTimeoutError: boolean +}; + +class GeneralSettings extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + loginAttemptLimitError: false, + loginAttemptLimitTimeoutError: false, + baseUrlError: false, + pluginUrlError: false + }; + } + render() { const { t, @@ -75,12 +92,16 @@ class GeneralSettings extends React.Component<Props> { onChange={this.handleLoginAttemptLimitChange} value={loginAttemptLimit} disabled={!hasUpdatePermission} + validationError={this.state.loginAttemptLimitError} + errorMessage={t("validation.login-attempt-limit-invalid")} /> <InputField label={t("general-settings.login-attempt-limit-timeout")} onChange={this.handleLoginAttemptLimitTimeoutChange} value={loginAttemptLimitTimeout} disabled={!hasUpdatePermission} + validationError={this.state.loginAttemptLimitTimeoutError} + errorMessage={t("validation.login-attempt-limit-timeout-invalid")} /> <Checkbox checked={skipFailedAuthenticators} @@ -126,6 +147,10 @@ class GeneralSettings extends React.Component<Props> { this.props.onChange(true, value, "anonymousAccessEnabled"); }; handleLoginAttemptLimitChange = (value: string) => { + this.setState({ + ...this.state, + loginAttemptLimitError: !validator.isNumberValid(value) + }); this.props.onChange(true, value, "loginAttemptLimit"); }; handleSkipFailedAuthenticatorsChange = (value: string) => { @@ -135,6 +160,10 @@ class GeneralSettings extends React.Component<Props> { this.props.onChange(true, value, "pluginUrl"); }; handleLoginAttemptLimitTimeoutChange = (value: string) => { + this.setState({ + ...this.state, + loginAttemptLimitTimeoutError: !validator.isNumberValid(value) + }); this.props.onChange(true, value, "loginAttemptLimitTimeout"); }; handleEnabledXsrfProtectionChange = (value: boolean) => { From 2198db867fc48ab9a7ade13436b994f1bcd6aa59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 16:41:17 +0200 Subject: [PATCH 35/73] outsourcing of login attemt variables --- scm-ui/public/locales/en/config.json | 7 +- .../src/config/components/form/ConfigForm.js | 42 ++++--- .../config/components/form/GeneralSettings.js | 103 +++++------------- .../config/components/form/LoginAttempt.js | 90 +++++++++++++++ 4 files changed, 149 insertions(+), 93 deletions(-) create mode 100644 scm-ui/src/config/components/form/LoginAttempt.js diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 8a19b01026..143755a318 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -43,16 +43,19 @@ "add-user-textfield": "Add user you want to add to admin users here", "add-user-button": "Add Admin User" }, + "login-attempt": { + "name": "Login Attempt", + "login-attempt-limit": "Login Attempt Limit", + "login-attempt-limit-timeout": "Login Attempt Limit Timeout" + }, "general-settings": { "realm-description": "Realm Description", "enable-repository-archive": "Enable Repository Archive", "disable-grouping-grid": "Disable Grouping Grid", "date-format": "Date Format", "anonymous-access-enabled": "Anonymous Access Enabled", - "login-attempt-limit": "Login Attempt Limit", "skip-failed-authenticators": "Skip Failed Authenticators", "plugin-url": "Plugin URL", - "login-attempt-limit-timeout": "Login Attempt Limit Timeout", "enabled-xsrf-protection": "Enabled XSRF Protection", "default-namespace-strategy": "Default Namespace Strategy" }, diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index bc4a94b501..430ad6abb2 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -8,6 +8,7 @@ import GeneralSettings from "./GeneralSettings"; import BaseUrlSettings from "./BaseUrlSettings"; import AdminSettings from "./AdminSettings"; import Notification from "../../../components/Notification"; +import LoginAttempt from "./LoginAttempt"; type Props = { submitForm: Config => void, @@ -19,7 +20,8 @@ type Props = { type State = { config: Config, - showNotification: boolean + showNotification: boolean, + loginAttemptError: boolean }; class ConfigForm extends React.Component<Props, State> { @@ -51,7 +53,8 @@ class ConfigForm extends React.Component<Props, State> { defaultNamespaceStrategy: "", _links: {} }, - showNotification: false + showNotification: false, + loginAttemptError: true }; } @@ -95,10 +98,8 @@ class ConfigForm extends React.Component<Props, State> { disableGroupingGrid={config.disableGroupingGrid} dateFormat={config.dateFormat} anonymousAccessEnabled={config.anonymousAccessEnabled} - loginAttemptLimit={config.loginAttemptLimit} skipFailedAuthenticators={config.skipFailedAuthenticators} pluginUrl={config.pluginUrl} - loginAttemptLimitTimeout={config.loginAttemptLimitTimeout} enabledXsrfProtection={config.enabledXsrfProtection} defaultNamespaceStrategy={config.defaultNamespaceStrategy} onChange={(isValid, changedValue, name) => @@ -107,6 +108,15 @@ class ConfigForm extends React.Component<Props, State> { hasUpdatePermission={configUpdatePermission} /> <hr /> + <LoginAttempt + loginAttemptLimit={config.loginAttemptLimit} + loginAttemptLimitTimeout={config.loginAttemptLimitTimeout} + onChange={(isValid, changedValue, name) => + this.onChange(isValid, changedValue, name) + } + hasUpdatePermission={configUpdatePermission} + /> + <hr /> <BaseUrlSettings baseUrl={config.baseUrl} forceBaseUrl={config.forceBaseUrl} @@ -139,25 +149,27 @@ class ConfigForm extends React.Component<Props, State> { /> <hr /> <SubmitButton - // disabled={!this.isValid()} loading={loading} label={t("config-form.submit")} - disabled={!configUpdatePermission} + disabled={!configUpdatePermission || !this.isValid()} /> </form> ); } + onChange = (isValid: boolean, changedValue: any, name: string) => { - if (isValid) { - this.setState({ - ...this.state, - config: { - ...this.state.config, - [name]: changedValue - } - }); - } + this.setState({ + ...this.state, + config: { + ...this.state.config, + [name]: changedValue + } + }); + }; + + isValid = () => { + return this.state.loginAttemptError; }; onClose = () => { diff --git a/scm-ui/src/config/components/form/GeneralSettings.js b/scm-ui/src/config/components/form/GeneralSettings.js index cb4e4e72b9..5ae999509e 100644 --- a/scm-ui/src/config/components/form/GeneralSettings.js +++ b/scm-ui/src/config/components/form/GeneralSettings.js @@ -2,7 +2,6 @@ import React from "react"; import { translate } from "react-i18next"; import { Checkbox, InputField } from "../../../components/forms/index"; -import * as validator from "../../../components/validation"; type Props = { realmDescription: string, @@ -10,10 +9,8 @@ type Props = { disableGroupingGrid: boolean, dateFormat: string, anonymousAccessEnabled: boolean, - loginAttemptLimit: number, skipFailedAuthenticators: boolean, pluginUrl: string, - loginAttemptLimitTimeout: number, enabledXsrfProtection: boolean, defaultNamespaceStrategy: string, t: string => string, @@ -21,23 +18,7 @@ type Props = { hasUpdatePermission: boolean }; -type State = { - loginAttemptLimitError: boolean, - loginAttemptLimitTimeoutError: boolean -}; - -class GeneralSettings extends React.Component<Props, State> { - constructor(props: Props) { - super(props); - - this.state = { - loginAttemptLimitError: false, - loginAttemptLimitTimeoutError: false, - baseUrlError: false, - pluginUrlError: false - }; - } - +class GeneralSettings extends React.Component<Props> { render() { const { t, @@ -46,10 +27,8 @@ class GeneralSettings extends React.Component<Props, State> { disableGroupingGrid, dateFormat, anonymousAccessEnabled, - loginAttemptLimit, skipFailedAuthenticators, pluginUrl, - loginAttemptLimitTimeout, enabledXsrfProtection, defaultNamespaceStrategy, hasUpdatePermission @@ -63,6 +42,30 @@ class GeneralSettings extends React.Component<Props, State> { value={realmDescription} disabled={!hasUpdatePermission} /> + <InputField + label={t("general-settings.date-format")} + onChange={this.handleDateFormatChange} + value={dateFormat} + disabled={!hasUpdatePermission} + /> + <InputField + label={t("general-settings.plugin-url")} + onChange={this.handlePluginUrlChange} + value={pluginUrl} + disabled={!hasUpdatePermission} + /> + <InputField + label={t("general-settings.default-namespace-strategy")} + onChange={this.handleDefaultNamespaceStrategyChange} + value={defaultNamespaceStrategy} + disabled={!hasUpdatePermission} + /> + <Checkbox + checked={enabledXsrfProtection} + label={t("general-settings.enabled-xsrf-protection")} + onChange={this.handleEnabledXsrfProtectionChange} + disabled={!hasUpdatePermission} + /> <Checkbox checked={enableRepositoryArchive} label={t("general-settings.enable-repository-archive")} @@ -75,58 +78,18 @@ class GeneralSettings extends React.Component<Props, State> { onChange={this.handleDisableGroupingGridChange} disabled={!hasUpdatePermission} /> - <InputField - label={t("general-settings.date-format")} - onChange={this.handleDateFormatChange} - value={dateFormat} - disabled={!hasUpdatePermission} - /> <Checkbox checked={anonymousAccessEnabled} label={t("general-settings.anonymous-access-enabled")} onChange={this.handleAnonymousAccessEnabledChange} disabled={!hasUpdatePermission} /> - <InputField - label={t("general-settings.login-attempt-limit")} - onChange={this.handleLoginAttemptLimitChange} - value={loginAttemptLimit} - disabled={!hasUpdatePermission} - validationError={this.state.loginAttemptLimitError} - errorMessage={t("validation.login-attempt-limit-invalid")} - /> - <InputField - label={t("general-settings.login-attempt-limit-timeout")} - onChange={this.handleLoginAttemptLimitTimeoutChange} - value={loginAttemptLimitTimeout} - disabled={!hasUpdatePermission} - validationError={this.state.loginAttemptLimitTimeoutError} - errorMessage={t("validation.login-attempt-limit-timeout-invalid")} - /> <Checkbox checked={skipFailedAuthenticators} label={t("general-settings.skip-failed-authenticators")} onChange={this.handleSkipFailedAuthenticatorsChange} disabled={!hasUpdatePermission} /> - <InputField - label={t("general-settings.plugin-url")} - onChange={this.handlePluginUrlChange} - value={pluginUrl} - disabled={!hasUpdatePermission} - /> - <Checkbox - checked={enabledXsrfProtection} - label={t("general-settings.enabled-xsrf-protection")} - onChange={this.handleEnabledXsrfProtectionChange} - disabled={!hasUpdatePermission} - /> - <InputField - label={t("general-settings.default-namespace-strategy")} - onChange={this.handleDefaultNamespaceStrategyChange} - value={defaultNamespaceStrategy} - disabled={!hasUpdatePermission} - /> </div> ); } @@ -146,26 +109,14 @@ class GeneralSettings extends React.Component<Props, State> { handleAnonymousAccessEnabledChange = (value: string) => { this.props.onChange(true, value, "anonymousAccessEnabled"); }; - handleLoginAttemptLimitChange = (value: string) => { - this.setState({ - ...this.state, - loginAttemptLimitError: !validator.isNumberValid(value) - }); - this.props.onChange(true, value, "loginAttemptLimit"); - }; + handleSkipFailedAuthenticatorsChange = (value: string) => { this.props.onChange(true, value, "skipFailedAuthenticators"); }; handlePluginUrlChange = (value: string) => { this.props.onChange(true, value, "pluginUrl"); }; - handleLoginAttemptLimitTimeoutChange = (value: string) => { - this.setState({ - ...this.state, - loginAttemptLimitTimeoutError: !validator.isNumberValid(value) - }); - this.props.onChange(true, value, "loginAttemptLimitTimeout"); - }; + handleEnabledXsrfProtectionChange = (value: boolean) => { this.props.onChange(true, value, "enabledXsrfProtection"); }; diff --git a/scm-ui/src/config/components/form/LoginAttempt.js b/scm-ui/src/config/components/form/LoginAttempt.js new file mode 100644 index 0000000000..489828fd0b --- /dev/null +++ b/scm-ui/src/config/components/form/LoginAttempt.js @@ -0,0 +1,90 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { InputField } from "../../../components/forms/index"; +import Subtitle from "../../../components/layout/Subtitle"; +import * as validator from "../../../components/validation"; + +type Props = { + loginAttemptLimit: number, + loginAttemptLimitTimeout: number, + t: string => string, + onChange: (boolean, any, string) => void, + hasUpdatePermission: boolean +}; + +type State = { + loginAttemptLimitError: boolean, + loginAttemptLimitTimeoutError: boolean +}; + +class LoginAttempt extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + loginAttemptLimitError: false, + loginAttemptLimitTimeoutError: false + }; + } + render() { + const { + t, + loginAttemptLimit, + loginAttemptLimitTimeout, + hasUpdatePermission + } = this.props; + + return ( + <div> + <Subtitle subtitle={t("login-attempt.name")} /> + <InputField + label={t("login-attempt.login-attempt-limit")} + onChange={this.handleLoginAttemptLimitChange} + value={loginAttemptLimit} + disabled={!hasUpdatePermission} + validationError={this.state.loginAttemptLimitError} + errorMessage={t("validation.login-attempt-limit-invalid")} + /> + <InputField + label={t("login-attempt.login-attempt-limit-timeout")} + onChange={this.handleLoginAttemptLimitTimeoutChange} + value={loginAttemptLimitTimeout} + disabled={!hasUpdatePermission} + validationError={this.state.loginAttemptLimitTimeoutError} + errorMessage={t("validation.login-attempt-limit-timeout-invalid")} + /> + </div> + ); + } + + //TODO: set Error in ConfigForm to disable Submit Button! + handleLoginAttemptLimitChange = (value: string) => { + this.setState({ + ...this.state, + loginAttemptLimitError: !validator.isNumberValid(value) + }); + this.props.onChange(this.loginAttemptIsValid(), value, "loginAttemptLimit"); + }; + + handleLoginAttemptLimitTimeoutChange = (value: string) => { + this.setState({ + ...this.state, + loginAttemptLimitTimeoutError: !validator.isNumberValid(value) + }); + this.props.onChange( + this.loginAttemptIsValid(), + value, + "loginAttemptLimitTimeout" + ); + }; + + loginAttemptIsValid = () => { + return ( + this.state.loginAttemptLimitError || + this.state.loginAttemptLimitTimeoutError + ); + }; +} + +export default translate("config")(LoginAttempt); From f79975b18d6608a75f8d6fa95b66a828ca619c51 Mon Sep 17 00:00:00 2001 From: Mohamed Karray <mohamed.karray.sr@gmail.com> Date: Mon, 20 Aug 2018 18:16:14 +0200 Subject: [PATCH 36/73] #8771 Permission endpoints --- .../PermissionAlreadyExistsException.java | 11 + .../PermissionNotFoundException.java | 12 + .../main/java/sonia/scm/web/VndMediaType.java | 1 + scm-webapp/pom.xml | 20 +- .../scm/api/rest/StatusExceptionMapper.java | 81 ++-- .../AuthorizationExceptionMapper.java | 50 ++ .../scm/api/v2/resources/MapperModule.java | 2 + ...ermissionAlreadyExistsExceptionMapper.java | 50 ++ .../PermissionCollectionResource.java | 18 - .../scm/api/v2/resources/PermissionDto.java | 17 +- .../PermissionDtoToPermissionMapper.java | 21 + .../PermissionNotFoundExceptionMapper.java | 50 ++ .../v2/resources/PermissionRootResource.java | 230 ++++++++- .../PermissionToPermissionDtoMapper.java | 37 ++ .../api/v2/resources/PermissionTypeDto.java | 26 -- .../RepositoryNotFoundExceptionMapper.java | 50 ++ .../RepositoryToRepositoryDtoMapper.java | 2 +- .../scm/api/v2/resources/ResourceLinks.java | 31 +- .../resources/PermissionRootResourceTest.java | 437 ++++++++++++++++++ .../api/v2/resources/ResourceLinksMock.java | 2 +- 20 files changed, 1035 insertions(+), 113 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java new file mode 100644 index 0000000000..aeaf64a3e9 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java @@ -0,0 +1,11 @@ +package sonia.scm.repository; + +import java.text.MessageFormat; + +public class PermissionAlreadyExistsException extends RepositoryException { + + public PermissionAlreadyExistsException(Repository repository, String permissionName) { + super(MessageFormat.format("the permission {0} of the repository {1}/{2} is already exists", permissionName, repository.getNamespace(), repository.getName())); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java new file mode 100644 index 0000000000..9e1b644faa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java @@ -0,0 +1,12 @@ +package sonia.scm.repository; + +import java.text.MessageFormat; + +public class PermissionNotFoundException extends RepositoryException{ + + + public PermissionNotFoundException(Repository repository, String permissionName) { + super(MessageFormat.format("the permission {0} of the repository {1}/{2} does not exists", permissionName,repository.getNamespace(), repository.getName() )); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 1e439a6a16..afb1417670 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -16,6 +16,7 @@ public class VndMediaType { public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; + public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index d0003c1f7c..c622042061 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -276,7 +276,25 @@ <version>${jersey-client.version}</version> <scope>test</scope> </dependency> - + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>5.2.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + <version>5.2.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>5.2.0</version> + <scope>test</scope> + </dependency> <!-- core plugins --> <dependency> diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java index 08d6b984d2..70b0d0ce2e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java @@ -1,30 +1,30 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. 2. Redistributions in - * binary form must reproduce the above copyright notice, this list of - * conditions and the following disclaimer in the documentation and/or other - * materials provided with the distribution. 3. Neither the name of SCM-Manager; - * nor the names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * +/* + Copyright (c) 2010, Sebastian Sdorra All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. 2. Redistributions in + binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 3. Neither the name of SCM-Manager; + nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + */ @@ -56,14 +56,14 @@ public class StatusExceptionMapper<E extends Throwable> private static final Logger logger = LoggerFactory.getLogger(StatusExceptionMapper.class); - //~--- constructors --------------------------------------------------------- + private final Response.Status status; + private final Class<E> type; /** - * Constructs ... + * Map an Exception to a HTTP Response * - * - * @param type - * @param status + * @param type the exception class + * @param status the http status to be mapped */ public StatusExceptionMapper(Class<E> type, Response.Status status) { @@ -71,15 +71,12 @@ public class StatusExceptionMapper<E extends Throwable> this.status = status; } - //~--- methods -------------------------------------------------------------- - /** - * Method description + * provide a http responses from an exception * + * @param exception the thrown exception * - * @param exception - * - * @return + * @return the http response with the exception presentation */ @Override public Response toResponse(E exception) @@ -95,12 +92,4 @@ public class StatusExceptionMapper<E extends Throwable> return Response.status(status).build(); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final Response.Status status; - - /** Field description */ - private final Class<E> type; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java new file mode 100644 index 0000000000..bf00bbfc5e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java @@ -0,0 +1,50 @@ +/* + Copyright (c) 2014, Sebastian Sdorra All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. 2. Redistributions in + binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 3. Neither the name of SCM-Manager; + nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + + */ + + +package sonia.scm.api.v2.resources; + + +import org.apache.shiro.authz.AuthorizationException; +import sonia.scm.api.rest.StatusExceptionMapper; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class AuthorizationExceptionMapper extends StatusExceptionMapper<AuthorizationException> { + + public AuthorizationExceptionMapper() { + super(AuthorizationException.class, Response.Status.UNAUTHORIZED); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 0ac6929689..0605d943e7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -25,6 +25,8 @@ public class MapperModule extends AbstractModule { bind(RepositoryTypeCollectionToDtoMapper.class); bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass()); + bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass()); + bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass()); bind(UriInfoStore.class).in(ServletScopes.REQUEST); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java new file mode 100644 index 0000000000..0cf83f097a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java @@ -0,0 +1,50 @@ +/* + Copyright (c) 2014, Sebastian Sdorra All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. 2. Redistributions in + binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 3. Neither the name of SCM-Manager; + nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + + */ + + +package sonia.scm.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.PermissionAlreadyExistsException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class PermissionAlreadyExistsExceptionMapper extends StatusExceptionMapper<PermissionAlreadyExistsException> { + + public PermissionAlreadyExistsExceptionMapper() { + super(PermissionAlreadyExistsException.class, Response.Status.CONFLICT); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java deleted file mode 100644 index 6c4b52c16d..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java +++ /dev/null @@ -1,18 +0,0 @@ -package sonia.scm.api.v2.resources; - -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Response; - -public class PermissionCollectionResource { - @GET - @Path("") - public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - throw new UnsupportedOperationException(); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java index ebd49423ab..b184bc3934 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java @@ -5,16 +5,25 @@ import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; import lombok.Setter; +import lombok.ToString; -@Getter @Setter +@Getter @Setter @ToString public class PermissionDto extends HalRepresentation { - @JsonInclude(JsonInclude.Include.NON_NULL) - private PermissionTypeDto type = PermissionTypeDto.READ; - @JsonInclude(JsonInclude.Include.NON_NULL) private String name; + /** + * the type can be replaced with a dto enum if the mapstruct 1.3.0 is stable + * the mapstruct has a Bug on mapping enums in the 1.2.0-Final Version + * + * see the bug fix: https://github.com/mapstruct/mapstruct/commit/460e87eef6eb71245b387fdb0509c726676a8e19 + * + **/ + @JsonInclude(JsonInclude.Include.NON_NULL) + private String type ; + + private boolean groupPermission = false; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java new file mode 100644 index 0000000000..9128479836 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java @@ -0,0 +1,21 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.Permission; + +@Mapper +public abstract class PermissionDtoToPermissionMapper { + + public abstract Permission map(PermissionDto permissionDto); + + /** + * this method is needed to modify an existing permission object + * + * @param target the target permission + * @param permissionDto the source dto + * @return the mapped target permission object + */ + public abstract Permission map(@MappingTarget Permission target, PermissionDto permissionDto); + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java new file mode 100644 index 0000000000..42e341ce0d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java @@ -0,0 +1,50 @@ +/* + Copyright (c) 2014, Sebastian Sdorra All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. 2. Redistributions in + binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 3. Neither the name of SCM-Manager; + nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + + */ + + +package sonia.scm.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.PermissionNotFoundException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class PermissionNotFoundExceptionMapper extends StatusExceptionMapper<PermissionNotFoundException> { + + public PermissionNotFoundExceptionMapper() { + super(PermissionNotFoundException.class, Response.Status.NOT_FOUND); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index cd1e970e43..e9e1d38d95 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -1,20 +1,234 @@ package sonia.scm.api.v2.resources; -import javax.inject.Inject; -import javax.inject.Provider; -import javax.ws.rs.Path; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import sonia.scm.repository.*; +import sonia.scm.web.VndMediaType; +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j public class PermissionRootResource { - private final Provider<PermissionCollectionResource> permissionCollectionResource; + private PermissionDtoToPermissionMapper dtoToModelMapper; + private PermissionToPermissionDtoMapper modelToDtoMapper; + private ResourceLinks resourceLinks; + private final RepositoryManager manager; + @Inject - public PermissionRootResource(Provider<PermissionCollectionResource> permissionCollectionResource) { - this.permissionCollectionResource = permissionCollectionResource; + public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) { + this.dtoToModelMapper = dtoToModelMapper; + this.modelToDtoMapper = modelToDtoMapper; + this.resourceLinks = resourceLinks; + this.manager = manager; } + + /** + * Adds a new permission to the user or group managed by the repository + * + * @param permission permission to add + * @return a web response with the status code 201 and the url to GET the added permission + */ + @POST + @StatusCodes({ + @ResponseCode(code = 201, condition = "creates", additionalHeaders = { + @ResponseHeader(name = "Location", description = "uri of the created permission") + }), + @ResponseCode(code = 500, condition = "internal server error"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 409, condition = "conflict") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Consumes(VndMediaType.PERMISSION) + public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws RepositoryException { + log.info("try to add new permission: {}", permission); + Repository repository = checkPermission(namespace, name); + checkPermissionAlreadyExists(permission, repository); + repository.getPermissions().add(dtoToModelMapper.map(permission)); + manager.modify(repository); + return Response.created(URI.create(resourceLinks.permission().self(namespace,name,permission.getName()))).build(); + } + + + /** + * Get the searched permission with permission name related to a repository + * + * @param namespace the repository namespace + * @param name the repository name + * @return the http response with a list of permissionDto objects + * @throws RepositoryNotFoundException if the repository does not exists + */ + @GET + @StatusCodes({ + @ResponseCode(code = 200, condition = "ok"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.PERMISSION) + @TypeHint(PermissionDto.class) + @Path("{permission-name}") + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { + Repository repository = checkPermission(namespace, name); + return Response.ok( + repository.getPermissions() + .stream() + .filter(permission -> StringUtils.isNotBlank(permission.getName()) && permission.getName().equals(permissionName)) + .map(permission -> modelToDtoMapper.map(permission, new NamespaceAndName(repository.getNamespace(),repository.getName()))) + .findFirst() + .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) + ).build(); + } + + + /** + * Get all permissions related to a repository + * + * @param namespace the repository namespace + * @param name the repository name + * @return the http response with a list of permissionDto objects + * @throws RepositoryNotFoundException if the repository does not exists + */ + @GET + @StatusCodes({ + @ResponseCode(code = 200, condition = "ok"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.PERMISSION) + @TypeHint(PermissionDto.class) @Path("") - public PermissionCollectionResource getPermissionCollectionResource() { - return permissionCollectionResource.get(); + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { + Repository repository = checkPermission(namespace, name); + List<PermissionDto> permissionDtoList = repository.getPermissions() + .stream() + .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(),repository.getName()))) + .collect(Collectors.toList()); + return Response.ok(permissionDtoList).build(); + } + + + /** + * Update a permission to the user or group managed by the repository + * + * @param permission permission to modify + * @param permissionName permission to modify + * @return a web response with the status code 204 + */ + @PUT + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Consumes(VndMediaType.PERMISSION) + @Path("{permission-name}") + public Response update(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("permission-name") String permissionName, + PermissionDto permission) throws RepositoryException { + log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); + Repository repository = checkPermission(namespace, name); + repository.getPermissions() + .stream() + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .findFirst() + .map(p -> dtoToModelMapper.map(p, permission)) + .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) + ; + manager.modify(repository); + log.info("the permission with name: {} is updated.", permissionName); + return Response.noContent().build(); + } + + /** + * Update a permission to the user or group managed by the repository + * + * @param permissionName permission to delete + * @return a web response with the status code 204 + */ + @DELETE + @StatusCodes({ + @ResponseCode(code = 204, condition = "delete success or nothing to delete"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Path("{permission-name}") + public Response delete(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("permission-name") String permissionName) throws RepositoryException { + log.info("try to delete the permission with name: {}.", permissionName); + Repository repository = checkPermission(namespace, name); + repository.getPermissions() + .stream() + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .findFirst() + .ifPresent(p -> repository.getPermissions().remove(p)) + ; + manager.modify(repository); + log.info("the permission with name: {} is updated.", permissionName); + return Response.noContent().build(); + } + + + + /** + * check if the actual user is permitted to manage the repository permissions + * return the repository if the user is permitted + * + * @param namespace the repository namespace + * @param name the repository name + * @return the repository if the user is permitted + * @throws RepositoryNotFoundException if the repository does not exists + */ + private Repository checkPermission(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { + return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name))) + .filter(repository -> { + checkUserPermitted(repository); + return true; + }) + .orElseThrow(() -> new RepositoryNotFoundException(name)); + } + + + /** + * throw exception if the user is not permitted + * @param repository + */ + protected void checkUserPermitted(Repository repository) { + RepositoryPermissions.modify(repository).check(); + } + + + /** + * check if the permission already exists in the repository + * + * @param permission the searched permission + * @param repository the repository to be inspected + * @throws PermissionAlreadyExistsException if the permission already exists in the repository + */ + private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws PermissionAlreadyExistsException { + boolean isPermissionAlreadyExist = repository.getPermissions() + .stream() + .anyMatch(p -> p.getName().equals(permission.getName())); + if (isPermissionAlreadyExist) { + throw new PermissionAlreadyExistsException(repository, permission.getName()); + } } } + + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java new file mode 100644 index 0000000000..49adb0e1c0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java @@ -0,0 +1,37 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.*; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Permission; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +@Mapper +public abstract class PermissionToPermissionDtoMapper { + + @Inject + private ResourceLinks resourceLinks; + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract PermissionDto map(Permission permission, @Context NamespaceAndName namespaceAndName); + + /** + * Add the self, update and delete links. + * + * @param target the mapped dto + * @param namespaceAndName the repository namespace and name + */ + @AfterMapping + void appendLinks(@MappingTarget PermissionDto target, @Context NamespaceAndName namespaceAndName) { + Links.Builder linksBuilder = linkingTo() + .self(resourceLinks.permission().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())); + linksBuilder.single(link("update", resourceLinks.permission().update(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))); + linksBuilder.single(link("delete", resourceLinks.permission().delete(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))); + target.add(linksBuilder.build()); + } +} + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java deleted file mode 100644 index 7b280f9f1f..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package sonia.scm.api.v2.resources; - -/** - * Type of permissionPrefix for a {@link RepositoryDto}. - * - * @author mkarray - */ - -public enum PermissionTypeDto { - - /** - * read permission - */ - READ, - - /** - * read and write permission - */ - WRITE, - - /** - * read, write and manage the properties and permissions - */ - OWNER - -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java new file mode 100644 index 0000000000..2116b8e31c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java @@ -0,0 +1,50 @@ +/* + Copyright (c) 2014, Sebastian Sdorra All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. 2. Redistributions in + binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 3. Neither the name of SCM-Manager; + nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + + */ + + +package sonia.scm.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.RepositoryNotFoundException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class RepositoryNotFoundExceptionMapper extends StatusExceptionMapper<RepositoryNotFoundException> { + + public RepositoryNotFoundExceptionMapper() { + super(RepositoryNotFoundException.class, Response.Status.NOT_FOUND); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index f46bc22cdf..0263b81048 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -36,7 +36,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit } if (RepositoryPermissions.modify(repository).isPermitted()) { linksBuilder.single(link("update", resourceLinks.repository().update(target.getNamespace(), target.getName()))); - linksBuilder.single(link("permissions", resourceLinks.permissionCollection().self(target.getNamespace(), target.getName()))); + linksBuilder.single(link("permissions", resourceLinks.permission().all(target.getNamespace(), target.getName()))); } try (RepositoryService repositoryService = serviceFactory.create(repository)) { if (repositoryService.isSupported(Command.TAGS)) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 319bbec32b..47e6e93777 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -298,20 +298,35 @@ class ResourceLinks { return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision).href(); } } - - public PermissionCollectionLinks permissionCollection() { - return new PermissionCollectionLinks(uriInfoStore.get()); + public PermissionLinks permission() { + return new PermissionLinks(uriInfoStore.get()); } - static class PermissionCollectionLinks { + static class PermissionLinks { private final LinkBuilder permissionLinkBuilder; - PermissionCollectionLinks(UriInfo uriInfo) { - permissionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class, PermissionCollectionResource.class); + PermissionLinks(UriInfo uriInfo) { + permissionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class); } - String self(String namespace, String name) { - return permissionLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("permissions").parameters().method("getPermissionCollectionResource").parameters().method("getAll").parameters().href(); + String all(String namespace, String name) { + return permissionLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("permissions").parameters().method("getAll").parameters().href(); + } + + String self(String repositoryNamespace, String repositoryName, String permissionName) { + return getLink(repositoryNamespace, repositoryName, permissionName, "get"); + } + + String update(String repositoryNamespace, String repositoryName, String permissionName) { + return getLink(repositoryNamespace, repositoryName, permissionName, "update"); + } + + String delete(String repositoryNamespace, String repositoryName, String permissionName) { + return getLink(repositoryNamespace, repositoryName, permissionName, "delete"); + } + + private String getLink(String repositoryNamespace, String repositoryName, String permissionName, String methodName) { + return permissionLinkBuilder.method("getRepositoryResource").parameters(repositoryNamespace, repositoryName).method("permissions").parameters().method(methodName).parameters(permissionName).href(); } } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java new file mode 100644 index 0000000000..5de6d916ab --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -0,0 +1,437 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.common.collect.ImmutableList; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.authz.AuthorizationException; +import org.assertj.core.util.Lists; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.jboss.resteasy.spi.HttpRequest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.*; +import sonia.scm.web.VndMediaType; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.initMocks; + +@SubjectAware( + username = "trillian", + password = "secret", + configuration = "classpath:sonia/scm/repository/shiro.ini" +) +@RunWith(MockitoJUnitRunner.Silent.class) +@Slf4j +public class PermissionRootResourceTest { + public static final String REPOSITORY_NAMESPACE = "repo_namespace"; + public static final String REPOSITORY_NAME = "repo"; + private static final String PERMISSION_NAME = "perm"; + + + private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/"; + private static final String PATH_OF_ONE_PERMISSION = PATH_OF_ALL_PERMISSIONS + PERMISSION_NAME; + private static final String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"type\" : \"READ\" }"; + private static final ArrayList<Permission> TEST_PERMISSIONS = Lists + .newArrayList( + new Permission("user_write", PermissionType.WRITE, false), + new Permission("user_read", PermissionType.READ, false), + new Permission("user_owner", PermissionType.OWNER, false), + new Permission("group_read", PermissionType.READ, true), + new Permission("group_write", PermissionType.WRITE, true), + new Permission("group_owner", PermissionType.OWNER, true) + ); + private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest() + .description("GET all permissions") + .method("GET") + .path(PATH_OF_ALL_PERMISSIONS); + private final ExpectedRequest requestPOSTPermission = new ExpectedRequest() + .description("create new permission") + .method("POST") + .content(PERMISSION_TEST_PAYLOAD) + .path(PATH_OF_ALL_PERMISSIONS); + private final ExpectedRequest requestGETPermission = new ExpectedRequest() + .description("GET permission") + .method("GET") + .path(PATH_OF_ONE_PERMISSION); + private final ExpectedRequest requestDELETEPermission = new ExpectedRequest() + .description("delete permission") + .method("DELETE") + .path(PATH_OF_ONE_PERMISSION); + private final ExpectedRequest requestPUTPermission = new ExpectedRequest() + .description("update permission") + .method("PUT") + .content(PERMISSION_TEST_PAYLOAD) + .path(PATH_OF_ONE_PERMISSION); + + + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Mock + private RepositoryManager repositoryManager; + + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @InjectMocks + private PermissionToPermissionDtoMapperImpl permissionToPermissionDtoMapper; + + @InjectMocks + private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper; + + + private PermissionRootResource permissionRootResource; + + + @BeforeEach + @Before + public void prepareEnvironment() { + initMocks(this); + permissionRootResource = spy(new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager)); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider + .of(new RepositoryResource(null, null, null, null, null, null, null, MockProvider.of(permissionRootResource))), null); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + dispatcher.getProviderFactory().registerProvider(RepositoryNotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(PermissionNotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(PermissionAlreadyExistsExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + } + + + @Test + public void shouldGetAllPermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + assertExpectedRequest(requestGETAllPermissions + .expectedResponseStatus(200) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + List<PermissionDto> actualPermissionDtos = mapper.readValue(body, new TypeReference<List<PermissionDto>>() { + }); + assertThat(actualPermissionDtos) + .as("response payload match permission object models") + .hasSize(TEST_PERMISSIONS.size()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(getExpectedPermissionDtos(TEST_PERMISSIONS)) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + + @Test + public void shouldGetPermissionByName() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission expectedPermission = TEST_PERMISSIONS.get(0); + assertExpectedRequest(requestGETPermission + .expectedResponseStatus(200) + .path(PATH_OF_ALL_PERMISSIONS + expectedPermission.getName()) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + PermissionDto actualPermissionDto = mapper.readValue(body, PermissionDto.class); + assertThat(actualPermissionDto) + .as("response payload match permission object model") + .isEqualToComparingFieldByFieldRecursively(getExpectedPermissionDto(expectedPermission)) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + + @Test + public void shouldGetCreatedPermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true); + ArrayList<Permission> permissions = Lists.newArrayList(TEST_PERMISSIONS); + permissions.add(newPermission); + ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(permissions); + assertExpectedRequest(requestPOSTPermission + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") + .expectedResponseStatus(201) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("POST response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + @Test + public void shouldNotAddExistingPermission() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission newPermission = TEST_PERMISSIONS.get(0); + assertExpectedRequest(requestPOSTPermission + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") + .expectedResponseStatus(409) + ); + } + + + @Test + public void shouldGetUpdatedPermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission modifiedPermission = TEST_PERMISSIONS.get(0); + // modify the type to owner + modifiedPermission.setType(PermissionType.OWNER); + ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS); + assertExpectedRequest(requestPUTPermission + .content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"type\" : \"OWNER\" , \"groupPermission\" : false}") + .path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("PUT response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + + @Test + public void shouldDeletePermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission deletedPermission = TEST_PERMISSIONS.get(0); + ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); + assertExpectedRequest(requestDELETEPermission + .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + @Test + public void deletingNotExistingPermissionShouldProcess() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission deletedPermission = TEST_PERMISSIONS.get(0); + ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); + assertExpectedRequest(requestDELETEPermission + .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + assertExpectedRequest(requestDELETEPermission + .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + private void assertGettingExpectedPermissions(ImmutableList<Permission> expectedPermissions) { + assertExpectedRequest(requestGETAllPermissions + .expectedResponseStatus(200) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + List<PermissionDto> actualPermissionDtos = mapper.readValue(body, new TypeReference<List<PermissionDto>>() { + }); + assertThat(actualPermissionDtos) + .as("response payload match permission object models") + .hasSize(expectedPermissions.size()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(getExpectedPermissionDtos(Lists.newArrayList(expectedPermissions))) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + + private PermissionDto[] getExpectedPermissionDtos(ArrayList<Permission> permissions) { + return permissions + .stream() + .map(this::getExpectedPermissionDto) + .toArray(PermissionDto[]::new); + } + + private PermissionDto getExpectedPermissionDto(Permission permission) { + PermissionDto result = new PermissionDto(); + result.setName(permission.getName()); + result.setGroupPermission(permission.isGroupPermission()); + result.setType(permission.getType().name()); + String permissionHref = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS + permission.getName(); + result.add(linkingTo() + .self(permissionHref) + .single(link("update", permissionHref)) + .single(link("delete", permissionHref)) + .build()); + return result; + } + + @TestFactory + @DisplayName("test endpoints on missing repository and user is Admin") + Stream<DynamicTest> missedRepositoryTestFactory() { + return createDynamicTestsToAssertResponses( + requestGETAllPermissions.expectedResponseStatus(404), + requestGETPermission.expectedResponseStatus(404), + requestPOSTPermission.expectedResponseStatus(404), + requestDELETEPermission.expectedResponseStatus(404), + requestPUTPermission.expectedResponseStatus(404)); + } + + + @TestFactory + @DisplayName("test endpoints on missing permission and user is Admin") + Stream<DynamicTest> missedPermissionTestFactory() { + authorizedUserHasARepository(); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(404), + requestPOSTPermission.expectedResponseStatus(201), + requestGETAllPermissions.expectedResponseStatus(200), + requestDELETEPermission.expectedResponseStatus(204), + requestPUTPermission.expectedResponseStatus(404)); + } + + private Repository authorizedUserHasARepository() { + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); + when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); + doNothing().when(permissionRootResource).checkUserPermitted(mockRepository); + when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + return mockRepository; + } + + private void authorizedUserHasARepositoryWithPermissions(ArrayList<Permission> permissions) { + when(authorizedUserHasARepository().getPermissions()).thenReturn(permissions); + } + + + @TestFactory + @DisplayName("test endpoints on missing permission and user is not Admin") + Stream<DynamicTest> missedPermissionUserForbiddenTestFactory() { + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository); + when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(401), + requestPOSTPermission.expectedResponseStatus(401), + requestGETAllPermissions.expectedResponseStatus(401), + requestDELETEPermission.expectedResponseStatus(401), + requestPUTPermission.expectedResponseStatus(401)); + } + + + private Stream<DynamicTest> createDynamicTestsToAssertResponses(ExpectedRequest... expectedRequests) { + + return Stream.of(expectedRequests) + .map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry))); + } + + private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) { + MockHttpResponse response = new MockHttpResponse(); + HttpRequest request = null; + try { + request = MockHttpRequest + .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) + .content(entry.content) + .contentType(VndMediaType.PERMISSION); + } catch (URISyntaxException e) { + fail(e.getMessage()); + } + dispatcher.invoke(request, response); + log.info("Test the Request :{}", entry); + assertThat(entry.expectedResponseStatus) + .as("assert status code") + .isEqualTo(response.getStatus()); + if (entry.responseValidator != null) { + entry.responseValidator.accept(response); + } + return response; + } + + @ToString + public class ExpectedRequest { + private String description; + private String method; + private String path; + private int expectedResponseStatus; + private byte[] content = new byte[]{}; + private Consumer<MockHttpResponse> responseValidator; + + public ExpectedRequest description(String description) { + this.description = description; + return this; + } + + public ExpectedRequest method(String method) { + this.method = method; + return this; + } + + public ExpectedRequest path(String path) { + this.path = path; + return this; + } + + public ExpectedRequest content(String content) { + if (content != null) { + this.content = content.getBytes(); + } + return this; + } + + public ExpectedRequest expectedResponseStatus(int expectedResponseStatus) { + this.expectedResponseStatus = expectedResponseStatus; + return this; + } + + public ExpectedRequest responseValidator(Consumer<MockHttpResponse> responseValidator) { + this.responseValidator = responseValidator; + return this; + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 882d754329..6df5ac1a7a 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -23,7 +23,7 @@ public class ResourceLinksMock { when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo)); - when(resourceLinks.permissionCollection()).thenReturn(new ResourceLinks.PermissionCollectionLinks(uriInfo)); + when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo)); when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo)); when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo)); From 98b8e343085a1ad87053c1ee382f21e37e8a7c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 08:07:09 +0200 Subject: [PATCH 37/73] remove unnecessary url --- scm-ui/src/components/validation.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/scm-ui/src/components/validation.js b/scm-ui/src/components/validation.js index 0afa016a1e..7fa4a262b9 100644 --- a/scm-ui/src/components/validation.js +++ b/scm-ui/src/components/validation.js @@ -14,17 +14,3 @@ export const isMailValid = (mail: string) => { export const isNumberValid = (number: string) => { return !isNaN(number); }; - -const urlRegex = new RegExp( - "^(https?:\\/\\/)?" + // protocol - "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|" + // domain name - "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address - "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path - "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string - "(\\#[-a-z\\d_]*)?$", - "i" -); // fragment locator - -export const isUrlValid = (url: string) => { - return urlRegex.test(url); -}; From c0c44ec22c1101438065c28200de014b586216ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 08:40:49 +0200 Subject: [PATCH 38/73] add tests for number validation --- scm-ui/src/components/validation.test.js | 15 +++++++++ .../src/config/components/form/ConfigForm.js | 33 +++++++++++++++---- .../config/components/form/LoginAttempt.js | 15 ++++----- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/scm-ui/src/components/validation.test.js b/scm-ui/src/components/validation.test.js index 74b9debd28..c264b20a1c 100644 --- a/scm-ui/src/components/validation.test.js +++ b/scm-ui/src/components/validation.test.js @@ -85,3 +85,18 @@ describe("test mail validation", () => { } }); }); + +describe("test number validation", () => { + it("should return false", () => { + const invalid = ["1a", "35gu", "dj6", "45,5", "test"]; + for (let number of invalid) { + expect(validator.isNumberValid(number)).toBe(false); + } + }); + it("should return true", () => { + const valid = ["1", "35", "2", "235", "34.4"]; + for (let number of valid) { + expect(validator.isNumberValid(number)).toBe(true); + } + }); +}); diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index 430ad6abb2..0dbd4e83ba 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -21,7 +21,10 @@ type Props = { type State = { config: Config, showNotification: boolean, - loginAttemptError: boolean + error: { + loginAttemptLimitTimeout: boolean, + loginAttemptLimit: boolean + } }; class ConfigForm extends React.Component<Props, State> { @@ -54,7 +57,10 @@ class ConfigForm extends React.Component<Props, State> { _links: {} }, showNotification: false, - loginAttemptError: true + error: { + loginAttemptLimitTimeout: false, + loginAttemptLimit: false + } }; } @@ -151,25 +157,40 @@ class ConfigForm extends React.Component<Props, State> { <SubmitButton loading={loading} label={t("config-form.submit")} - disabled={!configUpdatePermission || !this.isValid()} + disabled={!configUpdatePermission || this.hasError()} /> </form> ); } - onChange = (isValid: boolean, changedValue: any, name: string) => { this.setState({ ...this.state, config: { ...this.state.config, [name]: changedValue + }, + error: { + ...this.state.error, + [name]: !isValid } }); }; - isValid = () => { - return this.state.loginAttemptError; + hasError = () => { + console.log("loginAttemtLimit " + this.state.error.loginAttemptLimit); + console.log( + "loginAttemtLimitTimeout " + this.state.error.loginAttemptLimitTimeout + ); + + console.log( + this.state.error.loginAttemptLimit || + this.state.error.loginAttemptLimitTimeout + ); + return ( + this.state.error.loginAttemptLimit || + this.state.error.loginAttemptLimitTimeout + ); }; onClose = () => { diff --git a/scm-ui/src/config/components/form/LoginAttempt.js b/scm-ui/src/config/components/form/LoginAttempt.js index 489828fd0b..da3a4ce0da 100644 --- a/scm-ui/src/config/components/form/LoginAttempt.js +++ b/scm-ui/src/config/components/form/LoginAttempt.js @@ -64,7 +64,11 @@ class LoginAttempt extends React.Component<Props, State> { ...this.state, loginAttemptLimitError: !validator.isNumberValid(value) }); - this.props.onChange(this.loginAttemptIsValid(), value, "loginAttemptLimit"); + this.props.onChange( + validator.isNumberValid(value), + value, + "loginAttemptLimit" + ); }; handleLoginAttemptLimitTimeoutChange = (value: string) => { @@ -73,18 +77,11 @@ class LoginAttempt extends React.Component<Props, State> { loginAttemptLimitTimeoutError: !validator.isNumberValid(value) }); this.props.onChange( - this.loginAttemptIsValid(), + validator.isNumberValid(value), value, "loginAttemptLimitTimeout" ); }; - - loginAttemptIsValid = () => { - return ( - this.state.loginAttemptLimitError || - this.state.loginAttemptLimitTimeoutError - ); - }; } export default translate("config")(LoginAttempt); From 3071cab9b7fa719190677fe073823fc6e768e619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 08:57:25 +0200 Subject: [PATCH 39/73] remove unused console.log --- scm-ui/src/config/components/form/ConfigForm.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index 0dbd4e83ba..f859eaf07d 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -178,11 +178,6 @@ class ConfigForm extends React.Component<Props, State> { }; hasError = () => { - console.log("loginAttemtLimit " + this.state.error.loginAttemptLimit); - console.log( - "loginAttemtLimitTimeout " + this.state.error.loginAttemptLimitTimeout - ); - console.log( this.state.error.loginAttemptLimit || this.state.error.loginAttemptLimitTimeout From 7efb082c43536edd254640f6120c0bac5137271f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 09:17:15 +0200 Subject: [PATCH 40/73] reset error when loading config again --- scm-ui/src/config/containers/GlobalConfig.js | 8 +++++++- scm-ui/src/config/modules/config.js | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index 2f4aa3be22..ee02da9709 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -9,7 +9,8 @@ import { modifyConfig, isModifyConfigPending, getConfigUpdatePermission, - getModifyConfigFailure + getModifyConfigFailure, + modifyConfigReset } from "../modules/config"; import connect from "react-redux/es/connect/connect"; import ErrorPage from "../../components/ErrorPage"; @@ -28,6 +29,7 @@ type Props = { // context objects t: string => string, fetchConfig: void => void, + configReset: void => void, history: History }; @@ -38,6 +40,7 @@ class GlobalConfig extends React.Component<Props> { }; componentDidMount() { + this.props.configReset(); this.props.fetchConfig(); } @@ -83,6 +86,9 @@ const mapDispatchToProps = dispatch => { }, modifyConfig: (config: Config, callback?: () => void) => { dispatch(modifyConfig(config, callback)); + }, + configReset: () => { + dispatch(modifyConfigReset()); } }; }; diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 4534dbaeb6..45c75348b6 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -16,6 +16,7 @@ export const MODIFY_CONFIG = "scm/config/MODIFY_CONFIG"; export const MODIFY_CONFIG_PENDING = `${MODIFY_CONFIG}_${types.PENDING_SUFFIX}`; export const MODIFY_CONFIG_SUCCESS = `${MODIFY_CONFIG}_${types.SUCCESS_SUFFIX}`; export const MODIFY_CONFIG_FAILURE = `${MODIFY_CONFIG}_${types.FAILURE_SUFFIX}`; +export const MODIFY_CONFIG_RESET = `${MODIFY_CONFIG}_${types.RESET_SUFFIX}`; const CONFIG_URL = "config"; const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2"; @@ -66,7 +67,7 @@ export function modifyConfig(config: Config, callback?: () => void) { return function(dispatch: Dispatch) { dispatch(modifyConfigPending(config)); return apiClient - .put(config._links.update.href, config, CONTENT_TYPE_CONFIG) + .put(config._links.update.href + "letsfail!", config, CONTENT_TYPE_CONFIG) .then(() => { dispatch(modifyConfigSuccess(config)); if (callback) { @@ -108,6 +109,12 @@ export function modifyConfigFailure(config: Config, error: Error): Action { }; } +export function modifyConfigReset() { + return { + type: MODIFY_CONFIG_RESET + }; +} + //reducer function reducer(state: any = {}, action: any = {}) { From 7d100edd3df4e21b84ae7f983ef44509d155ab65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 10:48:34 +0200 Subject: [PATCH 41/73] repair false modify url --- scm-ui/src/config/modules/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 45c75348b6..6fe1594236 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -67,7 +67,7 @@ export function modifyConfig(config: Config, callback?: () => void) { return function(dispatch: Dispatch) { dispatch(modifyConfigPending(config)); return apiClient - .put(config._links.update.href + "letsfail!", config, CONTENT_TYPE_CONFIG) + .put(config._links.update.href, config, CONTENT_TYPE_CONFIG) .then(() => { dispatch(modifyConfigSuccess(config)); if (callback) { From a7ba473fcc0053bcff6f557929ed288164e8dfed Mon Sep 17 00:00:00 2001 From: Mohamed Karray <mohamed.karray.sr@gmail.com> Date: Tue, 21 Aug 2018 10:58:47 +0200 Subject: [PATCH 42/73] #8771 use the AuthenticationExceptionMapper from v1 --- .../AuthorizationExceptionMapper.java | 50 ------------------- .../resources/PermissionRootResourceTest.java | 11 ++-- 2 files changed, 6 insertions(+), 55 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java deleted file mode 100644 index bf00bbfc5e..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright (c) 2014, Sebastian Sdorra All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. 2. Redistributions in - binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other - materials provided with the distribution. 3. Neither the name of SCM-Manager; - nor the names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - http://bitbucket.org/sdorra/scm-manager - - */ - - -package sonia.scm.api.v2.resources; - - -import org.apache.shiro.authz.AuthorizationException; -import sonia.scm.api.rest.StatusExceptionMapper; - -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.Provider; - -/** - * @author mkarray - * @since 2.0.0 - */ -@Provider -public class AuthorizationExceptionMapper extends StatusExceptionMapper<AuthorizationException> { - - public AuthorizationExceptionMapper() { - super(AuthorizationException.class, Response.Status.UNAUTHORIZED); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 23cd51c9f9..7236a98151 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -25,6 +25,7 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.api.rest.AuthorizationExceptionMapper; import sonia.scm.repository.*; import sonia.scm.web.VndMediaType; @@ -155,11 +156,11 @@ public class PermissionRootResourceTest { doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository); when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); return createDynamicTestsToAssertResponses( - requestGETPermission.expectedResponseStatus(401), - requestPOSTPermission.expectedResponseStatus(401), - requestGETAllPermissions.expectedResponseStatus(401), - requestDELETEPermission.expectedResponseStatus(401), - requestPUTPermission.expectedResponseStatus(401)); + requestGETPermission.expectedResponseStatus(403), + requestPOSTPermission.expectedResponseStatus(403), + requestGETAllPermissions.expectedResponseStatus(403), + requestDELETEPermission.expectedResponseStatus(403), + requestPUTPermission.expectedResponseStatus(403)); } @Test From cd0c1a5f2a35a046c986c75f4c4a5e7d3778e15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:25:47 +0200 Subject: [PATCH 43/73] remove unnecessary console.log --- scm-ui/src/config/components/form/ConfigForm.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index f859eaf07d..afb6b243d6 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -178,10 +178,6 @@ class ConfigForm extends React.Component<Props, State> { }; hasError = () => { - console.log( - this.state.error.loginAttemptLimit || - this.state.error.loginAttemptLimitTimeout - ); return ( this.state.error.loginAttemptLimit || this.state.error.loginAttemptLimitTimeout From 68f62b9b053f7e37421bd448a7166bd19db334d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:27:35 +0200 Subject: [PATCH 44/73] transfer remove button of each component to a global remove button --- scm-ui/src/components/buttons/index.js | 1 + .../buttons/RemoveAdminGroupButton.js | 34 ------------------ .../buttons/RemoveAdminUserButton.js | 34 ------------------ .../buttons/RemoveProxyExcludeButton.js | 36 ------------------- .../components/table/AdminGroupTable.js | 11 +++--- .../config/components/table/AdminUserTable.js | 11 +++--- .../components/table/ProxyExcludesTable.js | 11 +++--- 7 files changed, 19 insertions(+), 119 deletions(-) delete mode 100644 scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js delete mode 100644 scm-ui/src/config/components/buttons/RemoveAdminUserButton.js delete mode 100644 scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js diff --git a/scm-ui/src/components/buttons/index.js b/scm-ui/src/components/buttons/index.js index 8dfc6e00ef..e0d94d29b9 100644 --- a/scm-ui/src/components/buttons/index.js +++ b/scm-ui/src/components/buttons/index.js @@ -4,3 +4,4 @@ export { default as CreateButton } from "./CreateButton"; export { default as DeleteButton } from "./DeleteButton"; export { default as EditButton } from "./EditButton"; export { default as SubmitButton } from "./SubmitButton"; +export {default as RemoveEntryOfTableButton} from "./RemoveEntryOfTableButton"; diff --git a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js deleted file mode 100644 index 6a819b6972..0000000000 --- a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js +++ /dev/null @@ -1,34 +0,0 @@ -//@flow -import React from "react"; -import { DeleteButton } from "../../../components/buttons"; -import { translate } from "react-i18next"; -import classNames from "classnames"; - -type Props = { - t: string => string, - groupname: string, - removeGroup: string => void, - disabled: boolean -}; - -type State = {}; - -class RemoveAdminGroupButton extends React.Component<Props, State> { - render() { - const { t, groupname, removeGroup, disabled } = this.props; - return ( - <div className={classNames("is-pulled-right")}> - <DeleteButton - label={t("admin-settings.remove-group-button")} - action={(event: Event) => { - event.preventDefault(); - removeGroup(groupname); - }} - disabled={disabled} - /> - </div> - ); - } -} - -export default translate("config")(RemoveAdminGroupButton); diff --git a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js deleted file mode 100644 index 7d79a84030..0000000000 --- a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js +++ /dev/null @@ -1,34 +0,0 @@ -//@flow -import React from "react"; -import { DeleteButton } from "../../../components/buttons"; -import { translate } from "react-i18next"; -import classNames from "classnames"; - -type Props = { - t: string => string, - username: string, - removeUser: string => void, - disabled: boolean -}; - -type State = {}; - -class RemoveAdminUserButton extends React.Component<Props, State> { - render() { - const { t, username, removeUser, disabled } = this.props; - return ( - <div className={classNames("is-pulled-right")}> - <DeleteButton - label={t("admin-settings.remove-user-button")} - action={(event: Event) => { - event.preventDefault(); - removeUser(username); - }} - disabled={disabled} - /> - </div> - ); - } -} - -export default translate("config")(RemoveAdminUserButton); diff --git a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js deleted file mode 100644 index acdfd6292a..0000000000 --- a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js +++ /dev/null @@ -1,36 +0,0 @@ -//@flow -import React from "react"; -import {DeleteButton} from "../../../components/buttons"; -import { translate } from "react-i18next"; -import classNames from "classnames"; - -type Props = { - t: string => string, - proxyExcludeName: string, - removeProxyExclude: string => void, - disabled: boolean -}; - -type State = {}; - - - -class RemoveProxyExcludeButton extends React.Component<Props, State> { - render() { - const { t , proxyExcludeName, removeProxyExclude} = this.props; - return ( - <div className={classNames("is-pulled-right")}> - <DeleteButton - label={t("proxy-settings.remove-proxy-exclude-button")} - action={(event: Event) => { - event.preventDefault(); - removeProxyExclude(proxyExcludeName); - }} - disabled={this.props.disabled} - /> - </div> - ); - } -} - -export default translate("config")(RemoveProxyExcludeButton); diff --git a/scm-ui/src/config/components/table/AdminGroupTable.js b/scm-ui/src/config/components/table/AdminGroupTable.js index d5566e7ce1..fc05c75f06 100644 --- a/scm-ui/src/config/components/table/AdminGroupTable.js +++ b/scm-ui/src/config/components/table/AdminGroupTable.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import RemoveAdminGroupButton from "../buttons/RemoveAdminGroupButton"; +import { RemoveEntryOfTableButton } from "../../../components/buttons"; type Props = { adminGroups: string[], @@ -25,10 +25,11 @@ class AdminGroupTable extends React.Component<Props, State> { <tr key={group}> <td key={group}>{group}</td> <td> - <RemoveAdminGroupButton - groupname={group} - removeGroup={this.removeGroup} + <RemoveEntryOfTableButton + entryname={group} + removeEntry={this.removeEntry} disabled={disabled} + label={t("admin-settings.remove-group-button")} /> </td> </tr> @@ -40,7 +41,7 @@ class AdminGroupTable extends React.Component<Props, State> { ); } - removeGroup = (groupname: string) => { + removeEntry = (groupname: string) => { const newGroups = this.props.adminGroups.filter(name => name !== groupname); this.props.onChange(true, newGroups, "adminGroups"); }; diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js index caf58f2cc0..c622a0e027 100644 --- a/scm-ui/src/config/components/table/AdminUserTable.js +++ b/scm-ui/src/config/components/table/AdminUserTable.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import RemoveAdminUserButton from "../buttons/RemoveAdminUserButton"; +import { RemoveEntryOfTableButton } from "../../../components/buttons"; type Props = { adminUsers: string[], @@ -25,10 +25,11 @@ class AdminUserTable extends React.Component<Props, State> { <tr key={user}> <td key={user}>{user}</td> <td> - <RemoveAdminUserButton - username={user} - removeUser={this.removeUser} + <RemoveEntryOfTableButton + entryname={user} + removeEntry={this.removeEntry} disabled={disabled} + label={t("admin-settings.remove-user-button")} /> </td> </tr> @@ -40,7 +41,7 @@ class AdminUserTable extends React.Component<Props, State> { ); } - removeUser = (username: string) => { + removeEntry = (username: string) => { const newUsers = this.props.adminUsers.filter(name => name !== username); this.props.onChange(true, newUsers, "adminUsers"); }; diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js index ccee16cea5..4476442c48 100644 --- a/scm-ui/src/config/components/table/ProxyExcludesTable.js +++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import RemoveProxyExcludeButton from "../buttons/RemoveProxyExcludeButton"; +import { RemoveEntryOfTableButton } from "../../../components/buttons"; type Props = { proxyExcludes: string[], @@ -25,10 +25,11 @@ class ProxyExcludesTable extends React.Component<Props, State> { <tr key={excludes}> <td key={excludes}>{excludes}</td> <td> - <RemoveProxyExcludeButton - proxyExcludeName={excludes} - removeProxyExclude={this.removeProxyExclude} + <RemoveEntryOfTableButton + entryname={excludes} + removeEntry={this.removeEntry} disabled={this.props.disabled} + label={t("proxy-settings.remove-proxy-exclude-button")} /> </td> </tr> @@ -40,7 +41,7 @@ class ProxyExcludesTable extends React.Component<Props, State> { ); } - removeProxyExclude = (excludename: string) => { + removeEntry = (excludename: string) => { const newExcludes = this.props.proxyExcludes.filter( name => name !== excludename ); From d5815142c0d3296bcc83cb0d789d505cddde8e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:28:01 +0200 Subject: [PATCH 45/73] add global remove button for table entries --- .../buttons/RemoveEntryOfTableButton.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 scm-ui/src/components/buttons/RemoveEntryOfTableButton.js diff --git a/scm-ui/src/components/buttons/RemoveEntryOfTableButton.js b/scm-ui/src/components/buttons/RemoveEntryOfTableButton.js new file mode 100644 index 0000000000..280226b938 --- /dev/null +++ b/scm-ui/src/components/buttons/RemoveEntryOfTableButton.js @@ -0,0 +1,33 @@ +//@flow +import React from "react"; +import { DeleteButton } from "."; +import classNames from "classnames"; + +type Props = { + entryname: string, + removeEntry: string => void, + disabled: boolean, + label: string +}; + +type State = {}; + +class RemoveEntryOfTableButton extends React.Component<Props, State> { + render() { + const { label, entryname, removeEntry, disabled } = this.props; + return ( + <div className={classNames("is-pulled-right")}> + <DeleteButton + label={label} + action={(event: Event) => { + event.preventDefault(); + removeEntry(entryname); + }} + disabled={disabled} + /> + </div> + ); + } +} + +export default RemoveEntryOfTableButton; From d8e4c7d63ac0894d9ffc8665812e2c0cc01bec68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:31:36 +0200 Subject: [PATCH 46/73] use global remove button for table entries in groups --- .../src/groups/components/MemberNameTable.js | 12 ++++--- .../components/buttons/RemoveMemberButton.js | 34 ------------------- 2 files changed, 7 insertions(+), 39 deletions(-) delete mode 100644 scm-ui/src/groups/components/buttons/RemoveMemberButton.js diff --git a/scm-ui/src/groups/components/MemberNameTable.js b/scm-ui/src/groups/components/MemberNameTable.js index 699c009413..0ae20da7d2 100644 --- a/scm-ui/src/groups/components/MemberNameTable.js +++ b/scm-ui/src/groups/components/MemberNameTable.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import RemoveMemberButton from "./buttons/RemoveMemberButton"; +import { RemoveEntryOfTableButton } from "../../components/buttons"; type Props = { members: string[], @@ -24,9 +24,11 @@ class MemberNameTable extends React.Component<Props, State> { <tr key={member}> <td key={member}>{member}</td> <td> - <RemoveMemberButton - membername={member} - removeMember={this.removeMember} + <RemoveEntryOfTableButton + entryname={member} + removeEntry={this.removeEntry} + disabled={false} + label={t("remove-member-button.label")} /> </td> </tr> @@ -38,7 +40,7 @@ class MemberNameTable extends React.Component<Props, State> { ); } - removeMember = (membername: string) => { + removeEntry = (membername: string) => { const newMembers = this.props.members.filter(name => name !== membername); this.props.memberListChanged(newMembers); }; diff --git a/scm-ui/src/groups/components/buttons/RemoveMemberButton.js b/scm-ui/src/groups/components/buttons/RemoveMemberButton.js deleted file mode 100644 index 40c7b39cc0..0000000000 --- a/scm-ui/src/groups/components/buttons/RemoveMemberButton.js +++ /dev/null @@ -1,34 +0,0 @@ -//@flow -import React from "react"; -import { DeleteButton } from "../../../components/buttons"; -import { translate } from "react-i18next"; -import classNames from "classnames"; - -type Props = { - t: string => string, - membername: string, - removeMember: string => void -}; - -type State = {}; - - - -class RemoveMemberButton extends React.Component<Props, State> { - render() { - const { t , membername, removeMember} = this.props; - return ( - <div className={classNames("is-pulled-right")}> - <DeleteButton - label={t("remove-member-button.label")} - action={(event: Event) => { - event.preventDefault(); - removeMember(membername); - }} - /> - </div> - ); - } -} - -export default translate("groups")(RemoveMemberButton); From d9e66fdbaa120921de62cf9893244f6808df33cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:33:48 +0200 Subject: [PATCH 47/73] delete of unnecessary comments --- .../components/fields/AddAdminGroupField.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/scm-ui/src/config/components/fields/AddAdminGroupField.js b/scm-ui/src/config/components/fields/AddAdminGroupField.js index 0991056b66..21cba365ed 100644 --- a/scm-ui/src/config/components/fields/AddAdminGroupField.js +++ b/scm-ui/src/config/components/fields/AddAdminGroupField.js @@ -12,16 +12,14 @@ type Props = { }; type State = { - groupToAdd: string, - //validationError: boolean + groupToAdd: string }; class AddAdminGroupField extends React.Component<Props, State> { constructor(props) { super(props); this.state = { - groupToAdd: "", - //validationError: false + groupToAdd: "" }; } @@ -30,7 +28,6 @@ class AddAdminGroupField extends React.Component<Props, State> { return ( <div className="field"> <InputField - label={t("admin-settings.add-group-textfield")} errorMessage={t("admin-settings.add-group-error")} onChange={this.handleAddGroupChange} @@ -43,7 +40,6 @@ class AddAdminGroupField extends React.Component<Props, State> { label={t("admin-settings.add-group-button")} action={this.addButtonClicked} disabled={disabled} - //disabled={!isMemberNameValid(this.state.memberToAdd)} /> </div> ); @@ -56,17 +52,14 @@ class AddAdminGroupField extends React.Component<Props, State> { appendGroup = () => { const { groupToAdd } = this.state; - //if (isMemberNameValid(memberToAdd)) { - this.props.addGroup(groupToAdd); - this.setState({ ...this.state, groupToAdd: "" }); - // } + this.props.addGroup(groupToAdd); + this.setState({ ...this.state, groupToAdd: "" }); }; handleAddGroupChange = (groupname: string) => { this.setState({ ...this.state, - groupToAdd: groupname, - //validationError: membername.length > 0 && !isMemberNameValid(membername) + groupToAdd: groupname }); }; } From 778881a2fbe1c4f813d809bf69a91030b6453f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Tue, 21 Aug 2018 11:47:02 +0200 Subject: [PATCH 48/73] Peer-Review --- .../PermissionAlreadyExistsException.java | 2 +- .../AuthorizationExceptionMapper.java | 1 - ...ermissionAlreadyExistsExceptionMapper.java | 1 - .../PermissionNotFoundExceptionMapper.java | 1 - .../v2/resources/PermissionRootResource.java | 66 +++++++++++-------- .../RepositoryNotFoundExceptionMapper.java | 1 - .../resources/PermissionRootResourceTest.java | 53 +++++++-------- 7 files changed, 64 insertions(+), 61 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java index aeaf64a3e9..43ad0a5e1d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java @@ -5,7 +5,7 @@ import java.text.MessageFormat; public class PermissionAlreadyExistsException extends RepositoryException { public PermissionAlreadyExistsException(Repository repository, String permissionName) { - super(MessageFormat.format("the permission {0} of the repository {1}/{2} is already exists", permissionName, repository.getNamespace(), repository.getName())); + super(MessageFormat.format("the permission {0} of the repository {1}/{2} already exists", permissionName, repository.getNamespace(), repository.getName())); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java index bf00bbfc5e..1e120df966 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java @@ -38,7 +38,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; /** - * @author mkarray * @since 2.0.0 */ @Provider diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java index 0cf83f097a..d654f8bca7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java @@ -38,7 +38,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; /** - * @author mkarray * @since 2.0.0 */ @Provider diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java index 42e341ce0d..61d62ecac5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java @@ -38,7 +38,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; /** - * @author mkarray * @since 2.0.0 */ @Provider diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index e9e1d38d95..716e240bee 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -6,11 +6,25 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; -import sonia.scm.repository.*; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.PermissionAlreadyExistsException; +import sonia.scm.repository.PermissionNotFoundException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; import javax.ws.rs.core.Response; import java.net.URI; import java.util.List; @@ -58,7 +72,7 @@ public class PermissionRootResource { checkPermissionAlreadyExists(permission, repository); repository.getPermissions().add(dtoToModelMapper.map(permission)); manager.modify(repository); - return Response.created(URI.create(resourceLinks.permission().self(namespace,name,permission.getName()))).build(); + return Response.created(URI.create(resourceLinks.permission().self(namespace, name, permission.getName()))).build(); } @@ -84,8 +98,8 @@ public class PermissionRootResource { return Response.ok( repository.getPermissions() .stream() - .filter(permission -> StringUtils.isNotBlank(permission.getName()) && permission.getName().equals(permissionName)) - .map(permission -> modelToDtoMapper.map(permission, new NamespaceAndName(repository.getNamespace(),repository.getName()))) + .filter(permission -> permissionName.equals(permission.getName())) + .map(permission -> modelToDtoMapper.map(permission, new NamespaceAndName(repository.getNamespace(), repository.getName()))) .findFirst() .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) ).build(); @@ -113,7 +127,7 @@ public class PermissionRootResource { Repository repository = checkPermission(namespace, name); List<PermissionDto> permissionDtoList = repository.getPermissions() .stream() - .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(),repository.getName()))) + .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(), repository.getName()))) .collect(Collectors.toList()); return Response.ok(permissionDtoList).build(); } @@ -136,56 +150,55 @@ public class PermissionRootResource { @Consumes(VndMediaType.PERMISSION) @Path("{permission-name}") public Response update(@PathParam("namespace") String namespace, - @PathParam("name") String name, - @PathParam("permission-name") String permissionName, - PermissionDto permission) throws RepositoryException { + @PathParam("name") String name, + @PathParam("permission-name") String permissionName, + PermissionDto permission) throws RepositoryException { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); Repository repository = checkPermission(namespace, name); repository.getPermissions() .stream() - .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) .findFirst() - .map(p -> dtoToModelMapper.map(p, permission)) + .map(p -> dtoToModelMapper.map(p, permission)) .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) - ; + ; manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); return Response.noContent().build(); } - /** + /** * Update a permission to the user or group managed by the repository * * @param permissionName permission to delete * @return a web response with the status code 204 */ - @DELETE - @StatusCodes({ - @ResponseCode(code = 204, condition = "delete success or nothing to delete"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) + @DELETE + @StatusCodes({ + @ResponseCode(code = 204, condition = "delete success or nothing to delete"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) @Path("{permission-name}") public Response delete(@PathParam("namespace") String namespace, - @PathParam("name") String name, - @PathParam("permission-name") String permissionName) throws RepositoryException { + @PathParam("name") String name, + @PathParam("permission-name") String permissionName) throws RepositoryException { log.info("try to delete the permission with name: {}.", permissionName); Repository repository = checkPermission(namespace, name); repository.getPermissions() .stream() - .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) .findFirst() .ifPresent(p -> repository.getPermissions().remove(p)) - ; + ; manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); return Response.noContent().build(); } - /** * check if the actual user is permitted to manage the repository permissions * return the repository if the user is permitted @@ -207,6 +220,7 @@ public class PermissionRootResource { /** * throw exception if the user is not permitted + * * @param repository */ protected void checkUserPermitted(Repository repository) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java index 2116b8e31c..dcab8e4fc0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java @@ -38,7 +38,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; /** - * @author mkarray * @since 2.0.0 */ @Provider diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 23cd51c9f9..bf78e7fe68 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -2,8 +2,6 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.sdorra.shiro.ShiroRule; -import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableList; import lombok.ToString; import lombok.extern.slf4j.Slf4j; @@ -15,7 +13,6 @@ import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.spi.HttpRequest; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -25,7 +22,11 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.repository.*; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Permission; +import sonia.scm.repository.PermissionType; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; import java.io.IOException; @@ -42,14 +43,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; -@SubjectAware( - username = "trillian", - password = "secret", - configuration = "classpath:sonia/scm/repository/shiro.ini" -) @RunWith(MockitoJUnitRunner.Silent.class) @Slf4j public class PermissionRootResourceTest { @@ -93,9 +93,6 @@ public class PermissionRootResourceTest { private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Rule - public ShiroRule shiro = new ShiroRule(); - @Mock private RepositoryManager repositoryManager; @@ -163,13 +160,13 @@ public class PermissionRootResourceTest { } @Test - public void shouldGetAllPermissions() { + public void shouldGetAllPermissions() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS)); } @Test - public void shouldGetPermissionByName() { + public void shouldGetPermissionByName() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission expectedPermission = TEST_PERMISSIONS.get(0); assertExpectedRequest(requestGETPermission @@ -192,7 +189,7 @@ public class PermissionRootResourceTest { } @Test - public void shouldGetCreatedPermissions() { + public void shouldGetCreatedPermissions() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true); ArrayList<Permission> permissions = Lists.newArrayList(TEST_PERMISSIONS); @@ -209,7 +206,7 @@ public class PermissionRootResourceTest { } @Test - public void shouldNotAddExistingPermission() { + public void shouldNotAddExistingPermission() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission newPermission = TEST_PERMISSIONS.get(0); assertExpectedRequest(requestPOSTPermission @@ -219,7 +216,7 @@ public class PermissionRootResourceTest { } @Test - public void shouldGetUpdatedPermissions() { + public void shouldGetUpdatedPermissions() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission modifiedPermission = TEST_PERMISSIONS.get(0); // modify the type to owner @@ -238,7 +235,7 @@ public class PermissionRootResourceTest { @Test - public void shouldDeletePermissions() { + public void shouldDeletePermissions() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission deletedPermission = TEST_PERMISSIONS.get(0); ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); @@ -253,7 +250,7 @@ public class PermissionRootResourceTest { } @Test - public void deletingNotExistingPermissionShouldProcess() { + public void deletingNotExistingPermissionShouldProcess() throws URISyntaxException { authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); Permission deletedPermission = TEST_PERMISSIONS.get(0); ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); @@ -275,7 +272,7 @@ public class PermissionRootResourceTest { assertGettingExpectedPermissions(expectedPermissions); } - private void assertGettingExpectedPermissions(ImmutableList<Permission> expectedPermissions) { + private void assertGettingExpectedPermissions(ImmutableList<Permission> expectedPermissions) throws URISyntaxException { assertExpectedRequest(requestGETAllPermissions .expectedResponseStatus(200) .responseValidator((response) -> { @@ -337,17 +334,13 @@ public class PermissionRootResourceTest { .map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry))); } - private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) { + private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) throws URISyntaxException { MockHttpResponse response = new MockHttpResponse(); HttpRequest request = null; - try { - request = MockHttpRequest - .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) - .content(entry.content) - .contentType(VndMediaType.PERMISSION); - } catch (URISyntaxException e) { - fail(e.getMessage()); - } + request = MockHttpRequest + .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) + .content(entry.content) + .contentType(VndMediaType.PERMISSION); dispatcher.invoke(request, response); log.info("Test the Request :{}", entry); assertThat(entry.expectedResponseStatus) From cb59226023e97721cfedf42095d055e3aa438ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Tue, 21 Aug 2018 09:57:36 +0000 Subject: [PATCH 49/73] Close branch feature/permission_endpoint_v2 From ebed0d0997a9d9cf70e88371a535a0bdcac0f686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:59:12 +0200 Subject: [PATCH 50/73] transfer field to add new entry of each component to global one --- .../components/forms/AddEntryToTableField.js | 68 +++++++++++++++++ .../components/fields/AddAdminGroupField.js | 67 ----------------- .../components/fields/AddAdminUserField.js | 73 ------------------- .../components/fields/AddProxyExcludeField.js | 73 ------------------- .../config/components/form/AdminSettings.js | 18 ++++- .../config/components/form/ProxySettings.js | 10 ++- .../src/groups/components/AddMemberField.js | 71 ------------------ scm-ui/src/groups/components/GroupForm.js | 10 ++- 8 files changed, 97 insertions(+), 293 deletions(-) create mode 100644 scm-ui/src/components/forms/AddEntryToTableField.js delete mode 100644 scm-ui/src/config/components/fields/AddAdminGroupField.js delete mode 100644 scm-ui/src/config/components/fields/AddAdminUserField.js delete mode 100644 scm-ui/src/config/components/fields/AddProxyExcludeField.js delete mode 100644 scm-ui/src/groups/components/AddMemberField.js diff --git a/scm-ui/src/components/forms/AddEntryToTableField.js b/scm-ui/src/components/forms/AddEntryToTableField.js new file mode 100644 index 0000000000..1770e07807 --- /dev/null +++ b/scm-ui/src/components/forms/AddEntryToTableField.js @@ -0,0 +1,68 @@ +//@flow +import React from "react"; + +import { AddButton } from "../buttons"; +import InputField from "./InputField"; + +type Props = { + addEntry: string => void, + disabled: boolean, + buttonLabel: string, + fieldLabel: string, + errorMessage: string +}; + +type State = { + entryToAdd: string +}; + +class AddEntryToTableField extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + entryToAdd: "" + }; + } + + render() { + const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props; + return ( + <div className="field"> + <InputField + label={fieldLabel} + errorMessage={errorMessage} + onChange={this.handleAddEntryChange} + validationError={false} + value={this.state.entryToAdd} + onReturnPressed={this.appendEntry} + disabled={disabled} + /> + <AddButton + label={buttonLabel} + action={this.addButtonClicked} + disabled={disabled} + /> + </div> + ); + } + + addButtonClicked = (event: Event) => { + event.preventDefault(); + this.appendEntry(); + }; + + appendEntry = () => { + const { entryToAdd } = this.state; + this.props.addEntry(entryToAdd); + this.setState({ ...this.state, entryToAdd: "" }); + }; + + handleAddEntryChange = (entryname: string) => { + this.setState({ + ...this.state, + entryToAdd: entryname + }); + }; +} + +export default AddEntryToTableField; diff --git a/scm-ui/src/config/components/fields/AddAdminGroupField.js b/scm-ui/src/config/components/fields/AddAdminGroupField.js deleted file mode 100644 index 21cba365ed..0000000000 --- a/scm-ui/src/config/components/fields/AddAdminGroupField.js +++ /dev/null @@ -1,67 +0,0 @@ -//@flow -import React from "react"; - -import { translate } from "react-i18next"; -import { AddButton } from "../../../components/buttons"; -import InputField from "../../../components/forms/InputField"; - -type Props = { - t: string => string, - addGroup: string => void, - disabled: boolean -}; - -type State = { - groupToAdd: string -}; - -class AddAdminGroupField extends React.Component<Props, State> { - constructor(props) { - super(props); - this.state = { - groupToAdd: "" - }; - } - - render() { - const { t, disabled } = this.props; - return ( - <div className="field"> - <InputField - label={t("admin-settings.add-group-textfield")} - errorMessage={t("admin-settings.add-group-error")} - onChange={this.handleAddGroupChange} - validationError={false} - value={this.state.groupToAdd} - onReturnPressed={this.appendGroup} - disabled={disabled} - /> - <AddButton - label={t("admin-settings.add-group-button")} - action={this.addButtonClicked} - disabled={disabled} - /> - </div> - ); - } - - addButtonClicked = (event: Event) => { - event.preventDefault(); - this.appendGroup(); - }; - - appendGroup = () => { - const { groupToAdd } = this.state; - this.props.addGroup(groupToAdd); - this.setState({ ...this.state, groupToAdd: "" }); - }; - - handleAddGroupChange = (groupname: string) => { - this.setState({ - ...this.state, - groupToAdd: groupname - }); - }; -} - -export default translate("config")(AddAdminGroupField); diff --git a/scm-ui/src/config/components/fields/AddAdminUserField.js b/scm-ui/src/config/components/fields/AddAdminUserField.js deleted file mode 100644 index 025ed6cb4d..0000000000 --- a/scm-ui/src/config/components/fields/AddAdminUserField.js +++ /dev/null @@ -1,73 +0,0 @@ -//@flow -import React from "react"; - -import { translate } from "react-i18next"; -import { AddButton } from "../../../components/buttons"; -import InputField from "../../../components/forms/InputField"; - -type Props = { - t: string => string, - addUser: string => void, - disabled: boolean -}; - -type State = { - userToAdd: string - //validationError: boolean -}; - -class AddAdminUserField extends React.Component<Props, State> { - constructor(props) { - super(props); - this.state = { - userToAdd: "" - //validationError: false - }; - } - - render() { - const { t, disabled } = this.props; - return ( - <div className="field"> - <InputField - label={t("admin-settings.add-user-textfield")} - errorMessage={t("admin-settings.add-user-error")} - onChange={this.handleAddUserChange} - validationError={false} - value={this.state.userToAdd} - onReturnPressed={this.appendUser} - disabled={disabled} - /> - <AddButton - label={t("admin-settings.add-user-button")} - action={this.addButtonClicked} - disabled={disabled} - //disabled={!isMemberNameValid(this.state.memberToAdd)} - /> - </div> - ); - } - - addButtonClicked = (event: Event) => { - event.preventDefault(); - this.appendUser(); - }; - - appendUser = () => { - const { userToAdd } = this.state; - //if (isMemberNameValid(memberToAdd)) { - this.props.addUser(userToAdd); - this.setState({ ...this.state, userToAdd: "" }); - // } - }; - - handleAddUserChange = (username: string) => { - this.setState({ - ...this.state, - userToAdd: username - //validationError: membername.length > 0 && !isMemberNameValid(membername) - }); - }; -} - -export default translate("config")(AddAdminUserField); diff --git a/scm-ui/src/config/components/fields/AddProxyExcludeField.js b/scm-ui/src/config/components/fields/AddProxyExcludeField.js deleted file mode 100644 index bb7e005b02..0000000000 --- a/scm-ui/src/config/components/fields/AddProxyExcludeField.js +++ /dev/null @@ -1,73 +0,0 @@ -//@flow -import React from "react"; - -import { translate } from "react-i18next"; -import { AddButton } from "../../../components/buttons"; -import InputField from "../../../components/forms/InputField"; - -type Props = { - t: string => string, - addProxyExclude: string => void, - disabled: boolean -}; - -type State = { - proxyExcludeToAdd: string - //validationError: boolean -}; - -class AddProxyExcludeField extends React.Component<Props, State> { - constructor(props) { - super(props); - this.state = { - proxyExcludeToAdd: "" - //validationError: false - }; - } - - render() { - const { t } = this.props; - return ( - <div className="field"> - <InputField - label={t("proxy-settings.add-proxy-exclude-textfield")} - errorMessage={t("proxy-settings.add-proxy-exclude-error")} - onChange={this.handleAddProxyExcludeChange} - validationError={false} - value={this.state.proxyExcludeToAdd} - onReturnPressed={this.appendProxyExclude} - disabled={this.props.disabled} - /> - <AddButton - label={t("proxy-settings.add-proxy-exclude-button")} - action={this.addButtonClicked} - disabled={this.props.disabled} - //disabled={!isMemberNameValid(this.state.memberToAdd)} - /> - </div> - ); - } - - addButtonClicked = (event: Event) => { - event.preventDefault(); - this.appendProxyExclude(); - }; - - appendProxyExclude = () => { - const { proxyExcludeToAdd } = this.state; - //if (isMemberNameValid(memberToAdd)) { - this.props.addProxyExclude(proxyExcludeToAdd); - this.setState({ ...this.state, proxyExcludeToAdd: "" }); - // } - }; - - handleAddProxyExcludeChange = (username: string) => { - this.setState({ - ...this.state, - proxyExcludeToAdd: username - //validationError: membername.length > 0 && !isMemberNameValid(membername) - }); - }; -} - -export default translate("config")(AddProxyExcludeField); diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index b7a39f234c..ea1a88fe6f 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -4,8 +4,7 @@ import { translate } from "react-i18next"; import Subtitle from "../../../components/layout/Subtitle"; import AdminGroupTable from "../table/AdminGroupTable"; import AdminUserTable from "../table/AdminUserTable"; -import AddAdminGroupField from "../fields/AddAdminGroupField"; -import AddAdminUserField from "../fields/AddAdminUserField"; +import AddEntryToTableField from "../../../components/forms/AddEntryToTableField"; type Props = { adminGroups: string[], @@ -29,8 +28,14 @@ class AdminSettings extends React.Component<Props> { } disabled={!hasUpdatePermission} /> - <AddAdminGroupField addGroup={this.addGroup} disabled={!hasUpdatePermission} + <AddEntryToTableField + addEntry={this.addGroup} + disabled={!hasUpdatePermission} + buttonLabel={t("admin-settings.add-group-button")} + fieldLabel={t("admin-settings.add-group-textfield")} + errorMessage={t("admin-settings.add-group-error")} /> + <AdminUserTable adminUsers={adminUsers} onChange={(isValid, changedValue, name) => @@ -38,7 +43,12 @@ class AdminSettings extends React.Component<Props> { } disabled={!hasUpdatePermission} /> - <AddAdminUserField addUser={this.addUser} disabled={!hasUpdatePermission} + <AddEntryToTableField + addEntry={this.addUser} + disabled={!hasUpdatePermission} + buttonLabel={t("admin-settings.add-user-button")} + fieldLabel={t("admin-settings.add-user-textfield")} + errorMessage={t("admin-settings.add-user-error")} /> </div> ); diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index eb434513fe..a1d513f9b1 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -4,7 +4,7 @@ import { translate } from "react-i18next"; import { Checkbox, InputField } from "../../../components/forms/index"; import Subtitle from "../../../components/layout/Subtitle"; import ProxyExcludesTable from "../table/ProxyExcludesTable"; -import AddProxyExcludeField from "../fields/AddProxyExcludeField"; +import AddEntryToTableField from "../../../components/forms/AddEntryToTableField"; type Props = { proxyPassword: string, @@ -65,6 +65,7 @@ class ProxySettings extends React.Component<Props> { onChange={this.handleProxyUserChange} disabled={!enableProxy || !hasUpdatePermission} /> + <ProxyExcludesTable proxyExcludes={proxyExcludes} onChange={(isValid, changedValue, name) => @@ -72,9 +73,12 @@ class ProxySettings extends React.Component<Props> { } disabled={!enableProxy || !hasUpdatePermission} /> - <AddProxyExcludeField - addProxyExclude={this.addProxyExclude} + <AddEntryToTableField + addEntry={this.addProxyExclude} disabled={!enableProxy || !hasUpdatePermission} + buttonLabel={t("proxy-settings.add-proxy-exclude-button")} + fieldLabel={t("proxy-settings.add-proxy-exclude-textfield")} + errorMessage={t("proxy-settings.add-proxy-exclude-error")} /> </div> ); diff --git a/scm-ui/src/groups/components/AddMemberField.js b/scm-ui/src/groups/components/AddMemberField.js deleted file mode 100644 index 6237e88291..0000000000 --- a/scm-ui/src/groups/components/AddMemberField.js +++ /dev/null @@ -1,71 +0,0 @@ -//@flow -import React from "react"; - -import { translate } from "react-i18next"; -import { AddButton } from "../../components/buttons"; -import InputField from "../../components/forms/InputField"; -import { isMemberNameValid } from "./groupValidation"; - -type Props = { - t: string => string, - addMember: string => void -}; - -type State = { - memberToAdd: string, - validationError: boolean -}; - -class AddMemberField extends React.Component<Props, State> { - constructor(props) { - super(props); - this.state = { - memberToAdd: "", - validationError: false - }; - } - - render() { - const { t } = this.props; - return ( - <div className="field"> - <InputField - label={t("add-member-textfield.label")} - errorMessage={t("add-member-textfield.error")} - onChange={this.handleAddMemberChange} - validationError={this.state.validationError} - value={this.state.memberToAdd} - onReturnPressed={this.appendMember} - /> - <AddButton - label={t("add-member-button.label")} - action={this.addButtonClicked} - disabled={!isMemberNameValid(this.state.memberToAdd)} - /> - </div> - ); - } - - addButtonClicked = (event: Event) => { - event.preventDefault(); - this.appendMember(); - }; - - appendMember = () => { - const { memberToAdd } = this.state; - if (isMemberNameValid(memberToAdd)) { - this.props.addMember(memberToAdd); - this.setState({ ...this.state, memberToAdd: "" }); - } - }; - - handleAddMemberChange = (membername: string) => { - this.setState({ - ...this.state, - memberToAdd: membername, - validationError: membername.length > 0 && !isMemberNameValid(membername) - }); - }; -} - -export default translate("groups")(AddMemberField); diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index 2e941ea7eb..a989bdc1c5 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -6,9 +6,9 @@ import { SubmitButton } from "../../components/buttons"; import { translate } from "react-i18next"; import type { Group } from "../types/Group"; import * as validator from "./groupValidation"; -import AddMemberField from "./AddMemberField"; import MemberNameTable from "./MemberNameTable"; import Textarea from "../../components/forms/Textarea"; +import AddEntryToTableField from "../../components/forms/AddEntryToTableField"; type Props = { t: string => string, @@ -96,7 +96,13 @@ class GroupForm extends React.Component<Props, State> { members={this.state.group.members} memberListChanged={this.memberListChanged} /> - <AddMemberField addMember={this.addMember} /> + <AddEntryToTableField + addEntry={this.addMember} + disabled={false} + buttonLabel={t("add-member-button.label")} + fieldLabel={t("add-member-textfield.label")} + errorMessage={t("add-member-textfield.error")} + /> <SubmitButton disabled={!this.isValid()} label={t("group-form.submit")} From 18bf13a6af73b4df210537c88053360f7047a5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 13:06:52 +0200 Subject: [PATCH 51/73] formatting --- scm-ui/src/config/components/form/AdminSettings.js | 1 - scm-ui/src/config/components/form/ProxySettings.js | 1 - 2 files changed, 2 deletions(-) diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index ea1a88fe6f..98ab7350fb 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -35,7 +35,6 @@ class AdminSettings extends React.Component<Props> { fieldLabel={t("admin-settings.add-group-textfield")} errorMessage={t("admin-settings.add-group-error")} /> - <AdminUserTable adminUsers={adminUsers} onChange={(isValid, changedValue, name) => diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index a1d513f9b1..42cd0ab228 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -65,7 +65,6 @@ class ProxySettings extends React.Component<Props> { onChange={this.handleProxyUserChange} disabled={!enableProxy || !hasUpdatePermission} /> - <ProxyExcludesTable proxyExcludes={proxyExcludes} onChange={(isValid, changedValue, name) => From 6f5789968705b9b692bf22bc17f19fb851ffeca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@web.de> Date: Tue, 21 Aug 2018 11:13:22 +0000 Subject: [PATCH 52/73] scm.iml edited online with Bitbucket --- scm.iml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 scm.iml diff --git a/scm.iml b/scm.iml deleted file mode 100644 index 20f9f4b564..0000000000 --- a/scm.iml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4"> - <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8"> - <output url="file://$MODULE_DIR$/target/classes" /> - <output-test url="file://$MODULE_DIR$/target/test-classes" /> - <content url="file://$MODULE_DIR$"> - <excludeFolder url="file://$MODULE_DIR$/target" /> - </content> - <orderEntry type="inheritedJdk" /> - <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" /> - <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" /> - <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-library:1.3" level="project" /> - <orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-all:1.10.19" level="project" /> - <orderEntry type="library" scope="TEST" name="Maven: org.assertj:assertj-core:3.10.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.cloudogu:ces-build-lib:9aadeeb" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.cloudbees:groovy-cps:1.21" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.guava:guava:11.0.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.code.findbugs:jsr305:1.3.9" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.codehaus.groovy:groovy-all:2.4.11" level="project" /> - </component> -</module> \ No newline at end of file From 298430a90fa706d19eae329348547a0ac0b46857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Tue, 21 Aug 2018 13:38:37 +0200 Subject: [PATCH 53/73] Satisfy sonar --- .../api/v2/resources/PermissionDtoToPermissionMapper.java | 2 +- .../scm/api/v2/resources/PermissionRootResource.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java index 9128479836..29cb7e496a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java @@ -16,6 +16,6 @@ public abstract class PermissionDtoToPermissionMapper { * @param permissionDto the source dto * @return the mapped target permission object */ - public abstract Permission map(@MappingTarget Permission target, PermissionDto permissionDto); + public abstract Permission modify(@MappingTarget Permission target, PermissionDto permissionDto); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index 716e240bee..fdce46b727 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -7,6 +7,7 @@ import com.webcohesion.enunciate.metadata.rs.TypeHint; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Permission; import sonia.scm.repository.PermissionAlreadyExistsException; import sonia.scm.repository.PermissionNotFoundException; import sonia.scm.repository.Repository; @@ -155,13 +156,12 @@ public class PermissionRootResource { PermissionDto permission) throws RepositoryException { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); Repository repository = checkPermission(namespace, name); - repository.getPermissions() + Permission existingPermission = repository.getPermissions() .stream() .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) .findFirst() - .map(p -> dtoToModelMapper.map(p, permission)) - .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) - ; + .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)); + dtoToModelMapper.modify(existingPermission, permission); manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); return Response.noContent().build(); From cd54086e7da755f29ff6ebd4405b782b1fceba7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Tue, 21 Aug 2018 14:18:03 +0200 Subject: [PATCH 54/73] Remove redundant permission check --- .../PermissionDtoToPermissionMapper.java | 2 +- .../v2/resources/PermissionRootResource.java | 28 ++++--------------- .../resources/PermissionRootResourceTest.java | 15 +++------- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java index 29cb7e496a..1e90c23aa7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java @@ -16,6 +16,6 @@ public abstract class PermissionDtoToPermissionMapper { * @param permissionDto the source dto * @return the mapped target permission object */ - public abstract Permission modify(@MappingTarget Permission target, PermissionDto permissionDto); + public abstract void modify(@MappingTarget Permission target, PermissionDto permissionDto); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index fdce46b727..d08f2e7057 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -14,7 +14,6 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryException; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.RepositoryPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -69,7 +68,7 @@ public class PermissionRootResource { @Consumes(VndMediaType.PERMISSION) public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws RepositoryException { log.info("try to add new permission: {}", permission); - Repository repository = checkPermission(namespace, name); + Repository repository = load(namespace, name); checkPermissionAlreadyExists(permission, repository); repository.getPermissions().add(dtoToModelMapper.map(permission)); manager.modify(repository); @@ -95,7 +94,7 @@ public class PermissionRootResource { @TypeHint(PermissionDto.class) @Path("{permission-name}") public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { - Repository repository = checkPermission(namespace, name); + Repository repository = load(namespace, name); return Response.ok( repository.getPermissions() .stream() @@ -125,7 +124,7 @@ public class PermissionRootResource { @TypeHint(PermissionDto.class) @Path("") public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { - Repository repository = checkPermission(namespace, name); + Repository repository = load(namespace, name); List<PermissionDto> permissionDtoList = repository.getPermissions() .stream() .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(), repository.getName()))) @@ -155,7 +154,7 @@ public class PermissionRootResource { @PathParam("permission-name") String permissionName, PermissionDto permission) throws RepositoryException { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); - Repository repository = checkPermission(namespace, name); + Repository repository = load(namespace, name); Permission existingPermission = repository.getPermissions() .stream() .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) @@ -186,7 +185,7 @@ public class PermissionRootResource { @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { log.info("try to delete the permission with name: {}.", permissionName); - Repository repository = checkPermission(namespace, name); + Repository repository = load(namespace, name); repository.getPermissions() .stream() .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) @@ -208,26 +207,11 @@ public class PermissionRootResource { * @return the repository if the user is permitted * @throws RepositoryNotFoundException if the repository does not exists */ - private Repository checkPermission(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { + private Repository load(String namespace, String name) throws RepositoryNotFoundException { return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name))) - .filter(repository -> { - checkUserPermitted(repository); - return true; - }) .orElseThrow(() -> new RepositoryNotFoundException(name)); } - - /** - * throw exception if the user is not permitted - * - * @param repository - */ - protected void checkUserPermitted(Repository repository) { - RepositoryPermissions.modify(repository).check(); - } - - /** * check if the permission already exists in the repository * diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 547e2dbcb1..39da0ce42e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -18,10 +18,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; -import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.api.rest.AuthorizationExceptionMapper; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Permission; @@ -44,14 +42,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; -@RunWith(MockitoJUnitRunner.Silent.class) @Slf4j public class PermissionRootResourceTest { private static final String REPOSITORY_NAMESPACE = "repo_namespace"; @@ -112,7 +107,7 @@ public class PermissionRootResourceTest { @Before public void prepareEnvironment() { initMocks(this); - permissionRootResource = spy(new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager)); + permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider .of(new RepositoryResource(null, null, null, null, null, null, null,null, MockProvider.of(permissionRootResource))), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); @@ -150,8 +145,7 @@ public class PermissionRootResourceTest { Stream<DynamicTest> missedPermissionUserForbiddenTestFactory() { Repository mockRepository = mock(Repository.class); when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); - doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository); - when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class)); return createDynamicTestsToAssertResponses( requestGETPermission.expectedResponseStatus(403), requestPOSTPermission.expectedResponseStatus(403), @@ -321,7 +315,6 @@ public class PermissionRootResourceTest { when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); - doNothing().when(permissionRootResource).checkUserPermitted(mockRepository); when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); return mockRepository; } @@ -344,9 +337,9 @@ public class PermissionRootResourceTest { .contentType(VndMediaType.PERMISSION); dispatcher.invoke(request, response); log.info("Test the Request :{}", entry); - assertThat(entry.expectedResponseStatus) + assertThat(response.getStatus()) .as("assert status code") - .isEqualTo(response.getStatus()); + .isEqualTo(entry.expectedResponseStatus); if (entry.responseValidator != null) { entry.responseValidator.accept(response); } From 8c128127deef3b505eb621a8912773ec18eaae2f Mon Sep 17 00:00:00 2001 From: Mohamed Karray <mohamed.karray.sr@gmail.com> Date: Wed, 22 Aug 2018 09:18:17 +0200 Subject: [PATCH 55/73] #8771 implement integration tests --- .../java/sonia/scm/it/PermissionsITCase.java | 139 ++++++++++++++++++ .../java/sonia/scm/it/RepositoriesITCase.java | 60 +------- .../sonia/scm/it/RepositoryAccessITCase.java | 11 +- .../java/sonia/scm/it/RepositoryUtil.java | 72 +++++++-- .../src/test/java/sonia/scm/it/RestUtil.java | 18 ++- .../src/test/java/sonia/scm/it/TestData.java | 56 ++++++- .../DefaultAuthorizationCollector.java | 4 +- 7 files changed, 275 insertions(+), 85 deletions(-) create mode 100644 scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java new file mode 100644 index 0000000000..c372efaa2f --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + */ + + +package sonia.scm.it; + +import org.apache.http.HttpStatus; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import sonia.scm.repository.PermissionType; +import sonia.scm.repository.client.api.RepositoryClient; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static org.junit.Assert.*; +import static sonia.scm.it.ScmTypes.availableScmTypes; + +@RunWith(Parameterized.class) +public class PermissionsITCase { + + public static final String USER_READ = "user_read"; + public static final String USER_PASS = "pass"; + private static final String USER_WRITE = "user_write"; + private static final String USER_OWNER = "user_owner"; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final String repositoryType; + private int createdPermissions; + + + public PermissionsITCase(String repositoryType) { + this.repositoryType = repositoryType; + } + + @Parameters(name = "{0}") + public static Collection<String> createParameters() { + return availableScmTypes(); + } + + @Before + public void prepareEnvironment() { + TestData.createDefault(); + TestData.createUser(USER_READ, USER_PASS); + TestData.createUserPermission(USER_READ, PermissionType.READ, repositoryType); + TestData.createUser(USER_WRITE, USER_PASS); + TestData.createUserPermission(USER_WRITE, PermissionType.WRITE, repositoryType); + TestData.createUser(USER_OWNER, USER_PASS); + TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType); + createdPermissions = 3; + } + + @Test + public void everyUserShouldSeePermissions() { + List<Object> userPermissions = TestData.getUserPermissions(USER_READ, USER_PASS, repositoryType); + assertEquals(userPermissions.size(), createdPermissions); + userPermissions = TestData.getUserPermissions(USER_WRITE, USER_PASS, repositoryType); + assertEquals(userPermissions.size(), createdPermissions); + userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType); + assertEquals(userPermissions.size(), createdPermissions); + } + + @Test + public void everyUserShouldCloneRepository() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_READ, USER_PASS); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_WRITE, USER_PASS); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_OWNER, USER_PASS); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + } + + @Test + public void userWithREADPermissionShouldBeNotAuthorizedToCommit() throws IOException { + assertFalse(RepositoryUtil.canUserCommit(USER_READ, USER_PASS, repositoryType, temporaryFolder)); + } + + @Test + public void userWithOwnerPermissionShouldBeAuthorizedToCommit() throws IOException { + assertTrue(RepositoryUtil.canUserCommit(USER_OWNER, USER_PASS, repositoryType, temporaryFolder)); + } + + @Test + public void userWithWritePermissionShouldBeAuthorizedToCommit() throws IOException { + assertTrue(RepositoryUtil.canUserCommit(USER_WRITE, USER_PASS, repositoryType, temporaryFolder)); + } + + @Test + public void userWithWOwnerPermissionShouldBeAuthorizedToDeleteRepository(){ + RepositoryUtil.assertDeleteRepositoryOperation(USER_OWNER, HttpStatus.SC_NO_CONTENT, HttpStatus.SC_NOT_FOUND, USER_PASS, repositoryType); + } + + @Test + public void userWithWReadPermissionShouldNotBeAuthorizedToDeleteRepository(){ + RepositoryUtil.assertDeleteRepositoryOperation(USER_READ, HttpStatus.SC_FORBIDDEN, HttpStatus.SC_OK, USER_PASS, repositoryType); + } + + @Test + public void userWithWWritePermissionShouldNotBeAuthorizedToDeleteRepository(){ + RepositoryUtil.assertDeleteRepositoryOperation(USER_WRITE, HttpStatus.SC_FORBIDDEN, HttpStatus.SC_OK, USER_PASS, repositoryType); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java index cd791cb013..e609b0075e 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java @@ -41,22 +41,18 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; -import sonia.scm.repository.Person; -import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.RepositoryClient; -import sonia.scm.repository.client.api.RepositoryClientFactory; import sonia.scm.web.VndMediaType; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.util.Collection; -import java.util.UUID; +import java.util.Objects; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static sonia.scm.it.RegExMatcher.matchesPattern; import static sonia.scm.it.RestUtil.createResourceUrl; import static sonia.scm.it.RestUtil.given; @@ -66,8 +62,6 @@ import static sonia.scm.it.TestData.repositoryJson; @RunWith(Parameterized.class) public class RepositoriesITCase { - public static final Person AUTHOR = new Person("SCM Administrator", "scmadmin@scm-manager.org"); - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -142,57 +136,13 @@ public class RepositoriesITCase { @Test public void shouldCloneRepository() throws IOException { - RepositoryClient client = createRepositoryClient(); - assertEquals("expected metadata dir", 1, client.getWorkingCopy().list().length); + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType,temporaryFolder.getRoot()); + assertEquals("expected metadata dir", 1, Objects.requireNonNull(client.getWorkingCopy().list()).length); } @Test public void shouldCommitFiles() throws IOException { - RepositoryClient client = createRepositoryClient(); - - for (int i = 0; i < 5; i++) { - createRandomFile(client); - } - - commit(client); - - RepositoryClient checkClient = createRepositoryClient(); - assertEquals("expected 5 files and metadata dir", 6, checkClient.getWorkingCopy().list().length); + assertTrue(RepositoryUtil.canScmAdminCommit(repositoryType,temporaryFolder)); } - private static void createRandomFile(RepositoryClient client) throws IOException { - String uuid = UUID.randomUUID().toString(); - String name = "file-" + uuid + ".uuid"; - - File file = new File(client.getWorkingCopy(), name); - try (FileOutputStream out = new FileOutputStream(file)) { - out.write(uuid.getBytes()); - } - - client.getAddCommand().add(name); - } - - private static void commit(RepositoryClient repositoryClient) throws IOException { - repositoryClient.getCommitCommand().commit(AUTHOR, "commit"); - if ( repositoryClient.isCommandSupported(ClientCommand.PUSH) ) { - repositoryClient.getPushCommand().push(); - } - } - - private RepositoryClient createRepositoryClient() throws IOException { - RepositoryClientFactory clientFactory = new RepositoryClientFactory(); - String cloneUrl = readCloneUrl(); - return clientFactory.create(repositoryType, cloneUrl, "scmadmin", "scmadmin", temporaryFolder.newFolder()); - } - - private String readCloneUrl() { - return given(VndMediaType.REPOSITORY) - - .when() - .get(repositoryUrl) - - .then() - .extract() - .path("_links.httpProtocol.href"); - } } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java index a461e40dea..ff8a3092a7 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -7,7 +7,9 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import sonia.scm.repository.client.api.RepositoryClient; +import java.io.File; import java.io.IOException; import java.util.Collection; @@ -23,7 +25,7 @@ public class RepositoryAccessITCase { public TemporaryFolder tempFolder = new TemporaryFolder(); private final String repositoryType; - private RepositoryUtil repositoryUtil; + private File folder; public RepositoryAccessITCase(String repositoryType) { this.repositoryType = repositoryType; @@ -35,16 +37,17 @@ public class RepositoryAccessITCase { } @Before - public void initClient() throws IOException { + public void initClient() { TestData.createDefault(); - repositoryUtil = new RepositoryUtil(repositoryType, tempFolder.getRoot()); + folder = tempFolder.getRoot(); } @Test public void shouldFindBranches() throws IOException { assumeFalse("There are no branches for SVN", repositoryType.equals("svn")); - repositoryUtil.createAndCommitFile("a.txt", "a"); + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder ); + RepositoryUtil.createAndCommitFile(folder, repositoryClient, "scmadmin", "a.txt", "a"); String branchesUrl = given() .when() diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java index 98d4c8cdab..0c59bdc5ad 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -3,34 +3,61 @@ package sonia.scm.it; import com.google.common.base.Charsets; import com.google.common.io.Files; import org.apache.http.HttpStatus; +import org.junit.rules.TemporaryFolder; import sonia.scm.repository.Changeset; import sonia.scm.repository.Person; import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.repository.client.api.RepositoryClientFactory; import sonia.scm.web.VndMediaType; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.util.UUID; -import static sonia.scm.it.RestUtil.ADMIN_PASSWORD; -import static sonia.scm.it.RestUtil.ADMIN_USERNAME; import static sonia.scm.it.RestUtil.given; public class RepositoryUtil { private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); - private final RepositoryClient repositoryClient; - private final File folder; + static void addRandomFileToRepository(RepositoryClient client) throws IOException { + String uuid = UUID.randomUUID().toString(); + String name = "file-" + uuid + ".uuid"; - RepositoryUtil(String repositoryType, File folder) throws IOException { - this.repositoryClient = createRepositoryClient(repositoryType, folder); - this.folder = folder; + File file = new File(client.getWorkingCopy(), name); + try (FileOutputStream out = new FileOutputStream(file)) { + out.write(uuid.getBytes()); + } + client.getAddCommand().add(name); + } + + static boolean canScmAdminCommit(String repositoryType, TemporaryFolder temporaryFolder) throws IOException { + return canUserCommit("scmadmin", "scmadmin", repositoryType, temporaryFolder); + } + + static boolean canUserCommit(String username, String password, String repositoryType, TemporaryFolder temporaryFolder) throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), username, password); + for (int i = 0; i < 5; i++) { + addRandomFileToRepository(client); + } + try{ + commit(client, username, "commit"); + }catch (RepositoryClientException e){ + return false; + } + RepositoryClient checkClient = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), username, password); + return checkClient.getWorkingCopy().list().length == 6; } static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException { - String httpProtocolUrl = given(VndMediaType.REPOSITORY) + return createRepositoryClient(repositoryType, folder, "scmadmin", "scmadmin"); + } + + static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException { + String httpProtocolUrl = given(VndMediaType.REPOSITORY, username, password) .when() .get(TestData.getDefaultRepositoryUrl(repositoryType)) @@ -40,20 +67,33 @@ public class RepositoryUtil { .extract() .path("_links.httpProtocol.href"); - - return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, ADMIN_USERNAME, ADMIN_PASSWORD, folder); + return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder); } + static void assertDeleteRepositoryOperation(String user, int deleteStatus, int getStatus, String password, String repositoryType) { + given(VndMediaType.REPOSITORY, user, password) - void createAndCommitFile(String fileName, String content) throws IOException { + .when() + .delete(TestData.getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(deleteStatus); + + given(VndMediaType.REPOSITORY, user, password) + + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(getStatus); + } + static void createAndCommitFile(File folder, RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { Files.write(content, new File(folder, fileName), Charsets.UTF_8); repositoryClient.getAddCommand().add(fileName); - commit("added " + fileName); + commit(repositoryClient, username, "added " + fileName); } - Changeset commit(String message) throws IOException { - Changeset changeset = repositoryClient.getCommitCommand().commit( - new Person("scmadmin", "scmadmin@scm-manager.org"), message - ); + static Changeset commit(RepositoryClient repositoryClient, String username, String message) throws IOException { + Changeset changeset = repositoryClient.getCommitCommand().commit(new Person(username, username + "@scm-manager.org"), message); if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { repositoryClient.getPushCommand().push(); } diff --git a/scm-it/src/test/java/sonia/scm/it/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/RestUtil.java index 76d2461637..a7409e1995 100644 --- a/scm-it/src/test/java/sonia/scm/it/RestUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RestUtil.java @@ -19,15 +19,19 @@ public class RestUtil { public static final String ADMIN_USERNAME = "scmadmin"; public static final String ADMIN_PASSWORD = "scmadmin"; - public static RequestSpecification given(String mediaType) { - return RestAssured.given() - .contentType(mediaType) - .accept(mediaType) - .auth().preemptive().basic(ADMIN_USERNAME, ADMIN_PASSWORD); - } - public static RequestSpecification given() { return RestAssured.given() .auth().preemptive().basic(ADMIN_USERNAME, ADMIN_PASSWORD); } + + public static RequestSpecification given(String mediaType) { + return given(mediaType, ADMIN_USERNAME, ADMIN_PASSWORD); + } + + public static RequestSpecification given(String mediaType, String username, String password) { + return RestAssured.given() + .contentType(mediaType) + .accept(mediaType) + .auth().preemptive().basic(username, password); + } } diff --git a/scm-it/src/test/java/sonia/scm/it/TestData.java b/scm-it/src/test/java/sonia/scm/it/TestData.java index b2785b2051..e9177807eb 100644 --- a/scm-it/src/test/java/sonia/scm/it/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/TestData.java @@ -3,6 +3,7 @@ package sonia.scm.it; import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.repository.PermissionType; import sonia.scm.web.VndMediaType; import javax.json.Json; @@ -19,7 +20,9 @@ public class TestData { private static final Logger LOG = LoggerFactory.getLogger(TestData.class); - private static final List<String> PROTECTED_USERS = asList("scmadmin", "anonymous"); + public static final String USER_SCM_ADMIN = "scmadmin"; + public static final String USER_ANONYMOUS = "anonymous"; +private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS); private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>(); @@ -38,6 +41,57 @@ public class TestData { return DEFAULT_REPOSITORIES.get(repositoryType); } + public static void createUser(String username, String password) { + given(VndMediaType.USER) + .when() + .content(" {\n" + + " \"active\": true,\n" + + " \"admin\": false,\n" + + " \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n" + + " \"displayName\": \""+username+"\",\n" + + " \"mail\": \"user1@scm-manager.org\",\n" + + " \"name\": \"" + username + "\",\n" + + " \"password\": \"" + password + "\",\n" + + " \"type\": \"xml\"\n" + + " \n" + + " }") + .post(createResourceUrl("users")) + .then() + .statusCode(HttpStatus.SC_CREATED) + ; + } + + + public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) { + given(VndMediaType.PERMISSION) + .when() + .content("{\n" + + "\t\"type\": \""+permissionType.name()+"\",\n" + + "\t\"name\": \""+name+"\",\n" + + "\t\"groupPermission\": false\n" + + "\t\n" + + "}") + .post(TestData.getDefaultPermissionUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_CREATED) + ; + } + + public static List<Object> getUserPermissions(String username, String password, String repositoryType) { + return given(VndMediaType.PERMISSION, username, password) + .when() + .get(TestData.getDefaultPermissionUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getList(""); + } + + private static String getDefaultPermissionUrl(String repositoryType) { + return getDefaultRepositoryUrl(repositoryType)+"/permissions/"; + } + + private static void cleanupRepositories() { List<String> repositories = given(VndMediaType.REPOSITORY_COLLECTION) .when() diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index d868b81499..e6a2502997 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -207,9 +207,9 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector boolean hasPermission = false; for (sonia.scm.repository.Permission permission : repositoryPermissions) { - if (isUserPermitted(user, groups, permission)) + hasPermission = isUserPermitted(user, groups, permission); + if (hasPermission) { - String perm = permission.getType().getPermissionPrefix().concat(repository.getId()); if (logger.isTraceEnabled()) { From b68c8400a7b15178ac8088481b35347f86bfbd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 22 Aug 2018 10:59:46 +0200 Subject: [PATCH 56/73] Move tests to test classes --- .../java/sonia/scm/it/PermissionsITCase.java | 50 ++++++++++++---- .../java/sonia/scm/it/RepositoriesITCase.java | 9 ++- .../sonia/scm/it/RepositoryAccessITCase.java | 4 +- .../java/sonia/scm/it/RepositoryUtil.java | 57 +++---------------- 4 files changed, 54 insertions(+), 66 deletions(-) diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java index c372efaa2f..07e2a36534 100644 --- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -41,13 +41,17 @@ import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import sonia.scm.repository.PermissionType; import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.repository.client.api.RepositoryClientException; +import sonia.scm.web.VndMediaType; import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Objects; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static sonia.scm.it.RepositoryUtil.addAndCommitRandomFile; +import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.ScmTypes.availableScmTypes; @RunWith(Parameterized.class) @@ -106,34 +110,56 @@ public class PermissionsITCase { assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); } - @Test - public void userWithREADPermissionShouldBeNotAuthorizedToCommit() throws IOException { - assertFalse(RepositoryUtil.canUserCommit(USER_READ, USER_PASS, repositoryType, temporaryFolder)); + @Test(expected = RepositoryClientException.class) + public void userWithReadPermissionShouldBeNotAuthorizedToCommit() throws IOException { + createAndCommit(USER_READ); } @Test public void userWithOwnerPermissionShouldBeAuthorizedToCommit() throws IOException { - assertTrue(RepositoryUtil.canUserCommit(USER_OWNER, USER_PASS, repositoryType, temporaryFolder)); + createAndCommit(USER_OWNER); } @Test public void userWithWritePermissionShouldBeAuthorizedToCommit() throws IOException { - assertTrue(RepositoryUtil.canUserCommit(USER_WRITE, USER_PASS, repositoryType, temporaryFolder)); + createAndCommit(USER_WRITE); + } + + private void createAndCommit(String username) throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), username, PermissionsITCase.USER_PASS); + addAndCommitRandomFile(client, username); } @Test - public void userWithWOwnerPermissionShouldBeAuthorizedToDeleteRepository(){ - RepositoryUtil.assertDeleteRepositoryOperation(USER_OWNER, HttpStatus.SC_NO_CONTENT, HttpStatus.SC_NOT_FOUND, USER_PASS, repositoryType); + public void userWithOwnerPermissionShouldBeAuthorizedToDeleteRepository(){ + assertDeleteRepositoryOperation(HttpStatus.SC_NO_CONTENT, HttpStatus.SC_NOT_FOUND, USER_OWNER, repositoryType); } @Test - public void userWithWReadPermissionShouldNotBeAuthorizedToDeleteRepository(){ - RepositoryUtil.assertDeleteRepositoryOperation(USER_READ, HttpStatus.SC_FORBIDDEN, HttpStatus.SC_OK, USER_PASS, repositoryType); + public void userWithReadPermissionShouldNotBeAuthorizedToDeleteRepository(){ + assertDeleteRepositoryOperation(HttpStatus.SC_FORBIDDEN, HttpStatus.SC_OK, USER_READ, repositoryType); } @Test - public void userWithWWritePermissionShouldNotBeAuthorizedToDeleteRepository(){ - RepositoryUtil.assertDeleteRepositoryOperation(USER_WRITE, HttpStatus.SC_FORBIDDEN, HttpStatus.SC_OK, USER_PASS, repositoryType); + public void userWithWritePermissionShouldNotBeAuthorizedToDeleteRepository(){ + assertDeleteRepositoryOperation(HttpStatus.SC_FORBIDDEN, HttpStatus.SC_OK, USER_WRITE, repositoryType); } + private void assertDeleteRepositoryOperation(int expectedDeleteStatus, int expectedGetStatus, String user, String repositoryType) { + given(VndMediaType.REPOSITORY, user, PermissionsITCase.USER_PASS) + + .when() + .delete(TestData.getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedDeleteStatus); + + given(VndMediaType.REPOSITORY, user, PermissionsITCase.USER_PASS) + + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedGetStatus); + } } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java index e609b0075e..21d4d97b1b 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java @@ -34,6 +34,7 @@ package sonia.scm.it; //~--- non-JDK imports -------------------------------------------------------- import org.apache.http.HttpStatus; +import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -52,7 +53,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static sonia.scm.it.RegExMatcher.matchesPattern; import static sonia.scm.it.RestUtil.createResourceUrl; import static sonia.scm.it.RestUtil.given; @@ -136,13 +136,16 @@ public class RepositoriesITCase { @Test public void shouldCloneRepository() throws IOException { - RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType,temporaryFolder.getRoot()); + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.getRoot()); assertEquals("expected metadata dir", 1, Objects.requireNonNull(client.getWorkingCopy().list()).length); } @Test public void shouldCommitFiles() throws IOException { - assertTrue(RepositoryUtil.canScmAdminCommit(repositoryType,temporaryFolder)); + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), "scmadmin", "scmadmin"); + String name = RepositoryUtil.addAndCommitRandomFile(client, "scmadmin"); + RepositoryClient checkClient = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), "scmadmin", "scmadmin"); + Assertions.assertThat(checkClient.getWorkingCopy().list()).contains(name); } } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java index ff8a3092a7..7b5f1ae2e8 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -46,8 +46,8 @@ public class RepositoryAccessITCase { public void shouldFindBranches() throws IOException { assumeFalse("There are no branches for SVN", repositoryType.equals("svn")); - RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder ); - RepositoryUtil.createAndCommitFile(folder, repositoryClient, "scmadmin", "a.txt", "a"); + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a"); String branchesUrl = given() .when() diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java index 0c59bdc5ad..6706fba21d 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -3,17 +3,14 @@ package sonia.scm.it; import com.google.common.base.Charsets; import com.google.common.io.Files; import org.apache.http.HttpStatus; -import org.junit.rules.TemporaryFolder; import sonia.scm.repository.Changeset; import sonia.scm.repository.Person; import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.RepositoryClient; -import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.repository.client.api.RepositoryClientFactory; import sonia.scm.web.VndMediaType; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.util.UUID; @@ -23,35 +20,6 @@ public class RepositoryUtil { private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); - static void addRandomFileToRepository(RepositoryClient client) throws IOException { - String uuid = UUID.randomUUID().toString(); - String name = "file-" + uuid + ".uuid"; - - File file = new File(client.getWorkingCopy(), name); - try (FileOutputStream out = new FileOutputStream(file)) { - out.write(uuid.getBytes()); - } - client.getAddCommand().add(name); - } - - static boolean canScmAdminCommit(String repositoryType, TemporaryFolder temporaryFolder) throws IOException { - return canUserCommit("scmadmin", "scmadmin", repositoryType, temporaryFolder); - } - - static boolean canUserCommit(String username, String password, String repositoryType, TemporaryFolder temporaryFolder) throws IOException { - RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), username, password); - for (int i = 0; i < 5; i++) { - addRandomFileToRepository(client); - } - try{ - commit(client, username, "commit"); - }catch (RepositoryClientException e){ - return false; - } - RepositoryClient checkClient = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), username, password); - return checkClient.getWorkingCopy().list().length == 6; - } - static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException { return createRepositoryClient(repositoryType, folder, "scmadmin", "scmadmin"); } @@ -69,25 +37,16 @@ public class RepositoryUtil { return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder); } - static void assertDeleteRepositoryOperation(String user, int deleteStatus, int getStatus, String password, String repositoryType) { - given(VndMediaType.REPOSITORY, user, password) - .when() - .delete(TestData.getDefaultRepositoryUrl(repositoryType)) - - .then() - .statusCode(deleteStatus); - - given(VndMediaType.REPOSITORY, user, password) - - .when() - .get(TestData.getDefaultRepositoryUrl(repositoryType)) - - .then() - .statusCode(getStatus); + static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException { + String uuid = UUID.randomUUID().toString(); + String name = "file-" + uuid + ".uuid"; + createAndCommitFile(client, username, name, uuid); + return name; } - static void createAndCommitFile(File folder, RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { - Files.write(content, new File(folder, fileName), Charsets.UTF_8); + + static void createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { + Files.write(content, new File(repositoryClient.getWorkingCopy(), fileName), Charsets.UTF_8); repositoryClient.getAddCommand().add(fileName); commit(repositoryClient, username, "added " + fileName); } From 585d37feedb3f380dec3469b77ee36b1dd283a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 22 Aug 2018 11:19:19 +0200 Subject: [PATCH 57/73] Split up test methods for each user --- .../java/sonia/scm/it/PermissionsITCase.java | 45 +++++++++++++++---- .../java/sonia/scm/it/RepositoryUtil.java | 11 +---- .../src/test/java/sonia/scm/it/TestData.java | 45 ++++++++++++------- 3 files changed, 68 insertions(+), 33 deletions(-) diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java index 07e2a36534..6335907e76 100644 --- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -53,6 +53,7 @@ import static org.junit.Assert.assertEquals; import static sonia.scm.it.RepositoryUtil.addAndCommitRandomFile; import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.ScmTypes.availableScmTypes; +import static sonia.scm.it.TestData.callUserPermissions; @RunWith(Parameterized.class) public class PermissionsITCase { @@ -61,6 +62,7 @@ public class PermissionsITCase { public static final String USER_PASS = "pass"; private static final String USER_WRITE = "user_write"; private static final String USER_OWNER = "user_owner"; + private static final String USER_OTHER = "user_other"; @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -87,29 +89,56 @@ public class PermissionsITCase { TestData.createUserPermission(USER_WRITE, PermissionType.WRITE, repositoryType); TestData.createUser(USER_OWNER, USER_PASS); TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType); + TestData.createUser(USER_OTHER, USER_PASS); createdPermissions = 3; } @Test - public void everyUserShouldSeePermissions() { + public void readUserShouldSeePermissions() { List<Object> userPermissions = TestData.getUserPermissions(USER_READ, USER_PASS, repositoryType); assertEquals(userPermissions.size(), createdPermissions); - userPermissions = TestData.getUserPermissions(USER_WRITE, USER_PASS, repositoryType); - assertEquals(userPermissions.size(), createdPermissions); - userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType); - assertEquals(userPermissions.size(), createdPermissions); } @Test - public void everyUserShouldCloneRepository() throws IOException { + public void writeUserShouldSeePermissions() { + List<Object> userPermissions = TestData.getUserPermissions(USER_WRITE, USER_PASS, repositoryType); + assertEquals(userPermissions.size(), createdPermissions); + } + + @Test + public void ownerShouldSeePermissions() { + List<Object> userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType); + assertEquals(userPermissions.size(), createdPermissions); + } + + @Test + public void otherUserShouldNotSeePermissions() { + callUserPermissions(USER_OTHER, USER_PASS, repositoryType, HttpStatus.SC_FORBIDDEN); + } + + @Test + public void readUserShouldCloneRepository() throws IOException { RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_READ, USER_PASS); assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); - client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_WRITE, USER_PASS); + } + + @Test + public void writeUserShouldCloneRepository() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_WRITE, USER_PASS); assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); - client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_OWNER, USER_PASS); + } + + @Test + public void ownerShouldCloneRepository() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_OWNER, USER_PASS); assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); } + @Test + public void otherUserShouldNotCloneRepository() { + TestData.callRepository(USER_OTHER, USER_PASS, repositoryType, HttpStatus.SC_FORBIDDEN); + } + @Test(expected = RepositoryClientException.class) public void userWithReadPermissionShouldBeNotAuthorizedToCommit() throws IOException { createAndCommit(USER_READ); diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java index 6706fba21d..e755f3c3c1 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -8,14 +8,11 @@ import sonia.scm.repository.Person; import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClientFactory; -import sonia.scm.web.VndMediaType; import java.io.File; import java.io.IOException; import java.util.UUID; -import static sonia.scm.it.RestUtil.given; - public class RepositoryUtil { private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); @@ -25,13 +22,7 @@ public class RepositoryUtil { } static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException { - String httpProtocolUrl = given(VndMediaType.REPOSITORY, username, password) - - .when() - .get(TestData.getDefaultRepositoryUrl(repositoryType)) - - .then() - .statusCode(HttpStatus.SC_OK) + String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK) .extract() .path("_links.httpProtocol.href"); diff --git a/scm-it/src/test/java/sonia/scm/it/TestData.java b/scm-it/src/test/java/sonia/scm/it/TestData.java index e9177807eb..b758c81c0e 100644 --- a/scm-it/src/test/java/sonia/scm/it/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/TestData.java @@ -1,5 +1,6 @@ package sonia.scm.it; +import io.restassured.response.ValidatableResponse; import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,7 +23,7 @@ public class TestData { public static final String USER_SCM_ADMIN = "scmadmin"; public static final String USER_ANONYMOUS = "anonymous"; -private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS); + private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS); private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>(); @@ -48,7 +49,7 @@ private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ " \"active\": true,\n" + " \"admin\": false,\n" + " \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n" + - " \"displayName\": \""+username+"\",\n" + + " \"displayName\": \"" + username + "\",\n" + " \"mail\": \"user1@scm-manager.org\",\n" + " \"name\": \"" + username + "\",\n" + " \"password\": \"" + password + "\",\n" + @@ -66,8 +67,8 @@ private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ given(VndMediaType.PERMISSION) .when() .content("{\n" + - "\t\"type\": \""+permissionType.name()+"\",\n" + - "\t\"name\": \""+name+"\",\n" + + "\t\"type\": \"" + permissionType.name() + "\",\n" + + "\t\"name\": \"" + name + "\",\n" + "\t\"groupPermission\": false\n" + "\t\n" + "}") @@ -77,18 +78,32 @@ private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ ; } - public static List<Object> getUserPermissions(String username, String password, String repositoryType) { - return given(VndMediaType.PERMISSION, username, password) - .when() - .get(TestData.getDefaultPermissionUrl(repositoryType)) - .then() - .statusCode(HttpStatus.SC_OK) - .extract() - .body().jsonPath().getList(""); - } + public static List<Object> getUserPermissions(String username, String password, String repositoryType) { + return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK) + .extract() + .body().jsonPath().getList(""); + } - private static String getDefaultPermissionUrl(String repositoryType) { - return getDefaultRepositoryUrl(repositoryType)+"/permissions/"; + public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) { + return given(VndMediaType.PERMISSION, username, password) + .when() + .get(TestData.getDefaultPermissionUrl(repositoryType)) + .then() + .statusCode(expectedStatusCode); + } + + public static ValidatableResponse callRepository(String username, String password, String repositoryType, int expectedStatusCode) { + return given(VndMediaType.REPOSITORY, username, password) + + .when() + .get(getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedStatusCode); + } + + public static String getDefaultPermissionUrl(String repositoryType) { + return getDefaultRepositoryUrl(repositoryType) + "/permissions/"; } From 02f4801b58e6db0569c5db5fb9212d278276c589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 22 Aug 2018 12:20:09 +0200 Subject: [PATCH 58/73] Let integration tests use links from HAL and test brute force links --- .../java/sonia/scm/it/PermissionsITCase.java | 49 +++++++++++++++---- .../src/test/java/sonia/scm/it/TestData.java | 14 ++++-- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java index 6335907e76..f015d9e73d 100644 --- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -50,10 +50,12 @@ import java.util.List; import java.util.Objects; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static sonia.scm.it.RepositoryUtil.addAndCommitRandomFile; import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.ScmTypes.availableScmTypes; -import static sonia.scm.it.TestData.callUserPermissions; +import static sonia.scm.it.TestData.USER_SCM_ADMIN; +import static sonia.scm.it.TestData.callRepository; @RunWith(Parameterized.class) public class PermissionsITCase { @@ -94,15 +96,35 @@ public class PermissionsITCase { } @Test - public void readUserShouldSeePermissions() { - List<Object> userPermissions = TestData.getUserPermissions(USER_READ, USER_PASS, repositoryType); - assertEquals(userPermissions.size(), createdPermissions); + public void readUserShouldNotSeePermissions() { + assertNull(callRepository(USER_WRITE, USER_PASS, repositoryType, HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href")); } @Test - public void writeUserShouldSeePermissions() { - List<Object> userPermissions = TestData.getUserPermissions(USER_WRITE, USER_PASS, repositoryType); - assertEquals(userPermissions.size(), createdPermissions); + public void readUserShouldNotSeeBruteForcePermissions() { + given(VndMediaType.PERMISSION, USER_READ, USER_PASS) + .when() + .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + public void writeUserShouldNotSeePermissions() { + assertNull(callRepository(USER_WRITE, USER_PASS, repositoryType, HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href")); + } + + @Test + public void writeUserShouldNotSeeBruteForcePermissions() { + given(VndMediaType.PERMISSION, USER_WRITE, USER_PASS) + .when() + .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); } @Test @@ -112,8 +134,17 @@ public class PermissionsITCase { } @Test - public void otherUserShouldNotSeePermissions() { - callUserPermissions(USER_OTHER, USER_PASS, repositoryType, HttpStatus.SC_FORBIDDEN); + public void otherUserShouldNotSeeRepository() { + callRepository(USER_OTHER, USER_PASS, repositoryType, HttpStatus.SC_FORBIDDEN); + } + + @Test + public void otherUserShouldNotSeeBruteForcePermissions() { + given(VndMediaType.PERMISSION, USER_OTHER, USER_PASS) + .when() + .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); } @Test diff --git a/scm-it/src/test/java/sonia/scm/it/TestData.java b/scm-it/src/test/java/sonia/scm/it/TestData.java index b758c81c0e..999d5117c8 100644 --- a/scm-it/src/test/java/sonia/scm/it/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/TestData.java @@ -72,7 +72,7 @@ public class TestData { "\t\"groupPermission\": false\n" + "\t\n" + "}") - .post(TestData.getDefaultPermissionUrl(repositoryType)) + .post(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) .then() .statusCode(HttpStatus.SC_CREATED) ; @@ -87,7 +87,7 @@ public class TestData { public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) { return given(VndMediaType.PERMISSION, username, password) .when() - .get(TestData.getDefaultPermissionUrl(repositoryType)) + .get(TestData.getDefaultPermissionUrl(username, password, repositoryType)) .then() .statusCode(expectedStatusCode); } @@ -102,8 +102,14 @@ public class TestData { .statusCode(expectedStatusCode); } - public static String getDefaultPermissionUrl(String repositoryType) { - return getDefaultRepositoryUrl(repositoryType) + "/permissions/"; + public static String getDefaultPermissionUrl(String username, String password, String repositoryType) { + return given(VndMediaType.REPOSITORY, username, password) + .when() + .get(getDefaultRepositoryUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href"); } From e77c633d64c1bd52d3427f6e4fb449b60a3a34c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Wed, 22 Aug 2018 12:29:45 +0200 Subject: [PATCH 59/73] Fix permission check for read permissions --- .../sonia/scm/api/v2/resources/PermissionRootResource.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index d08f2e7057..d8e1c56e29 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -14,6 +14,7 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryException; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -95,6 +96,7 @@ public class PermissionRootResource { @Path("{permission-name}") public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { Repository repository = load(namespace, name); + RepositoryPermissions.modify(repository).check(); return Response.ok( repository.getPermissions() .stream() @@ -125,6 +127,7 @@ public class PermissionRootResource { @Path("") public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { Repository repository = load(namespace, name); + RepositoryPermissions.modify(repository).check(); List<PermissionDto> permissionDtoList = repository.getPermissions() .stream() .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(), repository.getName()))) From 87e32087ac6d1f712812b22ad07afc7c96514dc0 Mon Sep 17 00:00:00 2001 From: Mohamed Karray <mohamed.karray.sr@gmail.com> Date: Wed, 22 Aug 2018 15:55:29 +0200 Subject: [PATCH 60/73] #8771 run JUnit 5 with maven and fix tests --- pom.xml | 78 ++++++++++++++----- scm-test/pom.xml | 6 -- scm-webapp/pom.xml | 18 ----- .../v2/resources/PermissionRootResource.java | 27 ++----- .../resources/PermissionRootResourceTest.java | 24 +++--- 5 files changed, 82 insertions(+), 71 deletions(-) diff --git a/pom.xml b/pom.xml index 3295e732db..1c08c334f5 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> @@ -83,7 +84,7 @@ <name>scm-manager release repository</name> <url>http://maven.scm-manager.org/nexus/content/groups/public</url> </repository> - + <repository> <id>ossrh</id> <url>https://oss.sonatype.org/content/repositories/snapshots</url> @@ -118,10 +119,26 @@ <dependencies> + <!-- JUnit 5 --> + <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <scope>test</scope> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + </dependency> + + <dependency> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> </dependency> <dependency> @@ -139,15 +156,11 @@ <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> - <version>${mockito.version}</version> - <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> - <version>3.10.0</version> - <scope>test</scope> </dependency> <dependency> @@ -161,7 +174,7 @@ <!-- Don't inherit this dependency! --> <scope>provided</scope> </dependency> - + </dependencies> <dependencyManagement> @@ -282,9 +295,32 @@ <version>${jackson.version}</version> </dependency> + <!-- JUnit 5 --> + <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> @@ -348,7 +384,11 @@ </pluginManagement> <plugins> - + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>2.22.0</version> + </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> @@ -536,9 +576,9 @@ <artifactId>maven-eclipse-plugin</artifactId> <version>2.6</version> </plugin> - + <!-- code coverage --> - + <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> @@ -558,7 +598,7 @@ </execution> </executions> </plugin> - + <!-- reporting --> <plugin> @@ -695,13 +735,13 @@ <!-- test libraries --> <mockito.version>2.10.0</mockito.version> <hamcrest.version>1.3</hamcrest.version> - <junit.version>4.12</junit.version> + <junit.version>5.2.0</junit.version> <!-- logging libraries --> <slf4j.version>1.7.22</slf4j.version> <logback.version>1.1.10</logback.version> <servlet.version>3.0.1</servlet.version> - + <jaxrs.version>2.0.1</jaxrs.version> <resteasy.version>3.1.3.Final</resteasy.version> <jersey-client.version>1.19.4</jersey-client.version> @@ -711,7 +751,7 @@ <!-- event bus --> <legman.version>1.3.0</legman.version> - + <!-- webserver --> <jetty.version>9.2.10.v20150310</jetty.version> <jetty.maven.version>9.2.10.v20150310</jetty.maven.version> diff --git a/scm-test/pom.xml b/scm-test/pom.xml index 0da030652b..98441ec898 100644 --- a/scm-test/pom.xml +++ b/scm-test/pom.xml @@ -28,12 +28,6 @@ <version>2.0.0-SNAPSHOT</version> </dependency> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <scope>compile</scope> - </dependency> - <dependency> <groupId>com.github.sdorra</groupId> <artifactId>shiro-unit</artifactId> diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 0b0ad145c6..b34da3454e 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -288,24 +288,6 @@ <scope>test</scope> </dependency> - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter-api</artifactId> - <version>5.2.0</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter-params</artifactId> - <version>5.2.0</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter-engine</artifactId> - <version>5.2.0</version> - <scope>test</scope> - </dependency> <!-- core plugins --> <dependency> diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index d8e1c56e29..61c7cc8f1e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -6,26 +6,11 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; -import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.Permission; -import sonia.scm.repository.PermissionAlreadyExistsException; -import sonia.scm.repository.PermissionNotFoundException; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryException; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryNotFoundException; -import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.repository.*; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; +import javax.ws.rs.*; import javax.ws.rs.core.Response; import java.net.URI; import java.util.List; @@ -96,7 +81,7 @@ public class PermissionRootResource { @Path("{permission-name}") public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { Repository repository = load(namespace, name); - RepositoryPermissions.modify(repository).check(); + checkUserPermission(repository); return Response.ok( repository.getPermissions() .stream() @@ -127,7 +112,7 @@ public class PermissionRootResource { @Path("") public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { Repository repository = load(namespace, name); - RepositoryPermissions.modify(repository).check(); + checkUserPermission(repository); List<PermissionDto> permissionDtoList = repository.getPermissions() .stream() .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(), repository.getName()))) @@ -135,6 +120,10 @@ public class PermissionRootResource { return Response.ok(permissionDtoList).build(); } + protected void checkUserPermission(Repository repository) { + RepositoryPermissions.modify(repository).check(); + } + /** * Update a permission to the user or group managed by the repository diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 39da0ce42e..16f8357850 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -2,6 +2,8 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableList; import lombok.ToString; import lombok.extern.slf4j.Slf4j; @@ -13,6 +15,7 @@ import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.spi.HttpRequest; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -21,11 +24,7 @@ import org.junit.jupiter.api.TestFactory; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.api.rest.AuthorizationExceptionMapper; -import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.Permission; -import sonia.scm.repository.PermissionType; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.*; import sonia.scm.web.VndMediaType; import java.io.IOException; @@ -42,12 +41,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; @Slf4j +@SubjectAware( + username = "trillian", + password = "secret", + configuration = "classpath:sonia/scm/repository/shiro.ini" +) public class PermissionRootResourceTest { private static final String REPOSITORY_NAMESPACE = "repo_namespace"; private static final String REPOSITORY_NAME = "repo"; @@ -89,6 +91,9 @@ public class PermissionRootResourceTest { private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + @Rule + public ShiroRule shiro = new ShiroRule(); + @Mock private RepositoryManager repositoryManager; @@ -107,7 +112,7 @@ public class PermissionRootResourceTest { @Before public void prepareEnvironment() { initMocks(this); - permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager); + permissionRootResource = spy(new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager)); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider .of(new RepositoryResource(null, null, null, null, null, null, null,null, MockProvider.of(permissionRootResource))), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); @@ -316,6 +321,7 @@ public class PermissionRootResourceTest { when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + doNothing().when(permissionRootResource).checkUserPermission(any()); return mockRepository; } From bc009c02ac14f01bfb7da52eb1dfc62850315ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Thu, 23 Aug 2018 17:24:00 +0200 Subject: [PATCH 61/73] Fix parameterized tests for junit 5 --- .../test/java/sonia/scm/it/IntegrationTestUtil.java | 10 +++++----- .../java/sonia/scm/it/RepositoryArchiveITCase.java | 2 +- .../test/java/sonia/scm/it/RepositoryHookITCase.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java b/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java index 741702ad81..3f17aff7a7 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java +++ b/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java @@ -120,15 +120,15 @@ public final class IntegrationTestUtil } } - public static Collection<String> createRepositoryTypeParameters() { - Collection<String> params = new ArrayList<>(); + public static Collection<String[]> createRepositoryTypeParameters() { + Collection<String[]> params = new ArrayList<>(); - params.add("git"); - params.add("svn" ); + params.add(new String[] {"git"}); + params.add(new String[] {"svn"}); if (IOUtil.search("hg") != null) { - params.add("hg"); + params.add(new String[] {"hg"}); } return params; diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java index 4cf7cd6571..bc79b9fa20 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java @@ -81,7 +81,7 @@ public class RepositoryArchiveITCase //~--- methods -------------------------------------------------------------- @Parameterized.Parameters(name = "{0}") - public static Collection<String> createParameters() { + public static Collection<String[]> createParameters() { return IntegrationTestUtil.createRepositoryTypeParameters(); } diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java index 2be3bfd81b..dae80cd00d 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java @@ -208,7 +208,7 @@ public class RepositoryHookITCase extends AbstractAdminITCaseBase * @return repository types test parameter */ @Parameters(name = "{0}") - public static Collection<String> createParameters() + public static Collection<String[]> createParameters() { return IntegrationTestUtil.createRepositoryTypeParameters(); } From b3fd051321d29417b5592fe18072bbeb30e10095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Fri, 24 Aug 2018 07:57:13 +0200 Subject: [PATCH 62/73] Check against feature instead of repository type --- .../test/java/sonia/scm/it/RepositoryAccessITCase.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java index 7b5f1ae2e8..861c9e7aee 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -1,12 +1,14 @@ package sonia.scm.it; import org.apache.http.HttpStatus; +import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.RepositoryClient; import java.io.File; @@ -14,7 +16,6 @@ import java.io.IOException; import java.util.Collection; import static org.junit.Assert.assertNotNull; -import static org.junit.Assume.assumeFalse; import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.ScmTypes.availableScmTypes; @@ -44,9 +45,10 @@ public class RepositoryAccessITCase { @Test public void shouldFindBranches() throws IOException { - assumeFalse("There are no branches for SVN", repositoryType.equals("svn")); - RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + + Assume.assumeTrue("There are no branches for " + repositoryType, repositoryClient.isCommandSupported(ClientCommand.BRANCH)); + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a"); String branchesUrl = given() From abe3dec8dfbe96b16121ee08c448d67a55a28372 Mon Sep 17 00:00:00 2001 From: Mohamed Karray <mohamed.karray.sr@gmail.com> Date: Fri, 24 Aug 2018 10:57:50 +0200 Subject: [PATCH 63/73] #8771 fix get all to return links add permissionRead and permissionWrite permissions --- .../sonia/scm/repository/PermissionType.java | 4 +- .../java/sonia/scm/repository/Repository.java | 9 +- .../scm/web/filter/PermissionFilter.java | 17 +-- .../src/test/java/sonia/scm/it/TestData.java | 2 +- .../PermissionCollectionToDtoMapper.java | 51 +++++++ .../v2/resources/PermissionRootResource.java | 36 +++-- .../PermissionToPermissionDtoMapper.java | 25 ++-- .../scm/api/v2/resources/ResourceLinks.java | 4 + .../resources/PermissionRootResourceTest.java | 126 ++++++++++++------ .../DefaultAuthorizationCollectorTest.java | 15 ++- 10 files changed, 196 insertions(+), 93 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionType.java b/scm-core/src/main/java/sonia/scm/repository/PermissionType.java index bd4d773877..bba0d44f3d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/PermissionType.java +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionType.java @@ -42,10 +42,10 @@ public enum PermissionType { /** read permision */ - READ(0, "repository:read:"), + READ(0, "repository:read,pull:"), /** read and write permissionPrefix */ - WRITE(10, "repository:read,write:"), + WRITE(10, "repository:read,pull,push:"), /** * read, write and diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 44841893e7..aabd022ecb 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -43,12 +43,7 @@ import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.*; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -60,7 +55,7 @@ import java.util.List; */ @StaticPermissions( value = "repository", - permissions = {"read", "write", "modify", "delete", "healthCheck"} + permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"} ) @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java index 7517f4848c..dd3c82e800 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java @@ -36,13 +36,11 @@ package sonia.scm.web.filter; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Splitter; - import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.subject.Subject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.ArgumentIsInvalidException; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; @@ -53,17 +51,14 @@ import sonia.scm.security.ScmSecurityException; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Iterator; - import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.shiro.authz.AuthorizationException; +import java.io.IOException; +import java.util.Iterator; + +//~--- JDK imports ------------------------------------------------------------ /** * Abstract http filter to check repository permissions. @@ -339,7 +334,7 @@ public abstract class PermissionFilter extends HttpFilter if (writeRequest) { - permitted = RepositoryPermissions.write(repository).isPermitted(); + permitted = RepositoryPermissions.push(repository).isPermitted(); } else { diff --git a/scm-it/src/test/java/sonia/scm/it/TestData.java b/scm-it/src/test/java/sonia/scm/it/TestData.java index 999d5117c8..ada67d588c 100644 --- a/scm-it/src/test/java/sonia/scm/it/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/TestData.java @@ -81,7 +81,7 @@ public class TestData { public static List<Object> getUserPermissions(String username, String password, String repositoryType) { return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK) .extract() - .body().jsonPath().getList(""); + .body().jsonPath().getList("_embedded.permissions"); } public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java new file mode 100644 index 0000000000..4789915f3d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java @@ -0,0 +1,51 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.Inject; +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; + +import java.util.List; + +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; +import static java.util.stream.Collectors.toList; + +public class PermissionCollectionToDtoMapper { + + private final ResourceLinks resourceLinks; + private final PermissionToPermissionDtoMapper permissionToPermissionDtoMapper; + + @Inject + public PermissionCollectionToDtoMapper(PermissionToPermissionDtoMapper permissionToPermissionDtoMapper, ResourceLinks resourceLinks) { + this.resourceLinks = resourceLinks; + this.permissionToPermissionDtoMapper = permissionToPermissionDtoMapper; + } + + public HalRepresentation map(Repository repository) { + List<PermissionDto> permissionDtoList = repository.getPermissions() + .stream() + .map(permission -> permissionToPermissionDtoMapper.map(permission, repository)) + .collect(toList()); + return new HalRepresentation(createLinks(repository), embedDtos(permissionDtoList)); + } + + private Links createLinks(Repository repository) { + RepositoryPermissions.permissionRead(repository).check(); + Links.Builder linksBuilder = linkingTo() + .with(Links.linkingTo().self(resourceLinks.permission().all(repository.getNamespace(), repository.getName())).build()); + if (RepositoryPermissions.permissionWrite(repository).isPermitted()) { + linksBuilder.single(link("create", resourceLinks.permission().create(repository.getNamespace(), repository.getName()))); + } + return linksBuilder.build(); + } + + private Embedded embedDtos(List<PermissionDto> permissionDtoList) { + return embeddedBuilder() + .with("permissions", permissionDtoList) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index 61c7cc8f1e..801c1b104c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -13,23 +13,23 @@ import javax.inject.Inject; import javax.ws.rs.*; import javax.ws.rs.core.Response; import java.net.URI; -import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @Slf4j public class PermissionRootResource { private PermissionDtoToPermissionMapper dtoToModelMapper; private PermissionToPermissionDtoMapper modelToDtoMapper; + private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; private ResourceLinks resourceLinks; private final RepositoryManager manager; @Inject - public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) { + public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, PermissionCollectionToDtoMapper permissionCollectionToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) { this.dtoToModelMapper = dtoToModelMapper; this.modelToDtoMapper = modelToDtoMapper; + this.permissionCollectionToDtoMapper = permissionCollectionToDtoMapper; this.resourceLinks = resourceLinks; this.manager = manager; } @@ -52,9 +52,11 @@ public class PermissionRootResource { }) @TypeHint(TypeHint.NO_CONTENT.class) @Consumes(VndMediaType.PERMISSION) + @Path("") public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws RepositoryException { log.info("try to add new permission: {}", permission); Repository repository = load(namespace, name); + RepositoryPermissions.permissionWrite(repository).check(); checkPermissionAlreadyExists(permission, repository); repository.getPermissions().add(dtoToModelMapper.map(permission)); manager.modify(repository); @@ -81,12 +83,12 @@ public class PermissionRootResource { @Path("{permission-name}") public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { Repository repository = load(namespace, name); - checkUserPermission(repository); + RepositoryPermissions.permissionRead(repository).check(); return Response.ok( repository.getPermissions() .stream() .filter(permission -> permissionName.equals(permission.getName())) - .map(permission -> modelToDtoMapper.map(permission, new NamespaceAndName(repository.getNamespace(), repository.getName()))) + .map(permission -> modelToDtoMapper.map(permission, repository)) .findFirst() .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) ).build(); @@ -112,16 +114,8 @@ public class PermissionRootResource { @Path("") public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { Repository repository = load(namespace, name); - checkUserPermission(repository); - List<PermissionDto> permissionDtoList = repository.getPermissions() - .stream() - .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(), repository.getName()))) - .collect(Collectors.toList()); - return Response.ok(permissionDtoList).build(); - } - - protected void checkUserPermission(Repository repository) { - RepositoryPermissions.modify(repository).check(); + RepositoryPermissions.permissionRead(repository).check(); + return Response.ok(permissionCollectionToDtoMapper.map(repository)).build(); } @@ -142,11 +136,12 @@ public class PermissionRootResource { @Consumes(VndMediaType.PERMISSION) @Path("{permission-name}") public Response update(@PathParam("namespace") String namespace, - @PathParam("name") String name, - @PathParam("permission-name") String permissionName, - PermissionDto permission) throws RepositoryException { + @PathParam("name") String name, + @PathParam("permission-name") String permissionName, + PermissionDto permission) throws RepositoryException { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); Repository repository = load(namespace, name); + RepositoryPermissions.permissionWrite(repository).check(); Permission existingPermission = repository.getPermissions() .stream() .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) @@ -174,10 +169,11 @@ public class PermissionRootResource { @TypeHint(TypeHint.NO_CONTENT.class) @Path("{permission-name}") public Response delete(@PathParam("namespace") String namespace, - @PathParam("name") String name, - @PathParam("permission-name") String permissionName) throws RepositoryException { + @PathParam("name") String name, + @PathParam("permission-name") String permissionName) throws RepositoryException { log.info("try to delete the permission with name: {}.", permissionName); Repository repository = load(namespace, name); + RepositoryPermissions.modify(repository).check(); repository.getPermissions() .stream() .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java index 49adb0e1c0..e7b2da1487 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java @@ -2,8 +2,9 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.Links; import org.mapstruct.*; -import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Permission; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; import javax.inject.Inject; @@ -17,20 +18,28 @@ public abstract class PermissionToPermissionDtoMapper { private ResourceLinks resourceLinks; @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes - public abstract PermissionDto map(Permission permission, @Context NamespaceAndName namespaceAndName); + public abstract PermissionDto map(Permission permission, @Context Repository repository); + + + @BeforeMapping + void validatePermissions(@Context Repository repository) { + RepositoryPermissions.permissionRead(repository).check(); + } /** * Add the self, update and delete links. * - * @param target the mapped dto - * @param namespaceAndName the repository namespace and name + * @param target the mapped dto + * @param repository the repository */ @AfterMapping - void appendLinks(@MappingTarget PermissionDto target, @Context NamespaceAndName namespaceAndName) { + void appendLinks(@MappingTarget PermissionDto target, @Context Repository repository) { Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.permission().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())); - linksBuilder.single(link("update", resourceLinks.permission().update(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))); - linksBuilder.single(link("delete", resourceLinks.permission().delete(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))); + .self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), target.getName())); + if (RepositoryPermissions.permissionWrite(repository).isPermitted()) { + linksBuilder.single(link("update", resourceLinks.permission().update(repository.getNamespace(), repository.getName(), target.getName()))); + linksBuilder.single(link("delete", resourceLinks.permission().delete(repository.getNamespace(), repository.getName(), target.getName()))); + } target.add(linksBuilder.build()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 47e6e93777..3361bc43b1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -313,6 +313,10 @@ class ResourceLinks { return permissionLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("permissions").parameters().method("getAll").parameters().href(); } + String create(String repositoryNamespace, String repositoryName) { + return permissionLinkBuilder.method("getRepositoryResource").parameters(repositoryNamespace, repositoryName).method("permissions").parameters().method("create").parameters().href(); + } + String self(String repositoryNamespace, String repositoryName, String permissionName) { return getLink(repositoryNamespace, repositoryName, permissionName, "get"); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 16f8357850..3736f99301 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -1,13 +1,17 @@ package sonia.scm.api.v2.resources; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableList; +import de.otto.edison.hal.HalRepresentation; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authz.AuthorizationException; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; import org.assertj.core.util.Lists; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockDispatcherFactory; @@ -33,6 +37,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import java.util.stream.Collectors; import java.util.stream.Stream; import static de.otto.edison.hal.Link.link; @@ -53,6 +58,10 @@ import static org.mockito.MockitoAnnotations.initMocks; public class PermissionRootResourceTest { private static final String REPOSITORY_NAMESPACE = "repo_namespace"; private static final String REPOSITORY_NAME = "repo"; + private static final String PERMISSION_WRITE = "repository:permissionWrite:" + REPOSITORY_NAME; + private static final String PERMISSION_READ = "repository:permissionRead:" + REPOSITORY_NAME; + private static final String PERMISSION_OWNER = "repository:modify:" + REPOSITORY_NAME; + private static final String PERMISSION_NAME = "perm"; private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/"; private static final String PATH_OF_ONE_PERMISSION = PATH_OF_ALL_PERMISSIONS + PERMISSION_NAME; @@ -106,15 +115,23 @@ public class PermissionRootResourceTest { @InjectMocks private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper; + private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; + private PermissionRootResource permissionRootResource; + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + @BeforeEach @Before public void prepareEnvironment() { initMocks(this); - permissionRootResource = spy(new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager)); + permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); + permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, null, null, null, null,null, MockProvider.of(permissionRootResource))), null); + .of(new RepositoryResource(null, null, null, null, null, null, null, null, MockProvider.of(permissionRootResource))), null); + subjectThreadState.bind(); + ThreadContext.bind(subject); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); dispatcher.getProviderFactory().registerProvider(RepositoryNotFoundExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(PermissionNotFoundExceptionMapper.class); @@ -123,7 +140,7 @@ public class PermissionRootResourceTest { } @TestFactory - @DisplayName("test endpoints on missing repository and user is Admin") + @DisplayName("test endpoints on missing repository") Stream<DynamicTest> missedRepositoryTestFactory() { return createDynamicTestsToAssertResponses( requestGETAllPermissions.expectedResponseStatus(404), @@ -134,9 +151,13 @@ public class PermissionRootResourceTest { } @TestFactory - @DisplayName("test endpoints on missing permission and user is Admin") + @DisplayName("test endpoints on missing permissions and user is Admin") Stream<DynamicTest> missedPermissionTestFactory() { - authorizedUserHasARepository(); + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); + when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); + when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); return createDynamicTestsToAssertResponses( requestGETPermission.expectedResponseStatus(404), requestPOSTPermission.expectedResponseStatus(201), @@ -146,10 +167,12 @@ public class PermissionRootResourceTest { } @TestFactory - @DisplayName("test endpoints on missing permission and user is not Admin") + @DisplayName("test endpoints on missing permissions and user is not Admin") Stream<DynamicTest> missedPermissionUserForbiddenTestFactory() { Repository mockRepository = mock(Repository.class); when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); + when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class)); return createDynamicTestsToAssertResponses( requestGETPermission.expectedResponseStatus(403), @@ -159,15 +182,27 @@ public class PermissionRootResourceTest { requestPUTPermission.expectedResponseStatus(403)); } + @Test + public void userWithPermissionWritePermissionShouldGetAllPermissionsWithCreateAndUpdateLinks() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); + assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS), PERMISSION_WRITE); + } + + @Test + public void userWithPermissionReadPermissionShouldGetAllPermissionsWithoutCreateAndUpdateLinks() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_READ); + assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS), PERMISSION_READ); + } + @Test public void shouldGetAllPermissions() throws URISyntaxException { - authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); - assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS)); + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_READ); + assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS), PERMISSION_READ); } @Test public void shouldGetPermissionByName() throws URISyntaxException { - authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_READ); Permission expectedPermission = TEST_PERMISSIONS.get(0); assertExpectedRequest(requestGETPermission .expectedResponseStatus(200) @@ -179,7 +214,7 @@ public class PermissionRootResourceTest { PermissionDto actualPermissionDto = mapper.readValue(body, PermissionDto.class); assertThat(actualPermissionDto) .as("response payload match permission object model") - .isEqualToComparingFieldByFieldRecursively(getExpectedPermissionDto(expectedPermission)) + .isEqualToComparingFieldByFieldRecursively(getExpectedPermissionDto(expectedPermission, PERMISSION_READ)) ; } catch (IOException e) { fail(); @@ -190,7 +225,7 @@ public class PermissionRootResourceTest { @Test public void shouldGetCreatedPermissions() throws URISyntaxException { - authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true); ArrayList<Permission> permissions = Lists.newArrayList(TEST_PERMISSIONS); permissions.add(newPermission); @@ -202,12 +237,12 @@ public class PermissionRootResourceTest { .as("POST response has no body") .isBlank()) ); - assertGettingExpectedPermissions(expectedPermissions); + assertGettingExpectedPermissions(expectedPermissions, PERMISSION_WRITE); } @Test public void shouldNotAddExistingPermission() throws URISyntaxException { - authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); Permission newPermission = TEST_PERMISSIONS.get(0); assertExpectedRequest(requestPOSTPermission .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") @@ -217,7 +252,7 @@ public class PermissionRootResourceTest { @Test public void shouldGetUpdatedPermissions() throws URISyntaxException { - authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); Permission modifiedPermission = TEST_PERMISSIONS.get(0); // modify the type to owner modifiedPermission.setType(PermissionType.OWNER); @@ -230,13 +265,13 @@ public class PermissionRootResourceTest { .as("PUT response has no body") .isBlank()) ); - assertGettingExpectedPermissions(expectedPermissions); + assertGettingExpectedPermissions(expectedPermissions, PERMISSION_WRITE); } @Test public void shouldDeletePermissions() throws URISyntaxException { - authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER); Permission deletedPermission = TEST_PERMISSIONS.get(0); ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); assertExpectedRequest(requestDELETEPermission @@ -246,12 +281,12 @@ public class PermissionRootResourceTest { .as("DELETE response has no body") .isBlank()) ); - assertGettingExpectedPermissions(expectedPermissions); + assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ); } @Test public void deletingNotExistingPermissionShouldProcess() throws URISyntaxException { - authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER); Permission deletedPermission = TEST_PERMISSIONS.get(0); ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); assertExpectedRequest(requestDELETEPermission @@ -261,7 +296,7 @@ public class PermissionRootResourceTest { .as("DELETE response has no body") .isBlank()) ); - assertGettingExpectedPermissions(expectedPermissions); + assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ); assertExpectedRequest(requestDELETEPermission .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) .expectedResponseStatus(204) @@ -269,23 +304,32 @@ public class PermissionRootResourceTest { .as("DELETE response has no body") .isBlank()) ); - assertGettingExpectedPermissions(expectedPermissions); + assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ); } - private void assertGettingExpectedPermissions(ImmutableList<Permission> expectedPermissions) throws URISyntaxException { + private void assertGettingExpectedPermissions(ImmutableList<Permission> expectedPermissions, String userPermission) throws URISyntaxException { assertExpectedRequest(requestGETAllPermissions .expectedResponseStatus(200) .responseValidator((response) -> { String body = response.getContentAsString(); ObjectMapper mapper = new ObjectMapper(); try { - List<PermissionDto> actualPermissionDtos = mapper.readValue(body, new TypeReference<List<PermissionDto>>() { - }); - assertThat(actualPermissionDtos) + HalRepresentation halRepresentation = mapper.readValue(body, HalRepresentation.class); + List<HalRepresentation> actualPermissionDtos = halRepresentation.getEmbedded().getItemsBy("permissions", HalRepresentation.class); + List<PermissionDto> permissionDtoStream = actualPermissionDtos.stream() + .map(hal -> { + PermissionDto result = new PermissionDto(); + result.setName(hal.getAttribute("name").asText()); + result.setType(hal.getAttribute("type").asText()); + result.setGroupPermission(hal.getAttribute("groupPermission").asBoolean()); + result.add(hal.getLinks()); + return result; + }).collect(Collectors.toList()); + assertThat(permissionDtoStream) .as("response payload match permission object models") .hasSize(expectedPermissions.size()) .usingRecursiveFieldByFieldElementComparator() - .containsExactlyInAnyOrder(getExpectedPermissionDtos(Lists.newArrayList(expectedPermissions))) + .containsExactlyInAnyOrder(getExpectedPermissionDtos(Lists.newArrayList(expectedPermissions), userPermission)) ; } catch (IOException e) { fail(); @@ -294,39 +338,45 @@ public class PermissionRootResourceTest { ); } - private PermissionDto[] getExpectedPermissionDtos(ArrayList<Permission> permissions) { + private PermissionDto[] getExpectedPermissionDtos(ArrayList<Permission> permissions, String userPermission) { return permissions .stream() - .map(this::getExpectedPermissionDto) + .map(p -> getExpectedPermissionDto(p, userPermission)) .toArray(PermissionDto[]::new); } - private PermissionDto getExpectedPermissionDto(Permission permission) { + private PermissionDto getExpectedPermissionDto(Permission permission, String userPermission) { PermissionDto result = new PermissionDto(); result.setName(permission.getName()); result.setGroupPermission(permission.isGroupPermission()); result.setType(permission.getType().name()); String permissionHref = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS + permission.getName(); - result.add(linkingTo() - .self(permissionHref) - .single(link("update", permissionHref)) - .single(link("delete", permissionHref)) - .build()); + if (PERMISSION_READ.equals(userPermission)) { + result.add(linkingTo() + .self(permissionHref) + .build()); + } else { + result.add(linkingTo() + .self(permissionHref) + .single(link("update", permissionHref)) + .single(link("delete", permissionHref)) + .build()); + } return result; } - private Repository authorizedUserHasARepository() { + private Repository createUserWithRepository(String userPermission) { Repository mockRepository = mock(Repository.class); when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); - doNothing().when(permissionRootResource).checkUserPermission(any()); + when(subject.isPermitted(userPermission != null ? eq(userPermission) : any(String.class))).thenReturn(true); return mockRepository; } - private void authorizedUserHasARepositoryWithPermissions(ArrayList<Permission> permissions) { - when(authorizedUserHasARepository().getPermissions()).thenReturn(permissions); + private void createUserWithRepositoryAndPermissions(ArrayList<Permission> permissions, String userPermission) { + when(createUserWithRepository(userPermission).getPermissions()).thenReturn(permissions); } private Stream<DynamicTest> createDynamicTestsToAssertResponses(ExpectedRequest... expectedRequests) { diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 2b157c4dab..2c0ef898bc 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -40,15 +40,12 @@ import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; import org.hamcrest.Matchers; -import org.junit.Test; import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import static org.mockito.Mockito.*; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import org.junit.Rule; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; @@ -60,6 +57,12 @@ import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.*; + /** * Unit tests for {@link AuthorizationCollector}. * @@ -200,7 +203,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read:one", "repository:read,write:two")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two")); } /** From 6acded2eed473d643be9c8844284fe7c6f334621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Mon, 27 Aug 2018 10:31:07 +0200 Subject: [PATCH 64/73] Flip equals check so that missing properties will not result in NPE --- .../java/sonia/scm/api/v2/resources/RepositoryResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 817eb29f11..c21b7727cb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -176,6 +176,6 @@ public class RepositoryResource { } private Predicate<Repository> nameAndNamespaceStaysTheSame(String namespace, String name) { - return changed -> changed.getName().equals(name) && changed.getNamespace().equals(namespace); + return changed -> name.equals(changed.getName()) && namespace.equals(changed.getNamespace()); } } From 7766a99154ec51518f64039ada3eac59d01e76de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Mon, 27 Aug 2018 10:45:20 +0200 Subject: [PATCH 65/73] Do not delete permissions on repository update --- .../api/v2/resources/RepositoryResource.java | 8 +++++- .../resources/RepositoryRootResourceTest.java | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index c21b7727cb..3ee27f5f84 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -128,11 +128,17 @@ public class RepositoryResource { public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { return adapter.update( loadBy(namespace, name), - existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()), + existing -> processUpdate(repositoryDto, existing), nameAndNamespaceStaysTheSame(namespace, name) ); } + private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) { + Repository changedRepository = dtoToRepositoryMapper.map(repositoryDto, existing.getId()); + changedRepository.setPermissions(existing.getPermissions()); + return changedRepository; + } + @Path("tags/") public TagRootResource tags() { return tagRootResource.get(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index d47b26e5eb..31cacf2d72 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -11,10 +11,13 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Answers; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Permission; +import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryException; import sonia.scm.repository.RepositoryIsNotArchivedException; @@ -35,10 +38,13 @@ import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -240,6 +246,28 @@ public class RepositoryRootResourceTest { verify(repositoryManager).create(any(Repository.class)); } + @Test + public void shouldNotOverwriteExistingPermissionsOnUpdate() throws Exception { + Repository existingRepository = mockRepository("space", "repo"); + existingRepository.setPermissions(singletonList(new Permission("user", PermissionType.READ))); + + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repository = Resources.toByteArray(url); + + ArgumentCaptor<Repository> modifiedRepositoryCaptor = forClass(Repository.class); + doNothing().when(repositoryManager).modify(modifiedRepositoryCaptor.capture()); + + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") + .contentType(VndMediaType.REPOSITORY) + .content(repository); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertFalse(modifiedRepositoryCaptor.getValue().getPermissions().isEmpty()); + } + private PageResult<Repository> createSingletonPageResult(Repository repository) { return new PageResult<>(singletonList(repository), 0); } From f66267e1070b1e0467328cb1139b69cdabc6b671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Mon, 27 Aug 2018 13:04:05 +0200 Subject: [PATCH 66/73] Correct test case --- scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java index f015d9e73d..63d8b46d47 100644 --- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -97,7 +97,7 @@ public class PermissionsITCase { @Test public void readUserShouldNotSeePermissions() { - assertNull(callRepository(USER_WRITE, USER_PASS, repositoryType, HttpStatus.SC_OK) + assertNull(callRepository(USER_READ, USER_PASS, repositoryType, HttpStatus.SC_OK) .extract() .body().jsonPath().getString("_links.permissions.href")); } From 9a630c5677bf42c6a013576919079e579d688526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= <rene.pfeuffer@cloudogu.com> Date: Mon, 27 Aug 2018 11:07:18 +0000 Subject: [PATCH 67/73] Close branch feature/permissions_fix_it_and_iunit5_tests From dca8fcbde197ea1a180ea533f6cdfa0cb57820b2 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 09:46:03 +0200 Subject: [PATCH 68/73] fix wrong comment ordering --- scm-ui/src/config/containers/GlobalConfig.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index ee02da9709..e1f6793fb1 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -24,12 +24,14 @@ type Props = { error: Error, config: Config, configUpdatePermission: boolean, + // dispatch functions modifyConfig: (config: Config, callback?: () => void) => void, - // context objects - t: string => string, fetchConfig: void => void, configReset: void => void, + + // context objects + t: string => string, history: History }; From eb207cecfddbdf79d427dd6c4378a8fab4d64af8 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 09:47:33 +0200 Subject: [PATCH 69/73] fix errors with null values for array types --- scm-ui/src/config/modules/config.js | 16 +++++++++- scm-ui/src/config/modules/config.test.js | 37 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 6fe1594236..5cf331c324 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -117,12 +117,26 @@ export function modifyConfigReset() { //reducer +function removeNullValues(config: Config) { + if (!config.adminGroups) { + config.adminGroups = []; + } + if (!config.adminUsers) { + config.adminUsers = []; + } + if (!config.proxyExcludes) { + config.proxyExcludes = []; + } + return config; +} + function reducer(state: any = {}, action: any = {}) { switch (action.type) { case FETCH_CONFIG_SUCCESS: + const config = removeNullValues(action.payload); return { ...state, - entries: action.payload, + entries: config, configUpdatePermission: action.payload._links.update ? true : false }; default: diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index d991bd929a..09d794dcb0 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -56,6 +56,35 @@ const config = { } }; +const configWithNullValues = { + proxyPassword: null, + proxyPort: 8080, + proxyServer: "proxy.mydomain.com", + proxyUser: null, + enableProxy: false, + realmDescription: "SONIA :: SCM Manager", + enableRepositoryArchive: false, + disableGroupingGrid: false, + dateFormat: "YYYY-MM-DD HH:mm:ss", + anonymousAccessEnabled: false, + adminGroups: null, + adminUsers: null, + baseUrl: "http://localhost:8081/scm", + forceBaseUrl: false, + loginAttemptLimit: -1, + proxyExcludes: null, + skipFailedAuthenticators: false, + pluginUrl: + "http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false", + loginAttemptLimitTimeout: 300, + enabledXsrfProtection: true, + defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy", + _links: { + self: { href: "http://localhost:8081/scm/api/rest/v2/config" }, + update: { href: "http://localhost:8081/scm/api/rest/v2/config" } + } +}; + const responseBody = { entries: config, configUpdatePermission: false @@ -175,6 +204,14 @@ describe("config reducer", () => { const newState = reducer({}, fetchConfigSuccess(config)); expect(newState.entries).toBe(config); }); + + it("should return empty arrays for null values", () => { + const config = reducer({}, fetchConfigSuccess(configWithNullValues)) + .entries; + expect(config.adminUsers).toEqual([]); + expect(config.adminGroups).toEqual([]); + expect(config.proxyExcludes).toEqual([]); + }); }); describe("selector tests", () => { From e3b6b6c18fff1e96ce044845433ea317f6ffa5d8 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 10:10:41 +0200 Subject: [PATCH 70/73] remove unnecessary fetches --- scm-ui/src/config/containers/GlobalConfig.js | 11 ++--------- scm-ui/src/config/modules/config.js | 1 + 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index e1f6793fb1..3d8858daf9 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -17,7 +17,6 @@ import ErrorPage from "../../components/ErrorPage"; import type { Config } from "../types/Config"; import ConfigForm from "../components/form/ConfigForm"; import Loading from "../../components/Loading"; -import type { History } from "history"; type Props = { loading: boolean, @@ -31,23 +30,17 @@ type Props = { configReset: void => void, // context objects - t: string => string, - history: History + t: string => string }; class GlobalConfig extends React.Component<Props> { - configModified = () => () => { - this.props.fetchConfig(); - this.props.history.push(`/config`); - }; - componentDidMount() { this.props.configReset(); this.props.fetchConfig(); } modifyConfig = (config: Config) => { - this.props.modifyConfig(config, this.configModified()); + this.props.modifyConfig(config); }; render() { diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 5cf331c324..91b20a8a09 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -132,6 +132,7 @@ function removeNullValues(config: Config) { function reducer(state: any = {}, action: any = {}) { switch (action.type) { + case MODIFY_CONFIG_SUCCESS: case FETCH_CONFIG_SUCCESS: const config = removeNullValues(action.payload); return { From dd76a00aa1602643897410e474b75e1831ed09a3 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 10:10:59 +0200 Subject: [PATCH 71/73] remove outdated todo --- scm-ui/src/config/components/form/ProxySettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index 42cd0ab228..9b9976cbb4 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -12,7 +12,7 @@ type Props = { proxyServer: string, proxyUser: string, enableProxy: boolean, - proxyExcludes: string[], //TODO: einbauen! + proxyExcludes: string[], t: string => string, onChange: (boolean, any, string) => void, hasUpdatePermission: boolean From 5eb55a9baa7b39e8e8fecc9ef216e74181acd753 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 10:12:01 +0200 Subject: [PATCH 72/73] extract logic of AdminGroupTable, AdminUserTable and ProxyExcludesTable in to ArrayConfigTable --- .../components/table/AdminGroupTable.js | 42 ++++++---------- .../config/components/table/AdminUserTable.js | 46 ++++++------------ .../components/table/ArrayConfigTable.js | 48 +++++++++++++++++++ .../components/table/ProxyExcludesTable.js | 38 ++++----------- 4 files changed, 87 insertions(+), 87 deletions(-) create mode 100644 scm-ui/src/config/components/table/ArrayConfigTable.js diff --git a/scm-ui/src/config/components/table/AdminGroupTable.js b/scm-ui/src/config/components/table/AdminGroupTable.js index fc05c75f06..db9f83af84 100644 --- a/scm-ui/src/config/components/table/AdminGroupTable.js +++ b/scm-ui/src/config/components/table/AdminGroupTable.js @@ -1,48 +1,34 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import { RemoveEntryOfTableButton } from "../../../components/buttons"; +import ArrayConfigTable from "./ArrayConfigTable"; type Props = { adminGroups: string[], - t: string => string, onChange: (boolean, any, string) => void, - disabled: boolean + disabled: boolean, + + // context props + t: string => string }; type State = {}; class AdminGroupTable extends React.Component<Props, State> { render() { - const { t, disabled } = this.props; + const { t, disabled, adminGroups } = this.props; return ( - <div> - <label className="label">{t("admin-settings.admin-groups")}</label> - <table className="table is-hoverable is-fullwidth"> - <tbody> - {this.props.adminGroups.map(group => { - return ( - <tr key={group}> - <td key={group}>{group}</td> - <td> - <RemoveEntryOfTableButton - entryname={group} - removeEntry={this.removeEntry} - disabled={disabled} - label={t("admin-settings.remove-group-button")} - /> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> + <ArrayConfigTable + items={adminGroups} + label={t("admin-settings.admin-groups")} + removeLabel={t("admin-settings.remove-group-button")} + onRemove={this.removeEntry} + disabled={disabled} + /> ); } - removeEntry = (groupname: string) => { - const newGroups = this.props.adminGroups.filter(name => name !== groupname); + removeEntry = (newGroups: string[]) => { this.props.onChange(true, newGroups, "adminGroups"); }; } diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js index c622a0e027..d1f35e8424 100644 --- a/scm-ui/src/config/components/table/AdminUserTable.js +++ b/scm-ui/src/config/components/table/AdminUserTable.js @@ -1,48 +1,32 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import { RemoveEntryOfTableButton } from "../../../components/buttons"; +import ArrayConfigTable from "./ArrayConfigTable"; type Props = { adminUsers: string[], - t: string => string, onChange: (boolean, any, string) => void, - disabled: boolean + disabled: boolean, + + // context props + t: string => string }; -type State = {}; - -class AdminUserTable extends React.Component<Props, State> { +class AdminUserTable extends React.Component<Props> { render() { - const { t, disabled } = this.props; + const { adminUsers, t, disabled } = this.props; return ( - <div> - <label className="label">{t("admin-settings.admin-users")}</label> - <table className="table is-hoverable is-fullwidth"> - <tbody> - {this.props.adminUsers.map(user => { - return ( - <tr key={user}> - <td key={user}>{user}</td> - <td> - <RemoveEntryOfTableButton - entryname={user} - removeEntry={this.removeEntry} - disabled={disabled} - label={t("admin-settings.remove-user-button")} - /> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> + <ArrayConfigTable + items={adminUsers} + label={t("admin-settings.admin-users")} + removeLabel={t("admin-settings.remove-user-button")} + onRemove={this.removeEntry} + disabled={disabled} + /> ); } - removeEntry = (username: string) => { - const newUsers = this.props.adminUsers.filter(name => name !== username); + removeEntry = (newUsers: string[]) => { this.props.onChange(true, newUsers, "adminUsers"); }; } diff --git a/scm-ui/src/config/components/table/ArrayConfigTable.js b/scm-ui/src/config/components/table/ArrayConfigTable.js new file mode 100644 index 0000000000..f47e6e9dca --- /dev/null +++ b/scm-ui/src/config/components/table/ArrayConfigTable.js @@ -0,0 +1,48 @@ +//@flow +import React from "react"; +import { RemoveEntryOfTableButton } from "../../../components/buttons"; + +type Props = { + items: string[], + label: string, + removeLabel: string, + onRemove: (string[], string) => void, + disabled: boolean +}; + +class ArrayConfigTable extends React.Component<Props> { + render() { + const { label, disabled, removeLabel, items } = this.props; + return ( + <div> + <label className="label">{label}</label> + <table className="table is-hoverable is-fullwidth"> + <tbody> + {items.map(item => { + return ( + <tr key={item}> + <td>{item}</td> + <td> + <RemoveEntryOfTableButton + entryname={item} + removeEntry={this.removeEntry} + disabled={disabled} + label={removeLabel} + /> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); + } + + removeEntry = (item: string) => { + const newItems = this.props.items.filter(name => name !== item); + this.props.onRemove(newItems, item); + }; +} + +export default ArrayConfigTable; diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js index 4476442c48..a7849ffdf2 100644 --- a/scm-ui/src/config/components/table/ProxyExcludesTable.js +++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import { RemoveEntryOfTableButton } from "../../../components/buttons"; +import ArrayConfigTable from "./ArrayConfigTable"; type Props = { proxyExcludes: string[], @@ -14,37 +14,19 @@ type State = {}; class ProxyExcludesTable extends React.Component<Props, State> { render() { - const { t } = this.props; + const { proxyExcludes, disabled, t } = this.props; return ( - <div> - <label className="label">{t("proxy-settings.proxy-excludes")}</label> - <table className="table is-hoverable is-fullwidth"> - <tbody> - {this.props.proxyExcludes.map(excludes => { - return ( - <tr key={excludes}> - <td key={excludes}>{excludes}</td> - <td> - <RemoveEntryOfTableButton - entryname={excludes} - removeEntry={this.removeEntry} - disabled={this.props.disabled} - label={t("proxy-settings.remove-proxy-exclude-button")} - /> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> + <ArrayConfigTable + items={proxyExcludes} + label={t("proxy-settings.proxy-excludes")} + removeLabel={t("proxy-settings.remove-proxy-exclude-button")} + onRemove={this.removeEntry} + disabled={disabled} + /> ); } - removeEntry = (excludename: string) => { - const newExcludes = this.props.proxyExcludes.filter( - name => name !== excludename - ); + removeEntry = (newExcludes: string[]) => { this.props.onChange(true, newExcludes, "proxyExcludes"); }; } From 7ccd26bc6ffed57c9c0486f8bcde0f13ef7f3c83 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 08:14:18 +0000 Subject: [PATCH 73/73] Close branch feature/ui-for-scm2_global-config