added configuration option for login info url

This commit is contained in:
Sebastian Sdorra
2019-08-13 09:45:37 +02:00
parent 2120a4ee02
commit 3823c033b9
14 changed files with 111 additions and 14 deletions

View File

@@ -75,6 +75,11 @@ public class ScmConfiguration implements Configuration {
public static final String DEFAULT_PLUGINURL =
"http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false";
/**
* Default url for login information (plugin and feature tips on the login page).
*/
public static final String DEFAULT_LOGIN_INFO_URL = "https://login-info.scm-manager.org/api/v1/login-info";
/**
* Default plugin url from version 1.0
*/
@@ -177,6 +182,9 @@ public class ScmConfiguration implements Configuration {
@XmlElement(name = "namespace-strategy")
private String namespaceStrategy = "UsernameNamespaceStrategy";
@XmlElement(name = "login-info-url")
private String loginInfoUrl = DEFAULT_LOGIN_INFO_URL;
/**
* Calls the {@link sonia.scm.ConfigChangedListener#configChanged(Object)}
@@ -216,6 +224,7 @@ public class ScmConfiguration implements Configuration {
this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout;
this.enabledXsrfProtection = other.enabledXsrfProtection;
this.namespaceStrategy = other.namespaceStrategy;
this.loginInfoUrl = other.loginInfoUrl;
}
/**
@@ -350,6 +359,9 @@ public class ScmConfiguration implements Configuration {
return namespaceStrategy;
}
public String getLoginInfoUrl() {
return loginInfoUrl;
}
/**
* Returns true if failed authenticators are skipped.
@@ -477,6 +489,10 @@ public class ScmConfiguration implements Configuration {
this.namespaceStrategy = namespaceStrategy;
}
public void setLoginInfoUrl(String loginInfoUrl) {
this.loginInfoUrl = loginInfoUrl;
}
@Override
// Only for permission checks, don't serialize to XML
@XmlTransient

View File

@@ -21,5 +21,6 @@ export type Config = {
loginAttemptLimitTimeout: number,
enabledXsrfProtection: boolean,
namespaceStrategy: string,
loginInfoUrl: string,
_links: Links
};

View File

@@ -43,7 +43,8 @@
"skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen",
"plugin-url": "Plugin URL",
"enabled-xsrf-protection": "XSRF Protection aktivieren",
"namespace-strategy": "Namespace Strategie"
"namespace-strategy": "Namespace Strategie",
"login-info-url": "Login Info URL",
},
"validation": {
"date-format-invalid": "Das Datumsformat ist ungültig",
@@ -73,6 +74,7 @@
"proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.",
"proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.",
"enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.",
"nameSpaceStrategyHelpText": "Strategie für Namespaces."
"nameSpaceStrategyHelpText": "Strategie für Namespaces.",
"loginInfoUrlHelpText": "URL zu den Login Informationen (Plugin und Feature Tipps auf der Login Seite)."
}
}

View File

@@ -43,7 +43,8 @@
"skip-failed-authenticators": "Skip Failed Authenticators",
"plugin-url": "Plugin URL",
"enabled-xsrf-protection": "Enabled XSRF Protection",
"namespace-strategy": "Namespace Strategy"
"namespace-strategy": "Namespace Strategy",
"login-info-url": "Login Info URL"
},
"validation": {
"date-format-invalid": "The date format is not valid",
@@ -73,6 +74,7 @@
"proxyUserHelpText": "The username for the proxy server authentication.",
"proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.",
"enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.",
"nameSpaceStrategyHelpText": "The namespace strategy."
"nameSpaceStrategyHelpText": "The namespace strategy.",
"loginInfoUrlHelpText": "URL to login information (plugin and feature tips at login page)."
}
}

View File

@@ -54,6 +54,7 @@ class ConfigForm extends React.Component<Props, State> {
loginAttemptLimitTimeout: 0,
enabledXsrfProtection: true,
namespaceStrategy: "",
loginInfoUrl: "",
_links: {}
},
showNotification: false,
@@ -119,6 +120,7 @@ class ConfigForm extends React.Component<Props, State> {
{noPermissionNotification}
<GeneralSettings
namespaceStrategies={namespaceStrategies}
loginInfoUrl={config.loginInfoUrl}
realmDescription={config.realmDescription}
enableRepositoryArchive={config.enableRepositoryArchive}
disableGroupingGrid={config.disableGroupingGrid}

View File

@@ -7,6 +7,7 @@ import NamespaceStrategySelect from "./NamespaceStrategySelect";
type Props = {
realmDescription: string,
loginInfoUrl: string,
enableRepositoryArchive: boolean,
disableGroupingGrid: boolean,
dateFormat: string,
@@ -27,6 +28,7 @@ class GeneralSettings extends React.Component<Props> {
const {
t,
realmDescription,
loginInfoUrl,
enabledXsrfProtection,
namespaceStrategy,
hasUpdatePermission,
@@ -57,6 +59,15 @@ class GeneralSettings extends React.Component<Props> {
</div>
</div>
<div className="columns">
<div className="column is-half">
<InputField
label={t("general-settings.login-info-url")}
onChange={this.handleLoginInfoUrlChange}
value={loginInfoUrl}
disabled={!hasUpdatePermission}
helpText={t("help.loginInfoUrlHelpText")}
/>
</div>
<div className="column is-half">
<Checkbox
checked={enabledXsrfProtection}
@@ -71,6 +82,10 @@ class GeneralSettings extends React.Component<Props> {
);
}
handleLoginInfoUrlChange = (value: string) => {
this.props.onChange(true, value, "loginInfoUrl");
};
handleRealmDescriptionChange = (value: string) => {
this.props.onChange(true, value, "realmDescription");
};

View File

@@ -4,6 +4,7 @@ import InfoBox from "./InfoBox";
import type { InfoItem } from "./InfoItem";
type Props = {
loginInfoLink: string
};
type State = {
@@ -21,7 +22,8 @@ class LoginInfo extends React.Component<Props, State> {
}
componentDidMount() {
fetch("https://login-info.scm-manager.org/api/v1/login-info")
const { loginInfoLink } = this.props;
fetch(loginInfoLink)
.then(response => response.json())
.then(info => {
this.setState({

View File

@@ -8,7 +8,7 @@ import {
getLoginFailure
} from "../modules/auth";
import { connect } from "react-redux";
import { getLoginLink } from "../modules/indexResource";
import { getLoginLink, getLoginInfoLink } from "../modules/indexResource";
import LoginForm from "../components/LoginForm";
import LoginInfo from "../components/LoginInfo";
import classNames from "classnames";
@@ -25,6 +25,7 @@ type Props = {
loading: boolean,
error?: Error,
link: string,
loginInfoLink?: string,
// dispatcher props
login: (link: string, username: string, password: string) => void,
@@ -49,19 +50,24 @@ class Login extends React.Component<Props> {
};
render() {
const { authenticated, loading, error, classes } = this.props;
const { authenticated, loginInfoLink, loading, error, classes } = this.props;
if (authenticated) {
return this.renderRedirect();
}
let loginInfo;
if (loginInfoLink) {
loginInfo = <LoginInfo loginInfoLink={loginInfoLink}/>
}
return (
<section className={classNames("hero", classes.section )}>
<div className="hero-body">
<div className="container">
<div className="columns">
<div className="columns is-centered">
<LoginForm loading={loading} error={error} login={this.login}/>
<LoginInfo/>
{loginInfo}
</div>
</div>
</div>
@@ -75,11 +81,13 @@ const mapStateToProps = state => {
const loading = isLoginPending(state);
const error = getLoginFailure(state);
const link = getLoginLink(state);
const loginInfoLink = getLoginInfoLink(state);
return {
authenticated,
loading,
error,
link
link,
loginInfoLink
};
};

View File

@@ -168,6 +168,10 @@ export function getSvnConfigLink(state: Object) {
return getLink(state, "svnConfig");
}
export function getLoginInfoLink(state: Object) {
return getLink(state, "loginInfo");
}
export function getUserAutoCompleteLink(state: Object): string {
const link = getLinkCollection(state, "autocomplete").find(
i => i.name === "users"

View File

@@ -32,6 +32,7 @@ public class ConfigDto extends HalRepresentation {
private long loginAttemptLimitTimeout;
private boolean enabledXsrfProtection;
private String namespaceStrategy;
private String loginInfoUrl;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package

View File

@@ -1,5 +1,6 @@
package sonia.scm.api.v2.resources;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Link;
@@ -7,6 +8,7 @@ import de.otto.edison.hal.Links;
import org.apache.shiro.SecurityUtils;
import sonia.scm.SCMContextProvider;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.PluginPermissions;
import sonia.scm.repository.RepositoryRolePermissions;
@@ -23,11 +25,13 @@ public class IndexDtoGenerator extends HalAppenderMapper {
private final ResourceLinks resourceLinks;
private final SCMContextProvider scmContextProvider;
private final ScmConfiguration configuration;
@Inject
public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider) {
public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider, ScmConfiguration configuration) {
this.resourceLinks = resourceLinks;
this.scmContextProvider = scmContextProvider;
this.configuration = configuration;
}
public IndexDto generate() {
@@ -36,6 +40,11 @@ public class IndexDtoGenerator extends HalAppenderMapper {
builder.self(resourceLinks.index().self());
builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self()));
String loginInfoUrl = configuration.getLoginInfoUrl();
if (!Strings.isNullOrEmpty(loginInfoUrl)) {
builder.single(link("loginInfo", loginInfoUrl));
}
if (SecurityUtils.getSubject().isAuthenticated()) {
builder.single(
link("me", resourceLinks.me().self()),

View File

@@ -50,6 +50,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
assertEquals(40 , config.getLoginAttemptLimitTimeout());
assertTrue(config.isEnabledXsrfProtection());
assertEquals("username", config.getNamespaceStrategy());
assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl());
}
private ConfigDto createDefaultDto() {
@@ -73,6 +74,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
configDto.setLoginAttemptLimitTimeout(40);
configDto.setEnabledXsrfProtection(true);
configDto.setNamespaceStrategy("username");
configDto.setLoginInfoUrl("https://scm-manager.org/login-info");
return configDto;
}

View File

@@ -3,9 +3,11 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import sonia.scm.SCMContextProvider;
import sonia.scm.config.ScmConfiguration;
import java.net.URI;
import java.util.Optional;
@@ -19,9 +21,22 @@ public class IndexResourceTest {
@Rule
public final ShiroRule shiroRule = new ShiroRule();
private final SCMContextProvider scmContextProvider = mock(SCMContextProvider.class);
private final IndexDtoGenerator indexDtoGenerator = new IndexDtoGenerator(ResourceLinksMock.createMock(URI.create("/")), scmContextProvider);
private final IndexResource indexResource = new IndexResource(indexDtoGenerator);
private ScmConfiguration configuration;
private SCMContextProvider scmContextProvider;
private IndexResource indexResource;
@Before
public void setUpObjectUnderTest() {
this.configuration = new ScmConfiguration();
this.scmContextProvider = mock(SCMContextProvider.class);
IndexDtoGenerator generator = new IndexDtoGenerator(
ResourceLinksMock.createMock(URI.create("/")),
scmContextProvider,
configuration
);
this.indexResource = new IndexResource(generator);
}
@Test
public void shouldRenderLoginUrlsForUnauthenticatedRequest() {
@@ -30,6 +45,22 @@ public class IndexResourceTest {
Assertions.assertThat(index.getLinks().getLinkBy("login")).matches(Optional::isPresent);
}
@Test
public void shouldRenderLoginInfoUrl() {
IndexDto index = indexResource.getIndex();
Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isPresent();
}
@Test
public void shouldNotRenderLoginInfoUrlWhenNoUrlIsConfigured() {
configuration.setLoginInfoUrl("");
IndexDto index = indexResource.getIndex();
Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isNotPresent();
}
@Test
public void shouldRenderSelfLinkForUnauthenticatedRequest() {
IndexDto index = indexResource.getIndex();

View File

@@ -80,6 +80,7 @@ public class ScmConfigurationToConfigDtoMapperTest {
assertEquals(2 , dto.getLoginAttemptLimitTimeout());
assertTrue(dto.isEnabledXsrfProtection());
assertEquals("username", dto.getNamespaceStrategy());
assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl());
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref());
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref());
@@ -118,6 +119,7 @@ public class ScmConfigurationToConfigDtoMapperTest {
config.setLoginAttemptLimitTimeout(2);
config.setEnabledXsrfProtection(true);
config.setNamespaceStrategy("username");
config.setLoginInfoUrl("https://scm-manager.org/login-info");
return config;
}