mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-05 04:55:50 +01:00
Add "enable file search" flag on global config.
Make the repository file search deactivatable via the global config. This feature could overwhelm the server on repositories with millions of file therefore it now can be turned off. Committed-by: Rene Pfeuffer <rene.pfeuffer@cloudogu.com> Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 94 KiB |
@@ -43,6 +43,9 @@ Die URL des RSS Release Feed des SCM-Managers. Darüber wird über die neue SCM-
|
||||
#### User converter
|
||||
Ist der Benutzer Konverter aktiviert, werden alle internen Benutzer beim Einloggen über ein externes System automatisch zu externen Benutzern konvertiert. Nach dem Konvertieren können sich die Benutzer nicht mehr mit dem lokalen SCM-Manager Passwort einloggen, sondern nur noch über das Fremdsystem.
|
||||
|
||||
#### Datei Suche
|
||||
Die Dateisuche ermöglicht es Dateipfade in Repositories zu suchen. Bei sehr großen Repositories kann die Suche zu Speicherproblemen führen. In diesem Fall kann die Suchfunktion hier deaktiviert werden.
|
||||
|
||||
#### Fallback E-Mail Domain Name
|
||||
Dieser Domain Name wird genutzt, wenn für einen User eine E-Mail-Adresse benötigt wird, für den keine hinterlegt ist. Diese Domain wird nicht zum Versenden von E-Mails genutzt und auch keine anderweitige Verbindung aufgebaut.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 88 KiB |
@@ -39,9 +39,12 @@ Example: If anonymous access is enabled and the "_anonymous" user has full acces
|
||||
#### Release Feed Url
|
||||
The url of the RSS Release Feed for SCM-Manager. This provides up-to-date version information. To disable this feature just leave the url blank.
|
||||
|
||||
#### User converter
|
||||
#### User Converter
|
||||
Internal users will automatically be converted to external on their first login using an external system. After conversion the users may only log in using the external system.
|
||||
|
||||
#### File Search
|
||||
File paths can be searched inside repositories. For very big repositories, the search may lead to memory issues. In this case, the search function can be disabled here.
|
||||
|
||||
#### Fallback Mail Domain Name
|
||||
This domain name will be used to create email addresses for users without one when needed. It will not be used to send mails nor will be accessed otherwise.
|
||||
|
||||
|
||||
2
gradle/changelog/deactivate_file_search.yaml
Normal file
2
gradle/changelog/deactivate_file_search.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Make file search deactivatable via global config
|
||||
@@ -221,6 +221,14 @@ public class ScmConfiguration implements Configuration {
|
||||
@XmlElement(name = "api-keys")
|
||||
private boolean enabledApiKeys = true;
|
||||
|
||||
/**
|
||||
* Enables file search inside repositories.
|
||||
*
|
||||
* @since 2.45.0
|
||||
*/
|
||||
@XmlElement(name = "file-search")
|
||||
private boolean enabledFileSearch = true;
|
||||
|
||||
@XmlElement(name = "namespace-strategy")
|
||||
private String namespaceStrategy = "UsernameNamespaceStrategy";
|
||||
|
||||
@@ -500,6 +508,14 @@ public class ScmConfiguration implements Configuration {
|
||||
return enableProxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the repository file search is enabled.
|
||||
*
|
||||
* @return {@code true} if the api keys is enabled
|
||||
* @since 2.45.0
|
||||
*/
|
||||
public boolean isEnabledFileSearch() { return enabledFileSearch; }
|
||||
|
||||
public boolean isForceBaseUrl() {
|
||||
return forceBaseUrl;
|
||||
}
|
||||
@@ -711,6 +727,16 @@ public class ScmConfiguration implements Configuration {
|
||||
this.enabledApiKeys = enabledApiKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@code true} to enable file search for repositories.
|
||||
*
|
||||
* @param enabledFileSearch {@code true} to enable file search for repositories
|
||||
* @since 2.45.0
|
||||
*/
|
||||
public void setEnabledFileSearch(boolean enabledFileSearch) {
|
||||
this.enabledFileSearch = enabledFileSearch;
|
||||
}
|
||||
|
||||
public void setNamespaceStrategy(String namespaceStrategy) {
|
||||
this.namespaceStrategy = namespaceStrategy;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ describe("Test config hooks", () => {
|
||||
enabledUserConverter: false,
|
||||
enabledXsrfProtection: false,
|
||||
enabledApiKeys: false,
|
||||
enabledFileSearch: true,
|
||||
forceBaseUrl: false,
|
||||
loginAttemptLimit: 0,
|
||||
loginAttemptLimitTimeout: 0,
|
||||
@@ -62,9 +63,9 @@ describe("Test config hooks", () => {
|
||||
skipFailedAuthenticators: false,
|
||||
_links: {
|
||||
update: {
|
||||
href: "/config"
|
||||
}
|
||||
}
|
||||
href: "/config",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
@@ -77,7 +78,7 @@ describe("Test config hooks", () => {
|
||||
setIndexLink(queryClient, "config", "/config");
|
||||
fetchMock.get("/api/v2/config", config);
|
||||
const { result, waitFor } = renderHook(() => useConfig(), {
|
||||
wrapper: createWrapper(undefined, queryClient)
|
||||
wrapper: createWrapper(undefined, queryClient),
|
||||
});
|
||||
await waitFor(() => !!result.current.data);
|
||||
expect(result.current.data).toEqual(config);
|
||||
@@ -91,15 +92,15 @@ describe("Test config hooks", () => {
|
||||
|
||||
const newConfig = {
|
||||
...config,
|
||||
baseUrl: "/hog"
|
||||
baseUrl: "/hog",
|
||||
};
|
||||
|
||||
fetchMock.putOnce("/api/v2/config", {
|
||||
status: 200
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useUpdateConfig(), {
|
||||
wrapper: createWrapper(undefined, queryClient)
|
||||
wrapper: createWrapper(undefined, queryClient),
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
|
||||
@@ -54,4 +54,5 @@ export type Config = HalRepresentation & {
|
||||
mailDomainName: string;
|
||||
emergencyContacts: string[];
|
||||
enabledApiKeys: boolean;
|
||||
enabledFileSearch: boolean;
|
||||
};
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
"login-attempt-limit": "Limit für Anmeldeversuche",
|
||||
"login-attempt-limit-timeout": "Timeout bei fehlgeschlagenen Anmeldeversuchen"
|
||||
},
|
||||
"function-settings": {
|
||||
"name": "Funktionen"
|
||||
},
|
||||
"general-settings": {
|
||||
"realm-description": "Realm Beschreibung",
|
||||
"disable-grouping-grid": "Gruppen deaktivieren",
|
||||
@@ -74,6 +77,7 @@
|
||||
"enabled-xsrf-protection": "XSRF Protection aktivieren",
|
||||
"enabled-user-converter": "Benutzer Konverter aktivieren",
|
||||
"enabled-api-keys": "API Schlüssel aktivieren",
|
||||
"enabled-file-search": "Dateisuche aktivieren",
|
||||
"namespace-strategy": "Namespace Strategie",
|
||||
"login-info-url": "Login Info URL",
|
||||
"emergencyContacts": {
|
||||
@@ -110,6 +114,7 @@
|
||||
"enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.",
|
||||
"enabledUserConverterHelpText": "Benutzer Konverter aktivieren. Interne Benutzer werden beim Einloggen über ein Fremdsystem zu externen Benutzern konvertiert.",
|
||||
"enabledApiKeysHelpText": "API Schlüssel aktivieren. Alle Benutzer dürfen API Schlüssel als zusätzliche Methode zur Authentifizierung nutzen.",
|
||||
"enabledFileSearchHelpText": "Dateisuche aktivieren. Dateipfade können innerhalb eines Repositories im Source Viewer gesucht werden.",
|
||||
"nameSpaceStrategyHelpText": "Strategie für Namespaces.",
|
||||
"loginInfoUrlHelpText": "URL zu der Login Information (Plugin und Feature Tipps auf der Login Seite). Um die Login Information zu deaktivieren, kann das Feld leer gelassen werden."
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
"login-attempt-limit": "Login Attempt Limit",
|
||||
"login-attempt-limit-timeout": "Login Attempt Limit Timeout"
|
||||
},
|
||||
"function-settings": {
|
||||
"name": "Functions"
|
||||
},
|
||||
"general-settings": {
|
||||
"realm-description": "Realm Description",
|
||||
"disable-grouping-grid": "Disable Grouping Grid",
|
||||
@@ -74,6 +77,7 @@
|
||||
"enabled-xsrf-protection": "Enabled XSRF Protection",
|
||||
"enabled-user-converter": "Enabled User Converter",
|
||||
"enabled-api-keys": "Enabled API Keys",
|
||||
"enabled-file-search": "Enabled File Search",
|
||||
"namespace-strategy": "Namespace Strategy",
|
||||
"login-info-url": "Login Info URL",
|
||||
"emergencyContacts": {
|
||||
@@ -110,6 +114,7 @@
|
||||
"enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.",
|
||||
"enabledUserConverterHelpText": "Enable User Converter. Internal users will automatically be converted to external on their first login using an external system.",
|
||||
"enabledApiKeysHelpText": "Enable API Keys. API Keys can be used as additional authentication method by every user.",
|
||||
"enabledFileSearchHelpText": "Enable file search. File paths can be searched inside a single repository on the source viewer.",
|
||||
"nameSpaceStrategyHelpText": "The namespace strategy.",
|
||||
"loginInfoUrlHelpText": "URL to login information (plugin and feature tips at login page). If this is omitted, no login information will be displayed."
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@ class BaseUrlSettings extends React.Component<Props> {
|
||||
const { t, baseUrl, forceBaseUrl, hasUpdatePermission } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<Subtitle subtitle={t("base-url-settings.name")} />
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("base-url-settings.base-url")}
|
||||
onChange={this.handleBaseUrlChange}
|
||||
@@ -58,7 +58,7 @@ class BaseUrlSettings extends React.Component<Props> {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import GeneralSettings from "./GeneralSettings";
|
||||
import BaseUrlSettings from "./BaseUrlSettings";
|
||||
import LoginAttempt from "./LoginAttempt";
|
||||
import PluginSettings from "./PluginSettings";
|
||||
import FunctionSettings from "./FunctionSettings";
|
||||
|
||||
type Props = {
|
||||
submitForm: (p: Config) => void;
|
||||
@@ -46,7 +47,7 @@ const ConfigForm: FC<Props> = ({
|
||||
loading,
|
||||
configReadPermission,
|
||||
configUpdatePermission,
|
||||
namespaceStrategies
|
||||
namespaceStrategies,
|
||||
}) => {
|
||||
const [t] = useTranslation("config");
|
||||
const [innerConfig, setInnerConfig] = useState<Config>({
|
||||
@@ -77,7 +78,7 @@ const ConfigForm: FC<Props> = ({
|
||||
mailDomainName: "",
|
||||
emergencyContacts: [],
|
||||
enabledApiKeys: true,
|
||||
_links: {}
|
||||
_links: {},
|
||||
});
|
||||
const [showNotification, setShowNotification] = useState(false);
|
||||
const [changed, setChanged] = useState(false);
|
||||
@@ -86,7 +87,7 @@ const ConfigForm: FC<Props> = ({
|
||||
loginAttemptLimit: boolean;
|
||||
}>({
|
||||
loginAttemptLimitTimeout: false,
|
||||
loginAttemptLimit: false
|
||||
loginAttemptLimit: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -149,14 +150,20 @@ const ConfigForm: FC<Props> = ({
|
||||
releaseFeedUrl={innerConfig.releaseFeedUrl}
|
||||
mailDomainName={innerConfig.mailDomainName}
|
||||
enabledXsrfProtection={innerConfig.enabledXsrfProtection}
|
||||
enabledUserConverter={innerConfig.enabledUserConverter}
|
||||
enabledApiKeys={innerConfig.enabledApiKeys}
|
||||
emergencyContacts={innerConfig.emergencyContacts}
|
||||
namespaceStrategy={innerConfig.namespaceStrategy}
|
||||
onChange={onChange}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<FunctionSettings
|
||||
enabledUserConverter={innerConfig.enabledUserConverter}
|
||||
enabledApiKeys={innerConfig.enabledApiKeys}
|
||||
enabledFileSearch={innerConfig.enabledFileSearch}
|
||||
onChange={onChange}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<LoginAttempt
|
||||
loginAttemptLimit={innerConfig.loginAttemptLimit}
|
||||
loginAttemptLimitTimeout={innerConfig.loginAttemptLimitTimeout}
|
||||
|
||||
100
scm-ui/ui-webapp/src/admin/components/form/FunctionSettings.tsx
Normal file
100
scm-ui/ui-webapp/src/admin/components/form/FunctionSettings.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { ConfigChangeHandler } from "@scm-manager/ui-types";
|
||||
import { Checkbox, Subtitle } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
enabledUserConverter: boolean;
|
||||
enabledApiKeys: boolean;
|
||||
enabledFileSearch: boolean;
|
||||
onChange: ConfigChangeHandler;
|
||||
hasUpdatePermission: boolean;
|
||||
};
|
||||
|
||||
const FunctionSettings: FC<Props> = ({
|
||||
enabledFileSearch,
|
||||
enabledUserConverter,
|
||||
enabledApiKeys,
|
||||
onChange,
|
||||
hasUpdatePermission,
|
||||
}) => {
|
||||
const { t } = useTranslation("config");
|
||||
|
||||
const handleEnabledApiKeysChange = (value: boolean) => {
|
||||
onChange(true, value, "enabledApiKeys");
|
||||
};
|
||||
const handleEnabledUserConverterChange = (value: boolean) => {
|
||||
onChange(true, value, "enabledUserConverter");
|
||||
};
|
||||
const handleEnabledFileSearchChange = (value: boolean) => {
|
||||
onChange(true, value, "enabledFileSearch");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("function-settings.name")} />
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<Checkbox
|
||||
label={t("general-settings.enabled-user-converter")}
|
||||
onChange={handleEnabledUserConverterChange}
|
||||
checked={enabledUserConverter}
|
||||
title={t("general-settings.enabled-user-converter")}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enabledUserConverterHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<Checkbox
|
||||
label={t("general-settings.enabled-api-keys")}
|
||||
onChange={handleEnabledApiKeysChange}
|
||||
checked={enabledApiKeys}
|
||||
title={t("general-settings.enabled-api-keys")}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enabledApiKeysHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<Checkbox
|
||||
label={t("general-settings.enabled-file-search")}
|
||||
onChange={handleEnabledFileSearchChange}
|
||||
checked={enabledFileSearch}
|
||||
title={t("general-settings.enabled-file-search")}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enabledFileSearch")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FunctionSettings;
|
||||
@@ -41,8 +41,6 @@ type Props = {
|
||||
releaseFeedUrl: string;
|
||||
mailDomainName: string;
|
||||
enabledXsrfProtection: boolean;
|
||||
enabledUserConverter: boolean;
|
||||
enabledApiKeys: boolean;
|
||||
emergencyContacts: string[];
|
||||
namespaceStrategy: string;
|
||||
namespaceStrategies?: NamespaceStrategies;
|
||||
@@ -58,8 +56,6 @@ const GeneralSettings: FC<Props> = ({
|
||||
releaseFeedUrl,
|
||||
mailDomainName,
|
||||
enabledXsrfProtection,
|
||||
enabledUserConverter,
|
||||
enabledApiKeys,
|
||||
emergencyContacts,
|
||||
namespaceStrategy,
|
||||
namespaceStrategies,
|
||||
@@ -79,9 +75,6 @@ const GeneralSettings: FC<Props> = ({
|
||||
const handleEnabledXsrfProtectionChange = (value: boolean) => {
|
||||
onChange(true, value, "enabledXsrfProtection");
|
||||
};
|
||||
const handleEnabledUserConverterChange = (value: boolean) => {
|
||||
onChange(true, value, "enabledUserConverter");
|
||||
};
|
||||
const handleAnonymousMode = (value: string) => {
|
||||
onChange(true, value as AnonymousMode, "anonymousMode");
|
||||
};
|
||||
@@ -97,9 +90,6 @@ const GeneralSettings: FC<Props> = ({
|
||||
const handleMailDomainNameChange = (value: string) => {
|
||||
onChange(true, value, "mailDomainName");
|
||||
};
|
||||
const handleEnabledApiKeysChange = (value: boolean) => {
|
||||
onChange(true, value, "enabledApiKeys");
|
||||
};
|
||||
const handleEmergencyContactsChange = (p: Option<AutocompleteObject>[]) => {
|
||||
onChange(
|
||||
true,
|
||||
@@ -111,7 +101,7 @@ const GeneralSettings: FC<Props> = ({
|
||||
return (
|
||||
<div>
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("general-settings.realm-description")}
|
||||
onChange={handleRealmDescriptionChange}
|
||||
@@ -120,7 +110,9 @@ const GeneralSettings: FC<Props> = ({
|
||||
helpText={t("help.realmDescriptionHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<NamespaceStrategySelect
|
||||
label={t("general-settings.namespace-strategy")}
|
||||
onChange={handleNamespaceStrategyChange}
|
||||
@@ -132,7 +124,7 @@ const GeneralSettings: FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("general-settings.login-info-url")}
|
||||
onChange={handleLoginInfoUrlChange}
|
||||
@@ -141,7 +133,9 @@ const GeneralSettings: FC<Props> = ({
|
||||
helpText={t("help.loginInfoUrlHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<Checkbox
|
||||
label={t("general-settings.enabled-xsrf-protection")}
|
||||
onChange={handleEnabledXsrfProtectionChange}
|
||||
@@ -153,7 +147,7 @@ const GeneralSettings: FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="column">
|
||||
<Select
|
||||
label={t("general-settings.anonymousMode.title")}
|
||||
onChange={handleAnonymousMode}
|
||||
@@ -169,19 +163,9 @@ const GeneralSettings: FC<Props> = ({
|
||||
testId={"anonymous-mode-select"}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<Checkbox
|
||||
label={t("general-settings.enabled-user-converter")}
|
||||
onChange={handleEnabledUserConverterChange}
|
||||
checked={enabledUserConverter}
|
||||
title={t("general-settings.enabled-user-converter")}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enabledUserConverterHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("general-settings.mail-domain-name")}
|
||||
onChange={handleMailDomainNameChange}
|
||||
@@ -190,19 +174,9 @@ const GeneralSettings: FC<Props> = ({
|
||||
helpText={t("help.mailDomainNameHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<Checkbox
|
||||
label={t("general-settings.enabled-api-keys")}
|
||||
onChange={handleEnabledApiKeysChange}
|
||||
checked={enabledApiKeys}
|
||||
title={t("general-settings.enabled-api-keys")}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enabledApiKeysHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("general-settings.alerts-url")}
|
||||
onChange={handleAlertsUrlChange}
|
||||
@@ -211,7 +185,9 @@ const GeneralSettings: FC<Props> = ({
|
||||
helpText={t("help.alertsUrlHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("general-settings.release-feed-url")}
|
||||
onChange={handleReleaseFeedUrlChange}
|
||||
|
||||
@@ -44,7 +44,7 @@ class LoginAttempt extends React.Component<Props, State> {
|
||||
|
||||
this.state = {
|
||||
loginAttemptLimitError: false,
|
||||
loginAttemptLimitTimeoutError: false
|
||||
loginAttemptLimitTimeoutError: false,
|
||||
};
|
||||
}
|
||||
render() {
|
||||
@@ -54,7 +54,7 @@ class LoginAttempt extends React.Component<Props, State> {
|
||||
<div>
|
||||
<Subtitle subtitle={t("login-attempt.name")} />
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="column">
|
||||
<InputField
|
||||
type="number"
|
||||
label={t("login-attempt.login-attempt-limit")}
|
||||
@@ -66,7 +66,9 @@ class LoginAttempt extends React.Component<Props, State> {
|
||||
helpText={t("help.loginAttemptLimitHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<InputField
|
||||
type="number"
|
||||
label={t("login-attempt.login-attempt-limit-timeout")}
|
||||
@@ -87,7 +89,7 @@ class LoginAttempt extends React.Component<Props, State> {
|
||||
handleLoginAttemptLimitChange = (value: string) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loginAttemptLimitError: !validator.isNumberValid(value)
|
||||
loginAttemptLimitError: !validator.isNumberValid(value),
|
||||
});
|
||||
this.props.onChange(validator.isNumberValid(value), Number(value), "loginAttemptLimit");
|
||||
};
|
||||
@@ -95,7 +97,7 @@ class LoginAttempt extends React.Component<Props, State> {
|
||||
handleLoginAttemptLimitTimeoutChange = (value: string) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loginAttemptLimitTimeoutError: !validator.isNumberValid(value)
|
||||
loginAttemptLimitTimeoutError: !validator.isNumberValid(value),
|
||||
});
|
||||
this.props.onChange(validator.isNumberValid(value), Number(value), "loginAttemptLimitTimeout");
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ class NamespaceStrategySelect extends React.Component<Props> {
|
||||
available = namespaceStrategies.available;
|
||||
}
|
||||
|
||||
return available.map(ns => {
|
||||
return available.map((ns) => {
|
||||
const key = "namespaceStrategies." + ns;
|
||||
let label = t(key);
|
||||
if (label === key) {
|
||||
@@ -51,7 +51,7 @@ class NamespaceStrategySelect extends React.Component<Props> {
|
||||
}
|
||||
return {
|
||||
value: ns,
|
||||
label: label
|
||||
label: label,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -75,6 +75,7 @@ class NamespaceStrategySelect extends React.Component<Props> {
|
||||
disabled={disabled}
|
||||
options={nsOptions}
|
||||
helpText={helpText}
|
||||
className="is-fullwidth"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ const PluginSettings: FC<Props> = ({ pluginUrl, pluginAuthUrl, onChange, hasUpda
|
||||
<div>
|
||||
<Subtitle subtitle={t("pluginSettings.subtitle")} />
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("pluginSettings.pluginUrl")}
|
||||
onChange={handlePluginCenterUrlChange}
|
||||
@@ -57,7 +57,9 @@ const PluginSettings: FC<Props> = ({ pluginUrl, pluginAuthUrl, onChange, hasUpda
|
||||
helpText={t("help.pluginUrlHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("pluginSettings.pluginAuthUrl")}
|
||||
onChange={handlePluginCenterAuthUrlChange}
|
||||
|
||||
@@ -40,16 +40,8 @@ type Props = WithTranslation & {
|
||||
|
||||
class ProxySettings extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
proxyPassword,
|
||||
proxyPort,
|
||||
proxyServer,
|
||||
proxyUser,
|
||||
enableProxy,
|
||||
proxyExcludes,
|
||||
hasUpdatePermission
|
||||
} = this.props;
|
||||
const { t, proxyPassword, proxyPort, proxyServer, proxyUser, enableProxy, proxyExcludes, hasUpdatePermission } =
|
||||
this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -66,7 +58,7 @@ class ProxySettings extends React.Component<Props> {
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="column is-full">
|
||||
<InputField
|
||||
label={t("proxySettings.server")}
|
||||
value={proxyServer}
|
||||
@@ -75,7 +67,9 @@ class ProxySettings extends React.Component<Props> {
|
||||
helpText={t("proxySettings.serverHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column is-full">
|
||||
<InputField
|
||||
label={t("proxySettings.port")}
|
||||
value={proxyPort}
|
||||
@@ -86,7 +80,7 @@ class ProxySettings extends React.Component<Props> {
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="column is-full">
|
||||
<InputField
|
||||
label={t("proxySettings.user")}
|
||||
value={proxyUser}
|
||||
@@ -95,7 +89,9 @@ class ProxySettings extends React.Component<Props> {
|
||||
helpText={t("proxySettings.userHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<InputField
|
||||
label={t("proxySettings.password")}
|
||||
onChange={this.handleProxyPasswordChange}
|
||||
|
||||
@@ -59,6 +59,7 @@ public class ConfigDto extends HalRepresentation implements UpdateConfigDto {
|
||||
private boolean enabledXsrfProtection;
|
||||
private boolean enabledUserConverter;
|
||||
private boolean enabledApiKeys;
|
||||
private boolean enabledFileSearch;
|
||||
private String namespaceStrategy;
|
||||
private String loginInfoUrl;
|
||||
private String alertsUrl;
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.ObjectFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.admin.ScmConfigurationStore;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.DefaultRepositoryExportingCheck;
|
||||
import sonia.scm.repository.Feature;
|
||||
@@ -51,6 +52,7 @@ import sonia.scm.web.EdisonHalAppender;
|
||||
import sonia.scm.web.api.RepositoryToHalMapper;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -69,7 +71,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
@Inject
|
||||
private ScmConfiguration scmConfiguration;
|
||||
private ScmConfigurationStore scmConfigurationStore;
|
||||
@Inject
|
||||
private RepositoryServiceFactory serviceFactory;
|
||||
@Inject
|
||||
@@ -171,7 +173,9 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
linksBuilder.single(link("changesets", resourceLinks.changeset().all(repository.getNamespace(), repository.getName())));
|
||||
linksBuilder.single(link("sources", resourceLinks.source().selfWithoutRevision(repository.getNamespace(), repository.getName())));
|
||||
linksBuilder.single(link("content", resourceLinks.source().content(repository.getNamespace(), repository.getName())));
|
||||
if (scmConfigurationStore.get().isEnabledFileSearch()) {
|
||||
linksBuilder.single(link("paths", resourceLinks.repository().paths(repository.getNamespace(), repository.getName())));
|
||||
}
|
||||
if (RepositoryPermissions.healthCheck(repository).isPermitted() && !healthCheckService.checkRunning(repository)) {
|
||||
linksBuilder.single(link("runHealthCheck", resourceLinks.repository().runHealthCheck(repository.getNamespace(), repository.getName())));
|
||||
}
|
||||
@@ -198,7 +202,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
||||
|
||||
private boolean isRenameNamespacePossible() {
|
||||
for (NamespaceStrategy strategy : strategies) {
|
||||
if (strategy.getClass().getSimpleName().equals(scmConfiguration.getNamespaceStrategy())) {
|
||||
if (strategy.getClass().getSimpleName().equals(scmConfigurationStore.get().getNamespaceStrategy())) {
|
||||
return strategy.canBeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.ObjectFactory;
|
||||
import sonia.scm.admin.ScmConfigurationStore;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.group.GroupPermissions;
|
||||
import sonia.scm.security.PermissionPermissions;
|
||||
@@ -51,7 +52,7 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
|
||||
@Inject
|
||||
private UserManager userManager;
|
||||
@Inject
|
||||
private ScmConfiguration scmConfiguration;
|
||||
private ScmConfigurationStore scmConfigurationStore;
|
||||
|
||||
@Override
|
||||
@Mapping(target = "attributes", ignore = true)
|
||||
@@ -70,7 +71,7 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
|
||||
if (UserPermissions.modify(user).isPermitted()) {
|
||||
linksBuilder.single(link("update", resourceLinks.user().update(user.getName())));
|
||||
linksBuilder.single(link("publicKeys", resourceLinks.user().publicKeys(user.getName())));
|
||||
if (scmConfiguration.isEnabledApiKeys()) {
|
||||
if (scmConfigurationStore.get().isEnabledApiKeys()) {
|
||||
linksBuilder.single(link("apiKeys", resourceLinks.user().apiKeys(user.getName())));
|
||||
}
|
||||
if (user.isExternal()) {
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.admin.ScmConfigurationStore;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.importexport.ExportFileExtensionResolver;
|
||||
@@ -146,6 +147,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
@Mock
|
||||
private RepositoryInitializer repositoryInitializer;
|
||||
@Mock
|
||||
private ScmConfigurationStore configurationStore;
|
||||
@Mock
|
||||
private ScmConfiguration configuration;
|
||||
@Mock
|
||||
private Set<NamespaceStrategy> strategies;
|
||||
@@ -189,6 +192,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() throws IOException {
|
||||
when(configurationStore.get()).thenReturn(configuration);
|
||||
super.repositoryToDtoMapper = repositoryToDtoMapper;
|
||||
super.dtoToRepositoryMapper = dtoToRepositoryMapper;
|
||||
super.manager = repositoryManager;
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.junit.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.admin.ScmConfigurationStore;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.CustomNamespaceStrategy;
|
||||
import sonia.scm.repository.HealthCheckFailure;
|
||||
@@ -87,6 +88,8 @@ public class RepositoryToRepositoryDtoMapperTest {
|
||||
@Mock
|
||||
private ScmPathInfo uriInfo;
|
||||
@Mock
|
||||
private ScmConfigurationStore scmConfigurationStore;
|
||||
@Mock
|
||||
private ScmConfiguration configuration;
|
||||
@Mock
|
||||
private Set<NamespaceStrategy> strategies;
|
||||
@@ -110,6 +113,7 @@ public class RepositoryToRepositoryDtoMapperTest {
|
||||
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
|
||||
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y"));
|
||||
doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator();
|
||||
when(scmConfigurationStore.get()).thenReturn(configuration);
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -321,8 +325,16 @@ public class RepositoryToRepositoryDtoMapperTest {
|
||||
dto.getLinks().getLinkBy("exportInfo").get().getHref());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotCreatePathsLink() {
|
||||
when(configuration.isEnabledFileSearch()).thenReturn(false);
|
||||
RepositoryDto dto = mapper.map(createTestRepository());
|
||||
assertThat(dto.getLinks().getLinkBy("paths")).isNotPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreatePathsLink() {
|
||||
when(configuration.isEnabledFileSearch()).thenReturn(true);
|
||||
RepositoryDto dto = mapper.map(createTestRepository());
|
||||
assertThat(dto.getLinks().getLinkBy("paths"))
|
||||
.isPresent()
|
||||
|
||||
@@ -34,13 +34,16 @@ import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.admin.ScmConfigurationStore;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.security.ApiKeyService;
|
||||
@@ -84,6 +87,7 @@ import static org.mockito.MockitoAnnotations.initMocks;
|
||||
password = "secret",
|
||||
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
||||
)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class UserRootResourceTest {
|
||||
|
||||
@Rule
|
||||
@@ -112,6 +116,8 @@ public class UserRootResourceTest {
|
||||
@Mock
|
||||
private GroupToGroupDtoMapper groupToGroupDtoMapper;
|
||||
@Mock
|
||||
private ScmConfigurationStore scmConfigurationStore;
|
||||
@Mock
|
||||
private ScmConfiguration scmConfiguration;
|
||||
@InjectMocks
|
||||
private UserDtoToUserMapperImpl dtoToUserMapper;
|
||||
@@ -135,6 +141,7 @@ public class UserRootResourceTest {
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
initMocks(this);
|
||||
when(scmConfigurationStore.get()).thenReturn(scmConfiguration);
|
||||
originalUser = createDummyUser("Neo");
|
||||
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
|
||||
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.admin.ScmConfigurationStore;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
@@ -57,6 +58,8 @@ public class UserToUserDtoMapperTest {
|
||||
@Mock
|
||||
private UserManager userManager;
|
||||
@Mock
|
||||
private ScmConfigurationStore scmConfigurationStore;
|
||||
@Mock
|
||||
private ScmConfiguration scmConfiguration;
|
||||
|
||||
@InjectMocks
|
||||
@@ -74,6 +77,8 @@ public class UserToUserDtoMapperTest {
|
||||
expectedBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/");
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
|
||||
when(scmConfigurationStore.get()).thenReturn(scmConfiguration);
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
Reference in New Issue
Block a user