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

View File

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

View File

@@ -43,7 +43,8 @@
"skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen",
"plugin-url": "Plugin URL", "plugin-url": "Plugin URL",
"enabled-xsrf-protection": "XSRF Protection aktivieren", "enabled-xsrf-protection": "XSRF Protection aktivieren",
"namespace-strategy": "Namespace Strategie" "namespace-strategy": "Namespace Strategie",
"login-info-url": "Login Info URL",
}, },
"validation": { "validation": {
"date-format-invalid": "Das Datumsformat ist ungültig", "date-format-invalid": "Das Datumsformat ist ungültig",
@@ -73,6 +74,7 @@
"proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.", "proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.",
"proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.", "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.", "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", "skip-failed-authenticators": "Skip Failed Authenticators",
"plugin-url": "Plugin URL", "plugin-url": "Plugin URL",
"enabled-xsrf-protection": "Enabled XSRF Protection", "enabled-xsrf-protection": "Enabled XSRF Protection",
"namespace-strategy": "Namespace Strategy" "namespace-strategy": "Namespace Strategy",
"login-info-url": "Login Info URL"
}, },
"validation": { "validation": {
"date-format-invalid": "The date format is not valid", "date-format-invalid": "The date format is not valid",
@@ -73,6 +74,7 @@
"proxyUserHelpText": "The username for the proxy server authentication.", "proxyUserHelpText": "The username for the proxy server authentication.",
"proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.", "proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.",
"enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.", "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, loginAttemptLimitTimeout: 0,
enabledXsrfProtection: true, enabledXsrfProtection: true,
namespaceStrategy: "", namespaceStrategy: "",
loginInfoUrl: "",
_links: {} _links: {}
}, },
showNotification: false, showNotification: false,
@@ -119,6 +120,7 @@ class ConfigForm extends React.Component<Props, State> {
{noPermissionNotification} {noPermissionNotification}
<GeneralSettings <GeneralSettings
namespaceStrategies={namespaceStrategies} namespaceStrategies={namespaceStrategies}
loginInfoUrl={config.loginInfoUrl}
realmDescription={config.realmDescription} realmDescription={config.realmDescription}
enableRepositoryArchive={config.enableRepositoryArchive} enableRepositoryArchive={config.enableRepositoryArchive}
disableGroupingGrid={config.disableGroupingGrid} disableGroupingGrid={config.disableGroupingGrid}

View File

@@ -7,6 +7,7 @@ import NamespaceStrategySelect from "./NamespaceStrategySelect";
type Props = { type Props = {
realmDescription: string, realmDescription: string,
loginInfoUrl: string,
enableRepositoryArchive: boolean, enableRepositoryArchive: boolean,
disableGroupingGrid: boolean, disableGroupingGrid: boolean,
dateFormat: string, dateFormat: string,
@@ -27,6 +28,7 @@ class GeneralSettings extends React.Component<Props> {
const { const {
t, t,
realmDescription, realmDescription,
loginInfoUrl,
enabledXsrfProtection, enabledXsrfProtection,
namespaceStrategy, namespaceStrategy,
hasUpdatePermission, hasUpdatePermission,
@@ -57,6 +59,15 @@ class GeneralSettings extends React.Component<Props> {
</div> </div>
</div> </div>
<div className="columns"> <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"> <div className="column is-half">
<Checkbox <Checkbox
checked={enabledXsrfProtection} checked={enabledXsrfProtection}
@@ -71,6 +82,10 @@ class GeneralSettings extends React.Component<Props> {
); );
} }
handleLoginInfoUrlChange = (value: string) => {
this.props.onChange(true, value, "loginInfoUrl");
};
handleRealmDescriptionChange = (value: string) => { handleRealmDescriptionChange = (value: string) => {
this.props.onChange(true, value, "realmDescription"); this.props.onChange(true, value, "realmDescription");
}; };

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ public class ConfigDto extends HalRepresentation {
private long loginAttemptLimitTimeout; private long loginAttemptLimitTimeout;
private boolean enabledXsrfProtection; private boolean enabledXsrfProtection;
private String namespaceStrategy; private String namespaceStrategy;
private String loginInfoUrl;
@Override @Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package @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; package sonia.scm.api.v2.resources;
import com.google.common.base.Strings;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Link; import de.otto.edison.hal.Link;
@@ -7,6 +8,7 @@ import de.otto.edison.hal.Links;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
import sonia.scm.SCMContextProvider; import sonia.scm.SCMContextProvider;
import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupPermissions; import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.PluginPermissions; import sonia.scm.plugin.PluginPermissions;
import sonia.scm.repository.RepositoryRolePermissions; import sonia.scm.repository.RepositoryRolePermissions;
@@ -23,11 +25,13 @@ public class IndexDtoGenerator extends HalAppenderMapper {
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
private final SCMContextProvider scmContextProvider; private final SCMContextProvider scmContextProvider;
private final ScmConfiguration configuration;
@Inject @Inject
public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider) { public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider, ScmConfiguration configuration) {
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
this.scmContextProvider = scmContextProvider; this.scmContextProvider = scmContextProvider;
this.configuration = configuration;
} }
public IndexDto generate() { public IndexDto generate() {
@@ -36,6 +40,11 @@ public class IndexDtoGenerator extends HalAppenderMapper {
builder.self(resourceLinks.index().self()); builder.self(resourceLinks.index().self());
builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().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()) { if (SecurityUtils.getSubject().isAuthenticated()) {
builder.single( builder.single(
link("me", resourceLinks.me().self()), link("me", resourceLinks.me().self()),

View File

@@ -50,6 +50,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
assertEquals(40 , config.getLoginAttemptLimitTimeout()); assertEquals(40 , config.getLoginAttemptLimitTimeout());
assertTrue(config.isEnabledXsrfProtection()); assertTrue(config.isEnabledXsrfProtection());
assertEquals("username", config.getNamespaceStrategy()); assertEquals("username", config.getNamespaceStrategy());
assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl());
} }
private ConfigDto createDefaultDto() { private ConfigDto createDefaultDto() {
@@ -73,6 +74,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
configDto.setLoginAttemptLimitTimeout(40); configDto.setLoginAttemptLimitTimeout(40);
configDto.setEnabledXsrfProtection(true); configDto.setEnabledXsrfProtection(true);
configDto.setNamespaceStrategy("username"); configDto.setNamespaceStrategy("username");
configDto.setLoginInfoUrl("https://scm-manager.org/login-info");
return configDto; 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.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import sonia.scm.SCMContextProvider; import sonia.scm.SCMContextProvider;
import sonia.scm.config.ScmConfiguration;
import java.net.URI; import java.net.URI;
import java.util.Optional; import java.util.Optional;
@@ -19,9 +21,22 @@ public class IndexResourceTest {
@Rule @Rule
public final ShiroRule shiroRule = new ShiroRule(); public final ShiroRule shiroRule = new ShiroRule();
private final SCMContextProvider scmContextProvider = mock(SCMContextProvider.class); private ScmConfiguration configuration;
private final IndexDtoGenerator indexDtoGenerator = new IndexDtoGenerator(ResourceLinksMock.createMock(URI.create("/")), scmContextProvider); private SCMContextProvider scmContextProvider;
private final IndexResource indexResource = new IndexResource(indexDtoGenerator); 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 @Test
public void shouldRenderLoginUrlsForUnauthenticatedRequest() { public void shouldRenderLoginUrlsForUnauthenticatedRequest() {
@@ -30,6 +45,22 @@ public class IndexResourceTest {
Assertions.assertThat(index.getLinks().getLinkBy("login")).matches(Optional::isPresent); 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 @Test
public void shouldRenderSelfLinkForUnauthenticatedRequest() { public void shouldRenderSelfLinkForUnauthenticatedRequest() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex();

View File

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