Add plugin wizard initialization step (#2045)

Adds a new initialization step after setting up the initial administration account that allows administrators to initialize the instance with a selection of plugin sets.

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
Co-authored-by: Matthias Thieroff <matthias.thieroff@cloudogu.com>
This commit is contained in:
Konstantin Schaper
2022-05-31 15:14:52 +02:00
committed by Eduard Heimbuch
parent 6216945f0d
commit 1b18191c57
63 changed files with 2294 additions and 135 deletions

View File

@@ -184,7 +184,7 @@ The following shows user as an example.
"configuration:read", "configuration:read",
"configuration:write", "configuration:write",
"plugin:read", "plugin:read",
"plugin:manage", "plugin:write",
"group:read", "group:read",
"user:read", "user:read",
"repository:read" "repository:read"
@@ -206,7 +206,7 @@ The following shows user as an example.
"configuration:read", "configuration:read",
"configuration:write", "configuration:write",
"plugin:read", "plugin:read",
"plugin:manage", "plugin:write",
"group:read", "group:read",
"user:read", "user:read",
"repository:read" "repository:read"

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -34,3 +34,14 @@ For automated processes, you might want to bypass the initial user creation. To
in a system property `scm.initialPassword`. If this is present, a user `scmadmin` with this password will be created, in a system property `scm.initialPassword`. If this is present, a user `scmadmin` with this password will be created,
if it does not already exist. To change the name of this user, you can set this with the property `scm.initialUser` if it does not already exist. To change the name of this user, you can set this with the property `scm.initialUser`
in addition. in addition.
When set, this also causes the initialization to skip the Plugin Wizard.
# Plugin Wizard
Once an initial user is created, the Plugin Wizard is going to appear.
Here you can select a series of pre-defined sets of plugins to kickstart
your development experience with the SCM-Manager. To install the selected
plugins, the server has to restart once.
![Form to select plugin sets](assets/plugin-wizard.png)

View File

@@ -0,0 +1,2 @@
- type: added
description: Initialization step to install pre-defined plugin sets ([#2045](https://github.com/scm-manager/scm-manager/pull/2045))

View File

@@ -26,7 +26,11 @@ package sonia.scm.initialization;
import sonia.scm.plugin.ExtensionPoint; import sonia.scm.plugin.ExtensionPoint;
/**
* @deprecated Limited use for Plugin Development, see as internal
*/
@ExtensionPoint @ExtensionPoint
@Deprecated(since = "2.35.0", forRemoval = true)
public interface InitializationStep { public interface InitializationStep {
String name(); String name();

View File

@@ -28,9 +28,22 @@ import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import sonia.scm.plugin.ExtensionPoint; import sonia.scm.plugin.ExtensionPoint;
import java.util.Locale;
/**
* @deprecated Limited use for Plugin Development, see as internal
*/
@ExtensionPoint @ExtensionPoint
@Deprecated(since = "2.35.0", forRemoval = true)
public interface InitializationStepResource { public interface InitializationStepResource {
String name(); String name();
/**
* @since 2.35.0
*/
default void setupIndex(Links.Builder builder, Embedded.Builder embeddedBuilder, Locale locale) {
setupIndex(builder, embeddedBuilder);
}
void setupIndex(Links.Builder builder, Embedded.Builder embeddedBuilder); void setupIndex(Links.Builder builder, Embedded.Builder embeddedBuilder);
} }

View File

@@ -26,6 +26,7 @@ package sonia.scm.plugin;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
/** /**
* The plugin manager is responsible for plugin related tasks, such as install, uninstall or updating. * The plugin manager is responsible for plugin related tasks, such as install, uninstall or updating.
@@ -64,6 +65,23 @@ public interface PluginManager {
*/ */
List<AvailablePlugin> getAvailable(); List<AvailablePlugin> getAvailable();
/**
* Returns all available plugin sets from the plugin center.
*
* @return a list of available plugin sets
* @since 2.35.0
*/
Set<PluginSet> getPluginSets();
/**
* Collects and installs all plugins and their dependencies for the given plugin sets.
*
* @param pluginSets Ids of plugin sets to install
* @param restartAfterInstallation restart context after all plugins have been installed
* @since 2.35.0
*/
void installPluginSets(Set<String> pluginSets, boolean restartAfterInstallation);
/** /**
* Returns all updatable plugins. * Returns all updatable plugins.
* *

View File

@@ -0,0 +1,55 @@
/*
* 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.
*/
package sonia.scm.plugin;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
import java.util.Map;
import java.util.Set;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class PluginSet {
private String id;
private int sequence;
private Set<String> plugins;
private Map<String, Description> descriptions;
private Map<String, String> images;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public static class Description {
private String name;
private List<String> features;
}
}

View File

@@ -43,6 +43,7 @@ public interface AccessTokenCookieIssuer {
* @param accessToken access token * @param accessToken access token
*/ */
void authenticate(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken); void authenticate(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken);
/** /**
* Invalidates the authentication cookie. * Invalidates the authentication cookie.
* *

View File

@@ -83,7 +83,7 @@ export const extractXsrfTokenFromCookie = (cookieString?: string) => {
const cookies = cookieString.split(";"); const cookies = cookieString.split(";");
for (const c of cookies) { for (const c of cookies) {
const parts = c.trim().split("="); const parts = c.trim().split("=");
if (parts[0] === "X-Bearer-Token") { if (parts[0] === "X-Bearer-Token" || parts[0] === "X-SCM-Init-Token") {
return extractXsrfTokenFromJwt(parts[1]); return extractXsrfTokenFromJwt(parts[1]);
} }
} }

View File

@@ -64,6 +64,7 @@ export * from "./loginInfo";
export * from "./usePluginCenterAuthInfo"; export * from "./usePluginCenterAuthInfo";
export * from "./compare"; export * from "./compare";
export * from "./utils"; export * from "./utils";
export * from "./links";
export { default as ApiProvider } from "./ApiProvider"; export { default as ApiProvider } from "./ApiProvider";
export * from "./ApiProvider"; export * from "./ApiProvider";

View File

@@ -34,7 +34,7 @@ type WaitForRestartOptions = {
timeout?: number; timeout?: number;
}; };
const waitForRestartAfter = ( export const waitForRestartAfter = (
promise: Promise<any>, promise: Promise<any>,
{ initialDelay = 1000, timeout = 500 }: WaitForRestartOptions = {} { initialDelay = 1000, timeout = 500 }: WaitForRestartOptions = {}
): Promise<void> => { ): Promise<void> => {

View File

@@ -21,17 +21,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
import React, { FC, ReactNode } from "react"; import React, { FC } from "react";
import Logo from "./../Logo"; import Logo from "./../Logo";
import { Links } from "@scm-manager/ui-types";
type Props = { type Props = {
links: Links; authenticated?: boolean;
authenticated: boolean;
children: ReactNode;
}; };
const SmallHeader: FC<{ children: ReactNode }> = ({ children }) => { const SmallHeader: FC = ({ children }) => {
return <div className="has-scm-background">{children}</div>; return <div className="has-scm-background">{children}</div>;
}; };
@@ -51,7 +48,7 @@ const LargeHeader: FC = () => {
); );
}; };
const Header: FC<Props> = ({ authenticated, children, links }) => { const Header: FC<Props> = ({ authenticated, children }) => {
if (authenticated) { if (authenticated) {
return <SmallHeader>{children}</SmallHeader>; return <SmallHeader>{children}</SmallHeader>;
} else { } else {

View File

@@ -26,6 +26,15 @@ import { HalRepresentation, HalRepresentationWithEmbedded } from "./hal";
type PluginType = "SCM" | "CLOUDOGU"; type PluginType = "SCM" | "CLOUDOGU";
export type PluginSet = HalRepresentation & {
id: string;
name: string;
sequence: number;
features: string[];
plugins: Plugin[];
images: Record<string, string>;
};
export type Plugin = HalRepresentation & { export type Plugin = HalRepresentation & {
name: string; name: string;
version: string; version: string;

View File

@@ -11,6 +11,20 @@
"password-confirmation": "Passwort Bestätigung", "password-confirmation": "Passwort Bestätigung",
"submit": "Absenden" "submit": "Absenden"
}, },
"pluginWizardStep": {
"title": "Plugin Sets",
"description": "Der SCM-Manager ist ein minimalistisches Werkzeug, um Git, SVN und mercurial Repositories zu verwalten. Mit Plugins vereinfachen Sie Ihren Workflow und erreichen mehr mit Ihrem SCM-Manager. Wählen Sie ein oder mehrere Plugin-Sets, um ihren Start zu beschleunigen.",
"submit": "Absenden",
"submitAndRestart": "Absenden und Neustarten",
"skip": {
"title": "Ohne weitere Plugins starten",
"subtitle": "Ausschließlich Core-Plugins werden installiert. Es können weiterhin jederzeit Plugins manuell über das Plugin-Center installiert werden."
},
"pluginSet": {
"expand": "Enthaltene Plugins anzeigen",
"collapse": "Enthaltene Plugins"
}
},
"error": { "error": {
"forbidden": "Falscher Token" "forbidden": "Falscher Token"
} }

View File

@@ -11,6 +11,21 @@
"password-confirmation": "Confirm Password", "password-confirmation": "Confirm Password",
"submit": "Submit" "submit": "Submit"
}, },
"pluginWizardStep": {
"title": "Plugin Sets",
"description": "Out of the box SCM-Manager is a streamlined tool to manage Git, SVN and mercurial repositories. Plugins enhance your workflow and help you to get more out of your SCM-Manager. Select one or more of our plugin-sets to jump-start your journey!",
"submit": "Submit",
"submitAndRestart": "Submit and restart",
"skip": {
"title": "Start without additional plugins",
"subtitle": "Only core-plugins will be installed. You may manually install plugins through the Plugin-Center any time."
},
"pluginSet": {
"expand": "View included Plugins",
"collapse": "Included Plugins"
}
},
"error": { "error": {
"forbidden": "Incorrect token" "forbidden": "Incorrect token"
} }

View File

@@ -67,7 +67,7 @@ const App: FC = () => {
return ( return (
<AppWrapper className="App"> <AppWrapper className="App">
{isAuthenticated ? <Feedback index={index} /> : null} {isAuthenticated ? <Feedback index={index} /> : null}
<Header authenticated={authenticatedOrAnonymous} links={index._links}> <Header authenticated={authenticatedOrAnonymous}>
<NavigationBar links={index._links} /> <NavigationBar links={index._links} />
</Header> </Header>
<div className="is-flex-grow-1">{content}</div> <div className="is-flex-grow-1">{content}</div>

View File

@@ -32,6 +32,7 @@ import { Link } from "@scm-manager/ui-types";
import i18next from "i18next"; import i18next from "i18next";
import { binder, extensionPoints } from "@scm-manager/ui-extensions"; import { binder, extensionPoints } from "@scm-manager/ui-extensions";
import InitializationAdminAccountStep from "./InitializationAdminAccountStep"; import InitializationAdminAccountStep from "./InitializationAdminAccountStep";
import InitializationPluginWizardStep from "./InitializationPluginWizardStep";
const Index: FC = () => { const Index: FC = () => {
const { isLoading, error, data } = useIndex(); const { isLoading, error, data } = useIndex();
@@ -39,7 +40,7 @@ const Index: FC = () => {
// TODO check componentDidUpdate method for anonymous user stuff // TODO check componentDidUpdate method for anonymous user stuff
i18next.on("languageChanged", lng => { i18next.on("languageChanged", (lng) => {
document.documentElement.setAttribute("lang", lng); document.documentElement.setAttribute("lang", lng);
}); });
@@ -73,3 +74,8 @@ binder.bind<extensionPoints.InitializationStep<"adminAccount">>(
"initialization.step.adminAccount", "initialization.step.adminAccount",
InitializationAdminAccountStep InitializationAdminAccountStep
); );
binder.bind<extensionPoints.InitializationStep<"pluginWizard">>(
"initialization.step.pluginWizard",
InitializationPluginWizardStep
);

View File

@@ -0,0 +1,228 @@
/*
* 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, useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { HalRepresentationWithEmbedded, PluginSet } from "@scm-manager/ui-types";
import { apiClient, requiredLink, waitForRestartAfter } from "@scm-manager/ui-api";
import { useMutation } from "react-query";
import { useForm } from "react-hook-form";
import { Checkbox, ErrorNotification, Icon, SubmitButton } from "@scm-manager/ui-components";
import styled from "styled-components";
const HighlightedImg = styled.img`
&:hover {
filter: drop-shadow(0px 2px 2px #00c79b);
}
`;
const HiddenInput = styled.input`
display: none;
`;
const HeroSection = styled.section`
padding-top: 2em;
`;
const BorderedDiv = styled.div`
border-radius: 4px;
border-width: 1px;
border-style: solid;
`;
type PluginSetsInstallation = {
pluginSetIds: string[];
};
const install = (link: string) => (data: PluginSetsInstallation) =>
waitForRestartAfter(apiClient.post(link, data, "application/json"));
const useInstallPluginSets = (link: string) => {
const { mutate, isLoading, error, isSuccess } = useMutation<unknown, Error, PluginSetsInstallation>(install(link));
return {
installPluginSets: mutate,
isLoading,
error,
isInstalled: isSuccess,
};
};
type PluginSetCardProps = {
pluginSet: PluginSet;
};
const PluginSetCard: FC<PluginSetCardProps> = ({ pluginSet, children }) => {
const [t] = useTranslation("initialization");
const [collapsed, setCollapsed] = useState(true);
return (
<div className="card block">
<div className="card-image pt-5">{children}</div>
<div className="card-content">
<h5 className="subtitle is-5">{pluginSet.name}</h5>
<div className="content">
<ul>
{pluginSet.features.map((feature) => (
<li>{feature}</li>
))}
</ul>
</div>
<div className="block pl-4">
<div
className="is-flex is-justify-content-space-between has-cursor-pointer"
onClick={() => setCollapsed((value) => !value)}
>
<span>{t(`pluginWizardStep.pluginSet.${collapsed ? "expand" : "collapse"}`)}</span>
<Icon color="inherit" name={collapsed ? "angle-down" : "angle-up"} />
</div>
{!collapsed ? (
<ul className="pt-2">
{pluginSet.plugins.map((plugin, idx) => (
<li key={idx} className="py-2">
<div className="is-size-6 has-text-weight-semibold">{plugin.displayName}</div>
<div className="is-size-6">
<a href={`https://scm-manager.org/plugins/${plugin.name}`} target="_blank">
{plugin.description}
</a>
</div>
</li>
))}
</ul>
) : null}
</div>
</div>
</div>
);
};
type Props = {
data: HalRepresentationWithEmbedded<{ pluginSets: PluginSet[] }>;
};
type FormValue = { [id: string]: boolean };
const InitializationPluginWizardStep: FC<Props> = ({ data: initializationContext }) => {
const {
installPluginSets,
isLoading: isInstalling,
isInstalled,
error: installationError,
} = useInstallPluginSets(requiredLink(initializationContext, "installPluginSets"));
const [skipInstallation, setSkipInstallation] = useState(false);
const { register, handleSubmit, watch } = useForm<FormValue>();
const data = initializationContext._embedded?.pluginSets;
const [t] = useTranslation("initialization");
const values = watch();
const pluginSetIds = useMemo(
() =>
Object.entries(values).reduce<string[]>((p, [id, flag]) => {
if (flag) {
p.push(id);
}
return p;
}, []),
[values]
);
const submit = useCallback(
() =>
installPluginSets({
pluginSetIds,
}),
[installPluginSets, pluginSetIds]
);
const isSelected = useCallback((pluginSetId: string) => pluginSetIds.includes(pluginSetId), [pluginSetIds]);
const hasPluginSets = useMemo(() => pluginSetIds.length > 0, [pluginSetIds]);
useEffect(() => {
if (hasPluginSets) {
setSkipInstallation(false);
}
}, [hasPluginSets]);
useEffect(() => {
if (isInstalled) {
window.location.reload();
}
}, [isInstalled]);
let content;
if (installationError) {
content = <ErrorNotification error={installationError} />;
} else {
content = (
<form onSubmit={handleSubmit(submit)} className="is-flex is-flex-direction-column">
<div className="block">
{data?.map((pluginSet, idx) => (
<PluginSetCard pluginSet={pluginSet} key={idx}>
<label htmlFor={`plugin-set-${idx}`} className="has-cursor-pointer">
<HighlightedImg
alt={pluginSet.name}
src={`data:image/svg+xml;base64,${pluginSet.images[isSelected(pluginSet.id) ? "check" : "standard"]}`}
/>
</label>
<HiddenInput type="checkbox" id={`plugin-set-${idx}`} {...register(pluginSet.id)} />
</PluginSetCard>
))}
<BorderedDiv className="card-block has-border-danger px-4 pt-3">
<Checkbox
disabled={hasPluginSets}
checked={skipInstallation}
onChange={setSkipInstallation}
title={t("pluginWizardStep.skip.title")}
label={t("pluginWizardStep.skip.subtitle")}
></Checkbox>
</BorderedDiv>
</div>
<SubmitButton
scrollToTop={false}
disabled={isInstalling || !(hasPluginSets || skipInstallation)}
loading={isInstalling}
className="is-align-self-flex-end"
>
{t(`pluginWizardStep.${hasPluginSets ? "submitAndRestart" : "submit"}`)}
</SubmitButton>
</form>
);
}
return (
<HeroSection className="hero">
<div className="hero-body">
<div className="container">
<div className="columns is-centered">
<div className="column is-8 box has-background-secondary-less">
<h3 className="title">{t("title")}</h3>
<h4 className="subtitle">{t("pluginWizardStep.title")}</h4>
<p className="is-size-6 block">{t("pluginWizardStep.description")}</p>
{content}
</div>
</div>
</div>
</div>
</HeroSection>
);
};
export default InitializationPluginWizardStep;

View File

@@ -27,14 +27,20 @@ package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import lombok.Data; import lombok.Data;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.UnauthenticatedException; import org.apache.shiro.authz.UnauthenticatedException;
import sonia.scm.initialization.InitializationAuthenticationService;
import sonia.scm.initialization.InitializationStepResource; import sonia.scm.initialization.InitializationStepResource;
import sonia.scm.lifecycle.AdminAccountStartupAction; import sonia.scm.lifecycle.AdminAccountStartupAction;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.security.AllowAnonymousAccess; import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.security.Tokens;
import sonia.scm.util.ValidationUtil; import sonia.scm.util.ValidationUtil;
import javax.inject.Inject; import javax.inject.Inject;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.Email; import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
@@ -42,9 +48,12 @@ import javax.validation.constraints.Pattern;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Link.link;
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow; import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
import static sonia.scm.initialization.InitializationWebTokenGenerator.INIT_TOKEN_HEADER;
@AllowAnonymousAccess @AllowAnonymousAccess
@Extension @Extension
@@ -52,20 +61,34 @@ public class AdminAccountStartupResource implements InitializationStepResource {
private final AdminAccountStartupAction adminAccountStartupAction; private final AdminAccountStartupAction adminAccountStartupAction;
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
private final InitializationAuthenticationService authenticationService;
@Inject @Inject
public AdminAccountStartupResource(AdminAccountStartupAction adminAccountStartupAction, ResourceLinks resourceLinks) { public AdminAccountStartupResource(AdminAccountStartupAction adminAccountStartupAction, ResourceLinks resourceLinks, InitializationAuthenticationService authenticationService) {
this.adminAccountStartupAction = adminAccountStartupAction; this.adminAccountStartupAction = adminAccountStartupAction;
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
this.authenticationService = authenticationService;
} }
@POST @POST
@Path("") @Path("")
@Consumes("application/json") @Consumes("application/json")
public void postAdminInitializationData(@Valid AdminInitializationData data) { public Response postAdminInitializationData(
@Context HttpServletRequest request,
@Context HttpServletResponse response,
@Valid AdminInitializationData data
) {
verifyInInitialization(); verifyInInitialization();
verifyToken(data); verifyToken(data);
createAdminUser(data); createAdminUser(data);
// Invalidate old access token cookies to prevent conflicts during authentication
authenticationService.invalidateCookies(request, response);
SecurityUtils.getSubject().login(Tokens.createAuthenticationToken(request, data.userName, data.password));
// Create cookie which will be used for authentication during the initialization process
authenticationService.authenticate(request, response);
return Response.noContent().build();
} }
private void verifyInInitialization() { private void verifyInInitialization() {

View File

@@ -168,7 +168,7 @@ public class AvailablePluginResource {
) )
@ApiResponse(responseCode = "200", description = "success") @ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse( @ApiResponse(
responseCode = "500", responseCode = "500",
description = "internal server error", description = "internal server error",

View File

@@ -47,6 +47,7 @@ import sonia.scm.web.EdisonHalAppender;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static de.otto.edison.hal.Embedded.embeddedBuilder; import static de.otto.edison.hal.Embedded.embeddedBuilder;
@@ -75,6 +76,10 @@ public class IndexDtoGenerator extends HalAppenderMapper {
} }
public IndexDto generate() { public IndexDto generate() {
return generate(Locale.getDefault());
}
public IndexDto generate(Locale locale) {
Links.Builder builder = Links.linkingTo(); Links.Builder builder = Links.linkingTo();
Embedded.Builder embeddedBuilder = embeddedBuilder(); Embedded.Builder embeddedBuilder = embeddedBuilder();
@@ -84,7 +89,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
if (initializationFinisher.isFullyInitialized()) { if (initializationFinisher.isFullyInitialized()) {
return handleNormalIndex(builder, embeddedBuilder); return handleNormalIndex(builder, embeddedBuilder);
} else { } else {
return handleInitialization(builder, embeddedBuilder); return handleInitialization(builder, embeddedBuilder, locale);
} }
} }
@@ -170,11 +175,11 @@ public class IndexDtoGenerator extends HalAppenderMapper {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private IndexDto handleInitialization(Links.Builder builder, Embedded.Builder embeddedBuilder) { private IndexDto handleInitialization(Links.Builder builder, Embedded.Builder embeddedBuilder, Locale locale) {
Links.Builder initializationLinkBuilder = Links.linkingTo(); Links.Builder initializationLinkBuilder = Links.linkingTo();
Embedded.Builder initializationEmbeddedBuilder = embeddedBuilder(); Embedded.Builder initializationEmbeddedBuilder = embeddedBuilder();
InitializationStep initializationStep = initializationFinisher.missingInitialization(); InitializationStep initializationStep = initializationFinisher.missingInitialization();
initializationFinisher.getResource(initializationStep.name()).setupIndex(initializationLinkBuilder, initializationEmbeddedBuilder); initializationFinisher.getResource(initializationStep.name()).setupIndex(initializationLinkBuilder, initializationEmbeddedBuilder, locale);
embeddedBuilder.with(initializationStep.name(), new InitializationDto(initializationLinkBuilder.build(), initializationEmbeddedBuilder.build())); embeddedBuilder.with(initializationStep.name(), new InitializationDto(initializationLinkBuilder.build(), initializationEmbeddedBuilder.build()));
return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion(), scmContextProvider.getInstanceId(), initializationStep.name()); return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion(), scmContextProvider.getInstanceId(), initializationStep.name());
} }

View File

@@ -35,9 +35,11 @@ import sonia.scm.security.AllowAnonymousAccess;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
@OpenAPIDefinition( @OpenAPIDefinition(
security = { security = {
@@ -80,7 +82,7 @@ public class IndexResource {
schema = @Schema(implementation = ErrorDto.class) schema = @Schema(implementation = ErrorDto.class)
) )
) )
public IndexDto getIndex() { public IndexDto getIndex(@Context HttpServletRequest request) {
return indexDtoGenerator.generate(); return indexDtoGenerator.generate(request.getLocale());
} }
} }

View File

@@ -112,7 +112,7 @@ public class InstalledPluginResource {
) )
@ApiResponse(responseCode = "200", description = "success") @ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse( @ApiResponse(
responseCode = "500", responseCode = "500",
description = "internal server error", description = "internal server error",
@@ -191,7 +191,7 @@ public class InstalledPluginResource {
) )
@ApiResponse(responseCode = "200", description = "success") @ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse( @ApiResponse(
responseCode = "500", responseCode = "500",
description = "internal server error", description = "internal server error",

View File

@@ -160,7 +160,7 @@ public class PendingPluginResource {
) )
@ApiResponse(responseCode = "200", description = "success") @ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse( @ApiResponse(
responseCode = "500", responseCode = "500",
description = "internal server error", description = "internal server error",
@@ -183,7 +183,7 @@ public class PendingPluginResource {
) )
@ApiResponse(responseCode = "200", description = "success") @ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:manage\" privilege") @ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse( @ApiResponse(
responseCode = "500", responseCode = "500",
description = "internal server error", description = "internal server error",

View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Set;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("squid:S2160") // we do not need equals for dto
public class PluginSetCollectionDto extends HalRepresentation {
Set<PluginSetDto> pluginSets;
}

View File

@@ -0,0 +1,49 @@
/*
* 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.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
import java.util.Map;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("squid:S2160") // we do not need equals for dto
public class PluginSetDto extends HalRepresentation {
private String id;
private int sequence;
private List<PluginDto> plugins;
private String name;
private List<String> features;
private Map<String, String> images;
}

View File

@@ -0,0 +1,65 @@
/*
* 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.
*/
package sonia.scm.api.v2.resources;
import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.PluginSet;
import javax.inject.Inject;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
public class PluginSetDtoMapper {
private final PluginDtoMapper pluginDtoMapper;
@Inject
protected PluginSetDtoMapper(PluginDtoMapper pluginDtoMapper) {
this.pluginDtoMapper = pluginDtoMapper;
}
public List<PluginSetDto> map(Collection<PluginSet> pluginSets, List<AvailablePlugin> availablePlugins, Locale locale) {
return pluginSets.stream()
.map(it -> map(it, availablePlugins, locale))
.sorted(Comparator.comparingInt(PluginSetDto::getSequence))
.collect(Collectors.toList());
}
private PluginSetDto map(PluginSet pluginSet, List<AvailablePlugin> availablePlugins, Locale locale) {
List<PluginDto> pluginDtos = pluginSet.getPlugins().stream()
.map(it -> availablePlugins.stream().filter(avail -> avail.getDescriptor().getInformation().getName().equals(it)).findFirst())
.filter(Optional::isPresent)
.map(Optional::get)
.map(pluginDtoMapper::mapAvailable)
.collect(Collectors.toList());
PluginSet.Description description = pluginSet.getDescriptions().get(locale.getLanguage());
return new PluginSetDto(pluginSet.getId(), pluginSet.getSequence(), pluginDtos, description.getName(), description.getFeatures(), pluginSet.getImages());
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
package sonia.scm.api.v2.resources;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.NotNull;
import java.util.Set;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PluginSetsInstallDto {
@NotNull
private Set<String> pluginSetIds;
}

View File

@@ -0,0 +1,149 @@
/*
* 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.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import sonia.scm.initialization.InitializationStepResource;
import sonia.scm.lifecycle.PluginWizardStartupAction;
import sonia.scm.lifecycle.PrivilegedStartupAction;
import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginManager;
import sonia.scm.plugin.PluginSet;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.web.VndMediaType;
import sonia.scm.web.security.AdministrationContext;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import static de.otto.edison.hal.Link.link;
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
@Extension
public class PluginWizardStartupResource implements InitializationStepResource {
private final PluginWizardStartupAction pluginWizardStartupAction;
private final ResourceLinks resourceLinks;
private final PluginManager pluginManager;
private final AccessTokenCookieIssuer cookieIssuer;
private final PluginSetDtoMapper pluginSetDtoMapper;
private final AdministrationContext context;
@Inject
public PluginWizardStartupResource(PluginWizardStartupAction pluginWizardStartupAction, ResourceLinks resourceLinks, PluginManager pluginManager, AccessTokenCookieIssuer cookieIssuer, PluginSetDtoMapper pluginSetDtoMapper, AdministrationContext context) {
this.pluginWizardStartupAction = pluginWizardStartupAction;
this.resourceLinks = resourceLinks;
this.pluginManager = pluginManager;
this.cookieIssuer = cookieIssuer;
this.pluginSetDtoMapper = pluginSetDtoMapper;
this.context = context;
}
@Override
public void setupIndex(Links.Builder builder, Embedded.Builder embeddedBuilder) {
setupIndex(builder, embeddedBuilder, Locale.getDefault());
}
@Override
public void setupIndex(Links.Builder builder, Embedded.Builder embeddedBuilder, Locale locale) {
context.runAsAdmin((PrivilegedStartupAction)() -> {
Set<PluginSet> pluginSets = pluginManager.getPluginSets();
List<AvailablePlugin> availablePlugins = pluginManager.getAvailable();
List<PluginSetDto> pluginSetDtos = pluginSetDtoMapper.map(pluginSets, availablePlugins, locale);
embeddedBuilder.with("pluginSets", pluginSetDtos);
String link = resourceLinks.pluginWizard().indexLink(name());
builder.single(link("installPluginSets", link));
});
}
@Override
public String name() {
return pluginWizardStartupAction.name();
}
@POST
@Path("")
@Consumes("application/json")
@Operation(
summary = "Install plugin sets and restart",
description = "Installs all plugins contained in the provided plugin sets and restarts the server",
tags = "Plugin Management",
requestBody = @RequestBody(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = PluginSetsInstallDto.class)
)
)
)
@ApiResponse(responseCode = "200", description = "success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"plugin:write\" privilege")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response installPluginSets(@Context HttpServletRequest request,
@Context HttpServletResponse response,
@Valid PluginSetsInstallDto dto) {
verifyInInitialization();
cookieIssuer.invalidate(request, response);
pluginManager.installPluginSets(dto.getPluginSetIds(), true);
return Response.ok().build();
}
private void verifyInInitialization() {
doThrow()
.violation("initialization not necessary")
.when(pluginWizardStartupAction.done());
}
}

View File

@@ -1207,6 +1207,25 @@ class ResourceLinks {
} }
} }
public PluginWizardLinks pluginWizard() {
return new PluginWizardLinks(new LinkBuilder(accessScmPathInfoStore().get(), InitializationResource.class, PluginWizardStartupResource.class));
}
public static class PluginWizardLinks {
private final LinkBuilder initializationLinkBuilder;
private PluginWizardLinks(LinkBuilder initializationLinkBuilder) {
this.initializationLinkBuilder = initializationLinkBuilder;
}
public String indexLink(String stepName) {
return initializationLinkBuilder
.method("step").parameters(stepName)
.method("installPluginSets").parameters()
.href();
}
}
public PluginCenterAuthLinks pluginCenterAuth() { public PluginCenterAuthLinks pluginCenterAuth() {
return new PluginCenterAuthLinks(scmPathInfoStore.get().get()); return new PluginCenterAuthLinks(scmPathInfoStore.get().get());
} }

View File

@@ -0,0 +1,89 @@
/*
* 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.
*/
package sonia.scm.initialization;
import org.apache.shiro.authc.AuthenticationException;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.web.security.AdministrationContext;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Singleton
public class InitializationAuthenticationService {
private static final String INITIALIZATION_SUBJECT = "SCM-INIT";
private final AccessTokenBuilderFactory tokenBuilderFactory;
private final PermissionAssigner permissionAssigner;
private final AccessTokenCookieIssuer cookieIssuer;
private final InitializationCookieIssuer initializationCookieIssuer;
private final AdministrationContext administrationContext;
@Inject
public InitializationAuthenticationService(AccessTokenBuilderFactory tokenBuilderFactory, PermissionAssigner permissionAssigner, AccessTokenCookieIssuer cookieIssuer, InitializationCookieIssuer initializationCookieIssuer, AdministrationContext administrationContext) {
this.tokenBuilderFactory = tokenBuilderFactory;
this.permissionAssigner = permissionAssigner;
this.cookieIssuer = cookieIssuer;
this.initializationCookieIssuer = initializationCookieIssuer;
this.administrationContext = administrationContext;
}
public void validateToken(AccessToken token) {
if (token == null || !INITIALIZATION_SUBJECT.equals(token.getSubject())) {
throw new AuthenticationException("Could not authenticate to initialization realm because of missing or invalid token.");
}
}
public void setPermissions() {
administrationContext.runAsAdmin(() -> permissionAssigner.setPermissionsForUser(
InitializationRealm.INIT_PRINCIPAL,
Set.of(new PermissionDescriptor("plugin:read,write"))
));
}
public void authenticate(HttpServletRequest request, HttpServletResponse response) {
AccessToken initToken =
tokenBuilderFactory.create()
.subject(INITIALIZATION_SUBJECT)
.expiresIn(365, TimeUnit.DAYS)
.refreshableFor(0, TimeUnit.SECONDS)
.build();
initializationCookieIssuer.authenticateForInitialization(request, response, initToken);
}
public void invalidateCookies(HttpServletRequest request, HttpServletResponse response) {
cookieIssuer.invalidate(request, response);
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.
*/
package sonia.scm.initialization;
import sonia.scm.security.AccessToken;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Generates cookies and invalidates initialization token cookies.
*
* @author Sebastian Sdorra
* @since 2.35.0
*/
public interface InitializationCookieIssuer {
/**
* Creates a cookie for token authentication and attaches it to the response.
*
* @param request http servlet request
* @param response http servlet response
* @param accessToken initialization access token
*/
void authenticateForInitialization(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken);
}

View File

@@ -0,0 +1,78 @@
/*
* 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.
*/
package sonia.scm.initialization;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.subject.SimplePrincipalCollection;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenResolver;
import sonia.scm.security.BearerToken;
import sonia.scm.user.User;
import javax.inject.Inject;
import javax.inject.Singleton;
import static com.google.common.base.Preconditions.checkArgument;
@Extension
@Singleton
public class InitializationRealm extends AuthenticatingRealm {
private static final String REALM = "InitializationRealm";
public static final String INIT_PRINCIPAL = "__SCM_INIT__";
private final InitializationAuthenticationService authenticationService;
private final AccessTokenResolver accessTokenResolver;
@Inject
public InitializationRealm(InitializationAuthenticationService authenticationService, AccessTokenResolver accessTokenResolver) {
this.authenticationService = authenticationService;
this.accessTokenResolver = accessTokenResolver;
setAuthenticationTokenClass(InitializationToken.class);
setCredentialsMatcher(new AllowAllCredentialsMatcher());
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
checkArgument(token instanceof InitializationToken, "%s is required", InitializationToken.class);
AccessToken accessToken = accessTokenResolver.resolve(BearerToken.valueOf(token.getCredentials().toString()));
authenticationService.validateToken(accessToken);
SimplePrincipalCollection principalCollection = new SimplePrincipalCollection(INIT_PRINCIPAL, REALM);
principalCollection.add(new User(INIT_PRINCIPAL), REALM);
authenticationService.setPermissions();
return new SimpleAuthenticationInfo(principalCollection, token.getCredentials());
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof InitializationToken;
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.
*/
package sonia.scm.initialization;
import org.apache.shiro.authc.AuthenticationToken;
public class InitializationToken implements AuthenticationToken {
private final String token;
private final String principal;
public InitializationToken(String token, String principal) {
this.token = token;
this.principal = principal;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public Object getCredentials() {
return token;
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.
*/
package sonia.scm.initialization;
import org.apache.shiro.authc.AuthenticationToken;
import sonia.scm.plugin.Extension;
import sonia.scm.web.WebTokenGenerator;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
@Extension
public class InitializationWebTokenGenerator implements WebTokenGenerator {
public static final String INIT_TOKEN_HEADER = "X-SCM-Init-Token";
@Override
public AuthenticationToken createToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
AuthenticationToken token = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(INIT_TOKEN_HEADER)) {
token = new InitializationToken(cookie.getValue(), "SCM_INIT");
}
}
}
return token;
}
}

View File

@@ -48,7 +48,7 @@ public class AdminAccountStartupAction implements InitializationStep {
private static final Logger LOG = LoggerFactory.getLogger(AdminAccountStartupAction.class); private static final Logger LOG = LoggerFactory.getLogger(AdminAccountStartupAction.class);
private static final String INITIAL_PASSWORD_PROPERTY = "scm.initialPassword"; public static final String INITIAL_PASSWORD_PROPERTY = "scm.initialPassword";
private static final String INITIAL_USER_PROPERTY = "scm.initialUser"; private static final String INITIAL_USER_PROPERTY = "scm.initialUser";
private final PasswordService passwordService; private final PasswordService passwordService;

View File

@@ -0,0 +1,60 @@
/*
* 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.
*/
package sonia.scm.lifecycle;
import sonia.scm.initialization.InitializationStep;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginSetConfigStore;
import javax.inject.Inject;
import javax.inject.Singleton;
@Extension
@Singleton
public class PluginWizardStartupAction implements InitializationStep {
private final PluginSetConfigStore store;
@Inject
public PluginWizardStartupAction(PluginSetConfigStore pluginSetConfigStore) {
this.store = pluginSetConfigStore;
}
@Override
public String name() {
return "pluginWizard";
}
@Override
public int sequence() {
return 1;
}
@Override
public boolean done() {
return System.getProperty(AdminAccountStartupAction.INITIAL_PASSWORD_PROPERTY) != null || store.getPluginSets().isPresent();
}
}

View File

@@ -28,4 +28,4 @@ import sonia.scm.plugin.ExtensionPoint;
import sonia.scm.web.security.PrivilegedAction; import sonia.scm.web.security.PrivilegedAction;
@ExtensionPoint @ExtensionPoint
interface PrivilegedStartupAction extends PrivilegedAction {} public interface PrivilegedStartupAction extends PrivilegedAction {}

View File

@@ -58,6 +58,7 @@ import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupManagerProvider; import sonia.scm.group.GroupManagerProvider;
import sonia.scm.group.xml.XmlGroupDAO; import sonia.scm.group.xml.XmlGroupDAO;
import sonia.scm.initialization.DefaultInitializationFinisher; import sonia.scm.initialization.DefaultInitializationFinisher;
import sonia.scm.initialization.InitializationCookieIssuer;
import sonia.scm.initialization.InitializationFinisher; import sonia.scm.initialization.InitializationFinisher;
import sonia.scm.io.ContentTypeResolver; import sonia.scm.io.ContentTypeResolver;
import sonia.scm.io.DefaultContentTypeResolver; import sonia.scm.io.DefaultContentTypeResolver;
@@ -271,6 +272,7 @@ class ScmServletModule extends ServletModule {
// bind events // bind events
bind(AccessTokenCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class); bind(AccessTokenCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class);
bind(InitializationCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class);
bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class); bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class);
// bind api link provider // bind api link provider

View File

@@ -39,6 +39,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@@ -65,6 +66,8 @@ public class DefaultPluginManager implements PluginManager {
private final Restarter restarter; private final Restarter restarter;
private final ScmEventBus eventBus; private final ScmEventBus eventBus;
private final PluginSetConfigStore pluginSetConfigStore;
private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>(); private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>();
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>(); private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
@@ -72,16 +75,17 @@ public class DefaultPluginManager implements PluginManager {
private final Function<List<AvailablePlugin>, PluginInstallationContext> contextFactory; private final Function<List<AvailablePlugin>, PluginInstallationContext> contextFactory;
@Inject @Inject
public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus) { public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus, PluginSetConfigStore pluginSetConfigStore) {
this(loader, center, installer, restarter, eventBus, null); this(loader, center, installer, restarter, eventBus, null, pluginSetConfigStore);
} }
DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus, Function<List<AvailablePlugin>, PluginInstallationContext> contextFactory) { DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus, Function<List<AvailablePlugin>, PluginInstallationContext> contextFactory, PluginSetConfigStore pluginSetConfigStore) {
this.loader = loader; this.loader = loader;
this.center = center; this.center = center;
this.installer = installer; this.installer = installer;
this.restarter = restarter; this.restarter = restarter;
this.eventBus = eventBus; this.eventBus = eventBus;
this.pluginSetConfigStore = pluginSetConfigStore;
if (contextFactory != null) { if (contextFactory != null) {
this.contextFactory = contextFactory; this.contextFactory = contextFactory;
@@ -109,7 +113,7 @@ public class DefaultPluginManager implements PluginManager {
@Override @Override
public Optional<AvailablePlugin> getAvailable(String name) { public Optional<AvailablePlugin> getAvailable(String name) {
PluginPermissions.read().check(); PluginPermissions.read().check();
return center.getAvailable() return center.getAvailablePlugins()
.stream() .stream()
.filter(filterByName(name)) .filter(filterByName(name))
.filter(this::isNotInstalledOrMoreUpToDate) .filter(this::isNotInstalledOrMoreUpToDate)
@@ -143,13 +147,49 @@ public class DefaultPluginManager implements PluginManager {
@Override @Override
public List<AvailablePlugin> getAvailable() { public List<AvailablePlugin> getAvailable() {
PluginPermissions.read().check(); PluginPermissions.read().check();
return center.getAvailable() return center.getAvailablePlugins()
.stream() .stream()
.filter(this::isNotInstalledOrMoreUpToDate) .filter(this::isNotInstalledOrMoreUpToDate)
.map(p -> getPending(p.getDescriptor().getInformation().getName()).orElse(p)) .map(p -> getPending(p.getDescriptor().getInformation().getName()).orElse(p))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override
public Set<PluginSet> getPluginSets() {
PluginPermissions.read().check();
return center.getAvailablePluginSets();
}
@Override
public void installPluginSets(Set<String> pluginSetIds, boolean restartAfterInstallation) {
PluginPermissions.write().check();
Set<PluginSet> pluginSets = getPluginSets();
Set<PluginSet> pluginSetsToInstall = pluginSetIds.stream()
.map(id -> pluginSets.stream().filter(pluginSet -> pluginSet.getId().equals(id)).findFirst())
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toSet());
Set<AvailablePlugin> pluginsToInstall = pluginSetsToInstall
.stream()
.flatMap(pluginSet -> pluginSet
.getPlugins()
.stream()
.map(this::collectPluginsToInstall)
.flatMap(Collection::stream)
)
.collect(Collectors.toSet());
Set<String> newlyInstalledPluginSetIds = pluginSetsToInstall.stream().map(PluginSet::getId).collect(Collectors.toSet());
Set<String> installedPluginSetIds = pluginSetConfigStore.getPluginSets().map(PluginSetsConfig::getPluginSets).orElse(new HashSet<>());
installedPluginSetIds.addAll(newlyInstalledPluginSetIds);
pluginSetConfigStore.setPluginSets(new PluginSetsConfig(installedPluginSetIds));
installPlugins(new ArrayList<>(pluginsToInstall), restartAfterInstallation);
}
@Override @Override
public List<InstalledPlugin> getUpdatable() { public List<InstalledPlugin> getUpdatable() {
return getInstalled() return getInstalled()
@@ -184,6 +224,10 @@ public class DefaultPluginManager implements PluginManager {
); );
List<AvailablePlugin> plugins = collectPluginsToInstall(name); List<AvailablePlugin> plugins = collectPluginsToInstall(name);
installPlugins(plugins, restartAfterInstallation);
}
private void installPlugins(List<AvailablePlugin> plugins, boolean restartAfterInstallation) {
List<PendingPluginInstallation> pendingInstallations = new ArrayList<>(); List<PendingPluginInstallation> pendingInstallations = new ArrayList<>();
for (AvailablePlugin plugin : plugins) { for (AvailablePlugin plugin : plugins) {

View File

@@ -42,52 +42,61 @@ import java.util.Set;
@Singleton @Singleton
public class PluginCenter { public class PluginCenter {
private static final String CACHE_NAME = "sonia.cache.plugins"; private static final String PLUGIN_CENTER_RESULT_CACHE_NAME = "sonia.cache.plugin-center";
private static final Logger LOG = LoggerFactory.getLogger(PluginCenter.class); private static final Logger LOG = LoggerFactory.getLogger(PluginCenter.class);
private final SCMContextProvider context; private final SCMContextProvider context;
private final ScmConfiguration configuration; private final ScmConfiguration configuration;
private final PluginCenterLoader loader; private final PluginCenterLoader loader;
private final Cache<String, Set<AvailablePlugin>> cache; private final Cache<String, PluginCenterResult> pluginCenterResultCache;
@Inject @Inject
public PluginCenter(SCMContextProvider context, CacheManager cacheManager, ScmConfiguration configuration, PluginCenterLoader loader) { public PluginCenter(SCMContextProvider context, CacheManager cacheManager, ScmConfiguration configuration, PluginCenterLoader loader) {
this.context = context; this.context = context;
this.configuration = configuration; this.configuration = configuration;
this.loader = loader; this.loader = loader;
this.cache = cacheManager.getCache(CACHE_NAME); this.pluginCenterResultCache = cacheManager.getCache(PLUGIN_CENTER_RESULT_CACHE_NAME);
} }
@Subscribe @Subscribe
public void handle(PluginCenterAuthenticationEvent event) { public void handle(PluginCenterAuthenticationEvent event) {
LOG.debug("clear plugin center cache, because of {}", event); LOG.debug("clear plugin center cache, because of {}", event);
cache.clear(); pluginCenterResultCache.clear();
} }
synchronized Set<AvailablePlugin> getAvailable() { synchronized Set<AvailablePlugin> getAvailablePlugins() {
String url = buildPluginUrl(configuration.getPluginUrl()); String url = buildPluginUrl(configuration.getPluginUrl());
Set<AvailablePlugin> plugins = cache.get(url); return getPluginCenterResult(url).getPlugins();
if (plugins == null) { }
LOG.debug("no cached available plugins found, start fetching");
plugins = fetchAvailablePlugins(url); synchronized Set<PluginSet> getAvailablePluginSets() {
String url = buildPluginUrl(configuration.getPluginUrl());
return getPluginCenterResult(url).getPluginSets();
}
private PluginCenterResult getPluginCenterResult(String url) {
PluginCenterResult pluginCenterResult = pluginCenterResultCache.get(url);
if (pluginCenterResult == null) {
LOG.debug("no cached plugin center result found, start fetching");
pluginCenterResult = fetchPluginCenter(url);
} else { } else {
LOG.debug("return available plugins from cache"); LOG.debug("return plugin center result from cache");
} }
return plugins; return pluginCenterResult;
} }
@CanIgnoreReturnValue @CanIgnoreReturnValue
private Set<AvailablePlugin> fetchAvailablePlugins(String url) { private PluginCenterResult fetchPluginCenter(String url) {
Set<AvailablePlugin> plugins = loader.load(url); PluginCenterResult pluginCenterResult = loader.load(url);
cache.put(url, plugins); pluginCenterResultCache.put(url, pluginCenterResult);
return plugins; return pluginCenterResult;
} }
synchronized void refresh() { synchronized void refresh() {
LOG.debug("refresh plugin center cache"); LOG.debug("refresh plugin center cache");
String url = buildPluginUrl(configuration.getPluginUrl()); String url = buildPluginUrl(configuration.getPluginUrl());
fetchAvailablePlugins(url); fetchPluginCenter(url);
} }
private String buildPluginUrl(String url) { private String buildPluginUrl(String url) {

View File

@@ -24,7 +24,6 @@
package sonia.scm.plugin; package sonia.scm.plugin;
import com.google.common.collect.ImmutableList;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -56,12 +55,22 @@ public final class PluginCenterDto implements Serializable {
@XmlElement(name = "plugins") @XmlElement(name = "plugins")
private List<Plugin> plugins; private List<Plugin> plugins;
@XmlElement(name = "plugin-sets")
private List<PluginSet> pluginSets;
public List<Plugin> getPlugins() { public List<Plugin> getPlugins() {
if (plugins == null) { if (plugins == null) {
plugins = ImmutableList.of(); plugins = List.of();
} }
return plugins; return plugins;
} }
public List<PluginSet> getPluginSets() {
if (pluginSets == null) {
pluginSets = List.of();
}
return pluginSets;
}
} }
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@@ -93,6 +102,36 @@ public final class PluginCenterDto implements Serializable {
private final Map<String, Link> links; private final Map<String, Link> links;
} }
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "pluginSets")
@Getter
@AllArgsConstructor
public static class PluginSet {
private final String id;
private final String versions;
private final int sequence;
@XmlElement(name = "plugins")
private final Set<String> plugins;
@XmlElement(name = "descriptions")
private final Map<String, Description> descriptions;
@XmlElement(name = "images")
private final Map<String, String> images;
}
@XmlAccessorType(XmlAccessType.FIELD)
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class Description {
private String name;
@XmlElement(name = "features")
private List<String> features;
}
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "conditions") @XmlRootElement(name = "conditions")
@Getter @Getter

View File

@@ -29,18 +29,31 @@ import org.mapstruct.factory.Mappers;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
@Mapper @Mapper
public abstract class PluginCenterDtoMapper { public abstract class PluginCenterDtoMapper {
PluginCenterDtoMapper() {}
static final PluginCenterDtoMapper INSTANCE = Mappers.getMapper(PluginCenterDtoMapper.class); static final PluginCenterDtoMapper INSTANCE = Mappers.getMapper(PluginCenterDtoMapper.class);
abstract PluginInformation map(PluginCenterDto.Plugin plugin); abstract PluginInformation map(PluginCenterDto.Plugin plugin);
abstract PluginCondition map(PluginCenterDto.Condition condition); abstract PluginCondition map(PluginCenterDto.Condition condition);
Set<AvailablePlugin> map(PluginCenterDto pluginCenterDto) { abstract PluginSet map(PluginCenterDto.PluginSet set);
abstract PluginSet.Description map(PluginCenterDto.Description description);
PluginCenterResult map(PluginCenterDto pluginCenterDto) {
Set<AvailablePlugin> plugins = new HashSet<>(); Set<AvailablePlugin> plugins = new HashSet<>();
Set<PluginSet> pluginSets = pluginCenterDto
.getEmbedded()
.getPluginSets()
.stream()
.map(this::map)
.collect(Collectors.toSet());
for (PluginCenterDto.Plugin plugin : pluginCenterDto.getEmbedded().getPlugins()) { for (PluginCenterDto.Plugin plugin : pluginCenterDto.getEmbedded().getPlugins()) {
// plugin center api returns always a download link, // plugin center api returns always a download link,
// but for cloudogu plugin without authentication the href is an empty string // but for cloudogu plugin without authentication the href is an empty string
@@ -51,7 +64,7 @@ public abstract class PluginCenterDtoMapper {
); );
plugins.add(new AvailablePlugin(descriptor)); plugins.add(new AvailablePlugin(descriptor));
} }
return plugins; return new PluginCenterResult(plugins, pluginSets);
} }
private String getInstallLink(PluginCenterDto.Plugin plugin) { private String getInstallLink(PluginCenterDto.Plugin plugin) {

View File

@@ -33,7 +33,6 @@ import sonia.scm.net.ahc.AdvancedHttpRequest;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.Collections; import java.util.Collections;
import java.util.Set;
import static sonia.scm.plugin.Tracing.SPAN_KIND; import static sonia.scm.plugin.Tracing.SPAN_KIND;
@@ -64,7 +63,7 @@ class PluginCenterLoader {
this.eventBus = eventBus; this.eventBus = eventBus;
} }
Set<AvailablePlugin> load(String url) { PluginCenterResult load(String url) {
try { try {
LOG.info("fetch plugins from {}", url); LOG.info("fetch plugins from {}", url);
AdvancedHttpRequest request = client.get(url).spanKind(SPAN_KIND); AdvancedHttpRequest request = client.get(url).spanKind(SPAN_KIND);
@@ -76,7 +75,7 @@ class PluginCenterLoader {
} catch (Exception ex) { } catch (Exception ex) {
LOG.error("failed to load plugins from plugin center, returning empty list", ex); LOG.error("failed to load plugins from plugin center, returning empty list", ex);
eventBus.post(new PluginCenterErrorEvent()); eventBus.post(new PluginCenterErrorEvent());
return Collections.emptySet(); return new PluginCenterResult(Collections.emptySet(), Collections.emptySet());
} }
} }
} }

View File

@@ -0,0 +1,37 @@
/*
* 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.
*/
package sonia.scm.plugin;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Set;
@AllArgsConstructor
@Getter
class PluginCenterResult {
private Set<AvailablePlugin> plugins;
private Set<PluginSet> pluginSets;
}

View File

@@ -0,0 +1,51 @@
/*
* 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.
*/
package sonia.scm.plugin;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class PluginSetConfigStore {
private final ConfigurationStore<PluginSetsConfig> pluginSets;
@Inject
PluginSetConfigStore(ConfigurationStoreFactory configurationStoreFactory) {
pluginSets = configurationStoreFactory.withType(PluginSetsConfig.class).withName("pluginSets").build();
}
public Optional<PluginSetsConfig> getPluginSets() {
return pluginSets.getOptional();
}
public void setPluginSets(PluginSetsConfig config) {
this.pluginSets.set(config);
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.
*/
package sonia.scm.plugin;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.Set;
@Data
@XmlRootElement
@AllArgsConstructor
@NoArgsConstructor
@XmlAccessorType(XmlAccessType.FIELD)
public class PluginSetsConfig {
@XmlElement(name = "pluginSets")
Set<String> pluginSets;
}

View File

@@ -29,6 +29,7 @@ import com.google.common.base.Strings;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.initialization.InitializationCookieIssuer;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
@@ -36,16 +37,18 @@ import javax.inject.Inject;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.Date; import java.util.Arrays;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static sonia.scm.initialization.InitializationWebTokenGenerator.INIT_TOKEN_HEADER;
/** /**
* Generates cookies and invalidates access token cookies. * Generates cookies and invalidates access token cookies.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 2.0.0 * @since 2.0.0
*/ */
public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIssuer { public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIssuer, InitializationCookieIssuer {
/** /**
* the logger for DefaultAccessTokenCookieIssuer * the logger for DefaultAccessTokenCookieIssuer
@@ -87,6 +90,25 @@ public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIs
response.addCookie(c); response.addCookie(c);
} }
/**
* Creates a cookie for authentication during the initialization process and attaches it to the response.
*
* @param request http servlet request
* @param response http servlet response
* @param accessToken initialization access token
*/
public void authenticateForInitialization(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken) {
LOG.trace("create and attach cookie for initialization access token {}", accessToken.getId());
Cookie c = new Cookie(INIT_TOKEN_HEADER, accessToken.compact());
c.setPath(contextPath(request));
c.setMaxAge(999999999);
c.setHttpOnly(isHttpOnly());
c.setSecure(isSecure(request));
// attach cookie to response
response.addCookie(c);
}
/** /**
* Invalidates the authentication cookie. * Invalidates the authentication cookie.
* *
@@ -95,8 +117,20 @@ public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIs
*/ */
public void invalidate(HttpServletRequest request, HttpServletResponse response) { public void invalidate(HttpServletRequest request, HttpServletResponse response) {
LOG.trace("invalidates access token cookie"); LOG.trace("invalidates access token cookie");
invalidateCookie(request, response, HttpUtil.COOKIE_BEARER_AUTHENTICATION);
Cookie c = new Cookie(HttpUtil.COOKIE_BEARER_AUTHENTICATION, Util.EMPTY_STRING); if (request.getCookies() != null && Arrays.stream(request.getCookies()).anyMatch(cookie -> cookie.getName().equals(INIT_TOKEN_HEADER))) {
LOG.trace("invalidates initialization token cookie");
invalidateInitTokenCookie(request, response);
}
}
private void invalidateInitTokenCookie(HttpServletRequest request, HttpServletResponse response) {
invalidateCookie(request, response, INIT_TOKEN_HEADER);
}
private void invalidateCookie(HttpServletRequest request, HttpServletResponse response, String cookieBearerAuthentication) {
Cookie c = new Cookie(cookieBearerAuthentication, Util.EMPTY_STRING);
c.setPath(contextPath(request)); c.setPath(contextPath(request));
c.setMaxAge(0); c.setMaxAge(0);
c.setHttpOnly(isHttpOnly()); c.setHttpOnly(isHttpOnly());

View File

@@ -0,0 +1,64 @@
/*
* 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.
*/
package sonia.scm.update.plugin;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.plugin.PluginSetConfigStore;
import sonia.scm.plugin.PluginSetsConfig;
import sonia.scm.user.xml.XmlUserDAO;
import sonia.scm.version.Version;
import javax.inject.Inject;
import java.util.Collections;
@Extension
public class PluginSetsConfigInitializationUpdateStep implements UpdateStep {
private final PluginSetConfigStore pluginSetConfigStore;
private final XmlUserDAO userDAO;
@Inject
public PluginSetsConfigInitializationUpdateStep(PluginSetConfigStore pluginSetConfigStore, XmlUserDAO userDAO) {
this.pluginSetConfigStore = pluginSetConfigStore;
this.userDAO = userDAO;
}
@Override
public void doUpdate() throws Exception {
if (!userDAO.getAll().isEmpty() && pluginSetConfigStore.getPluginSets().isEmpty()) {
pluginSetConfigStore.setPluginSets(new PluginSetsConfig(Collections.emptySet()));
}
}
@Override
public Version getTargetVersion() {
return Version.parse("2.0.0");
}
@Override
public String getAffectedDataType() {
return "sonia.scm.plugin.PluginSetsConfig";
}
}

View File

@@ -24,6 +24,9 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -33,10 +36,19 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.initialization.InitializationAuthenticationService;
import sonia.scm.lifecycle.AdminAccountStartupAction; import sonia.scm.lifecycle.AdminAccountStartupAction;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilder;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.security.DefaultAccessTokenCookieIssuer;
import sonia.scm.web.RestDispatcher; import sonia.scm.web.RestDispatcher;
import javax.inject.Provider; import javax.inject.Provider;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import static java.lang.String.format; import static java.lang.String.format;
@@ -46,9 +58,13 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.jboss.resteasy.mock.MockHttpRequest.post; import static org.jboss.resteasy.mock.MockHttpRequest.post;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@SubjectAware(
configuration = "classpath:sonia/scm/repository/shiro.ini"
)
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class AdminAccountStartupResourceTest { class AdminAccountStartupResourceTest {
@@ -60,9 +76,16 @@ class AdminAccountStartupResourceTest {
@Mock @Mock
private Provider<ScmPathInfoStore> pathInfoStoreProvider; private Provider<ScmPathInfoStore> pathInfoStoreProvider;
@Mock @Mock
private InitializationAuthenticationService authenticationService;
@Mock
private ScmPathInfoStore pathInfoStore; private ScmPathInfoStore pathInfoStore;
@Mock @Mock
private ScmPathInfo pathInfo; private ScmPathInfo pathInfo;
@Mock
private AccessTokenBuilderFactory accessTokenBuilderFactory;
@Mock
private AccessTokenBuilder accessTokenBuilder;
private final AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class));
@InjectMocks @InjectMocks
private AdminAccountStartupResource resource; private AdminAccountStartupResource resource;
@@ -73,6 +96,9 @@ class AdminAccountStartupResourceTest {
lenient().when(pathInfoStore.get()).thenReturn(pathInfo); lenient().when(pathInfoStore.get()).thenReturn(pathInfo);
dispatcher.addSingletonResource(new InitializationResource(singleton(resource))); dispatcher.addSingletonResource(new InitializationResource(singleton(resource)));
lenient().when(startupAction.name()).thenReturn("adminAccount"); lenient().when(startupAction.name()).thenReturn("adminAccount");
lenient().when(accessTokenBuilderFactory.create()).thenReturn(accessTokenBuilder);
AccessToken accessToken = mock(AccessToken.class);
lenient().when(accessTokenBuilder.build()).thenReturn(accessToken);
} }
@Test @Test
@@ -121,10 +147,17 @@ class AdminAccountStartupResourceTest {
@Test @Test
void shouldCreateAdminUser() throws URISyntaxException { void shouldCreateAdminUser() throws URISyntaxException {
Subject subject = mock(Subject.class);
ThreadContext.bind(subject);
MockHttpRequest request = MockHttpRequest request =
post("/v2/initialization/adminAccount") post("/v2/initialization/adminAccount")
.contentType("application/json") .contentType("application/json")
.content(createInput("initial-token", "trillian", "Tricia", "tricia@hitchhiker.com", "password", "password")); .content(createInput("initial-token", "trillian", "Tricia", "tricia@hitchhiker.com", "password", "password"));
HttpServletRequest servletRequest = mock(HttpServletRequest.class);
dispatcher.putDefaultContextObject(HttpServletRequest.class, servletRequest);
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(204); assertThat(response.getStatus()).isEqualTo(204);

View File

@@ -183,7 +183,7 @@ class IndexDtoGeneratorTest {
Embedded.Builder initializationEmbeddedBuilder = invocationOnMock.getArgument(1, Embedded.Builder.class); Embedded.Builder initializationEmbeddedBuilder = invocationOnMock.getArgument(1, Embedded.Builder.class);
initializationLinkBuilder.single(link("init", "/init")); initializationLinkBuilder.single(link("init", "/init"));
return null; return null;
}).when(initializationStepResource).setupIndex(any(), any()); }).when(initializationStepResource).setupIndex(any(), any(), any());
IndexDto dto = generator.generate(); IndexDto dto = generator.generate();

View File

@@ -30,19 +30,24 @@ import org.assertj.core.api.Assertions;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.SCMContextProvider; import sonia.scm.SCMContextProvider;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.initialization.InitializationFinisher; import sonia.scm.initialization.InitializationFinisher;
import sonia.scm.plugin.PluginCenterAuthenticator;
import sonia.scm.search.SearchEngine; import sonia.scm.search.SearchEngine;
import javax.servlet.http.HttpServletRequest;
import java.net.URI; import java.net.URI;
import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@SubjectAware(configuration = "classpath:sonia/scm/shiro-002.ini") @SubjectAware(configuration = "classpath:sonia/scm/shiro-002.ini")
@RunWith(MockitoJUnitRunner.class)
public class IndexResourceTest { public class IndexResourceTest {
@Rule @Rule
@@ -52,8 +57,12 @@ public class IndexResourceTest {
private SCMContextProvider scmContextProvider; private SCMContextProvider scmContextProvider;
private IndexResource indexResource; private IndexResource indexResource;
@Mock
private HttpServletRequest httpServletRequest;
@Before @Before
public void setUpObjectUnderTest() { public void setUpObjectUnderTest() {
when(httpServletRequest.getLocale()).thenReturn(Locale.ENGLISH);
this.configuration = new ScmConfiguration(); this.configuration = new ScmConfiguration();
this.scmContextProvider = mock(SCMContextProvider.class); this.scmContextProvider = mock(SCMContextProvider.class);
InitializationFinisher initializationFinisher = mock(InitializationFinisher.class); InitializationFinisher initializationFinisher = mock(InitializationFinisher.class);
@@ -72,7 +81,7 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "dent", password = "secret") @SubjectAware(username = "dent", password = "secret")
public void shouldRenderPluginCenterAuthLink() { public void shouldRenderPluginCenterAuthLink() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("pluginCenterAuth")).isPresent(); Assertions.assertThat(index.getLinks().getLinkBy("pluginCenterAuth")).isPresent();
} }
@@ -80,21 +89,21 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void shouldNotRenderPluginCenterLoginLinkIfPermissionsAreMissing() { public void shouldNotRenderPluginCenterLoginLinkIfPermissionsAreMissing() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("pluginCenterAuth")).isNotPresent(); Assertions.assertThat(index.getLinks().getLinkBy("pluginCenterAuth")).isNotPresent();
} }
@Test @Test
public void shouldRenderLoginUrlsForUnauthenticatedRequest() { public void shouldRenderLoginUrlsForUnauthenticatedRequest() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("login")).matches(Optional::isPresent); Assertions.assertThat(index.getLinks().getLinkBy("login")).matches(Optional::isPresent);
} }
@Test @Test
public void shouldRenderLoginInfoUrl() { public void shouldRenderLoginInfoUrl() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isPresent(); Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isPresent();
} }
@@ -103,21 +112,21 @@ public class IndexResourceTest {
public void shouldNotRenderLoginInfoUrlWhenNoUrlIsConfigured() { public void shouldNotRenderLoginInfoUrlWhenNoUrlIsConfigured() {
configuration.setLoginInfoUrl(""); configuration.setLoginInfoUrl("");
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isNotPresent(); Assertions.assertThat(index.getLinks().getLinkBy("loginInfo")).isNotPresent();
} }
@Test @Test
public void shouldRenderSelfLinkForUnauthenticatedRequest() { public void shouldRenderSelfLinkForUnauthenticatedRequest() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("self")).matches(Optional::isPresent); Assertions.assertThat(index.getLinks().getLinkBy("self")).matches(Optional::isPresent);
} }
@Test @Test
public void shouldRenderUiPluginsLinkForUnauthenticatedRequest() { public void shouldRenderUiPluginsLinkForUnauthenticatedRequest() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("uiPlugins")).matches(Optional::isPresent); Assertions.assertThat(index.getLinks().getLinkBy("uiPlugins")).matches(Optional::isPresent);
} }
@@ -125,7 +134,7 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void shouldRenderSelfLinkForAuthenticatedRequest() { public void shouldRenderSelfLinkForAuthenticatedRequest() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("self")).matches(Optional::isPresent); Assertions.assertThat(index.getLinks().getLinkBy("self")).matches(Optional::isPresent);
} }
@@ -133,7 +142,7 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void shouldRenderUiPluginsLinkForAuthenticatedRequest() { public void shouldRenderUiPluginsLinkForAuthenticatedRequest() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("uiPlugins")).matches(Optional::isPresent); Assertions.assertThat(index.getLinks().getLinkBy("uiPlugins")).matches(Optional::isPresent);
} }
@@ -141,7 +150,7 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void shouldRenderMeUrlForAuthenticatedRequest() { public void shouldRenderMeUrlForAuthenticatedRequest() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("me")).matches(Optional::isPresent); Assertions.assertThat(index.getLinks().getLinkBy("me")).matches(Optional::isPresent);
} }
@@ -149,7 +158,7 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void shouldRenderLogoutUrlForAuthenticatedRequest() { public void shouldRenderLogoutUrlForAuthenticatedRequest() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("logout")).matches(Optional::isPresent); Assertions.assertThat(index.getLinks().getLinkBy("logout")).matches(Optional::isPresent);
} }
@@ -157,7 +166,7 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void shouldRenderRepositoriesForAuthenticatedRequest() { public void shouldRenderRepositoriesForAuthenticatedRequest() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("repositories")).matches(Optional::isPresent); Assertions.assertThat(index.getLinks().getLinkBy("repositories")).matches(Optional::isPresent);
} }
@@ -165,7 +174,7 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void shouldNotRenderAdminLinksIfNotAuthorized() { public void shouldNotRenderAdminLinksIfNotAuthorized() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("users")).matches(o -> !o.isPresent()); Assertions.assertThat(index.getLinks().getLinkBy("users")).matches(o -> !o.isPresent());
Assertions.assertThat(index.getLinks().getLinkBy("groups")).matches(o -> !o.isPresent()); Assertions.assertThat(index.getLinks().getLinkBy("groups")).matches(o -> !o.isPresent());
@@ -175,7 +184,7 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void shouldRenderAutoCompleteLinks() { public void shouldRenderAutoCompleteLinks() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinksBy("autocomplete")) Assertions.assertThat(index.getLinks().getLinksBy("autocomplete"))
.extracting("name") .extracting("name")
@@ -185,7 +194,7 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "user_without_autocomplete_permission", password = "secret") @SubjectAware(username = "user_without_autocomplete_permission", password = "secret")
public void userWithoutAutocompletePermissionShouldSeeAutoCompleteLinksOnlyForNamespaces() { public void userWithoutAutocompletePermissionShouldSeeAutoCompleteLinksOnlyForNamespaces() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinksBy("autocomplete")) Assertions.assertThat(index.getLinks().getLinksBy("autocomplete"))
.extracting("name") .extracting("name")
@@ -195,7 +204,7 @@ public class IndexResourceTest {
@Test @Test
@SubjectAware(username = "dent", password = "secret") @SubjectAware(username = "dent", password = "secret")
public void shouldRenderAdminLinksIfAuthorized() { public void shouldRenderAdminLinksIfAuthorized() {
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getLinks().getLinkBy("users")).matches(Optional::isPresent); Assertions.assertThat(index.getLinks().getLinkBy("users")).matches(Optional::isPresent);
Assertions.assertThat(index.getLinks().getLinkBy("groups")).matches(Optional::isPresent); Assertions.assertThat(index.getLinks().getLinkBy("groups")).matches(Optional::isPresent);
@@ -206,7 +215,7 @@ public class IndexResourceTest {
public void shouldGenerateVersion() { public void shouldGenerateVersion() {
when(scmContextProvider.getVersion()).thenReturn("v1"); when(scmContextProvider.getVersion()).thenReturn("v1");
IndexDto index = indexResource.getIndex(); IndexDto index = indexResource.getIndex(httpServletRequest);
Assertions.assertThat(index.getVersion()).isEqualTo("v1"); Assertions.assertThat(index.getVersion()).isEqualTo("v1");
} }

View File

@@ -0,0 +1,99 @@
/*
* 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.
*/
package sonia.scm.api.v2.resources;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.PluginSet;
import java.util.List;
import java.util.Locale;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static sonia.scm.plugin.PluginTestHelper.createAvailable;
@ExtendWith(MockitoExtension.class)
class PluginSetDtoMapperTest {
@Mock
private PluginDtoMapper pluginDtoMapper;
@InjectMocks
private PluginSetDtoMapper mapper;
@Test
void shouldMap() {
AvailablePlugin git = createAvailable("scm-git-plugin");
AvailablePlugin svn = createAvailable("scm-svn-plugin");
AvailablePlugin hg = createAvailable("scm-hg-plugin");
PluginDto gitDto = new PluginDto();
gitDto.setName("scm-git-plugin");
PluginDto svnDto = new PluginDto();
svnDto.setName("scm-svn-plugin");
PluginDto hgDto = new PluginDto();
hgDto.setName("scm-hg-plugin");
when(pluginDtoMapper.mapAvailable(git)).thenReturn(gitDto);
when(pluginDtoMapper.mapAvailable(svn)).thenReturn(svnDto);
when(pluginDtoMapper.mapAvailable(hg)).thenReturn(hgDto);
List<AvailablePlugin> availablePlugins = List.of(git, svn, hg);
PluginSet pluginSet = new PluginSet(
"my-plugin-set",
1,
ImmutableSet.of("scm-git-plugin", "scm-hg-plugin"),
ImmutableMap.of("en", new PluginSet.Description("My Plugin Set", List.of("this is awesome!"))),
ImmutableMap.of("standard", "base64image")
);
PluginSet pluginSet2 = new PluginSet(
"my-other-plugin-set",
0,
ImmutableSet.of("scm-svn-plugin", "scm-hg-plugin"),
ImmutableMap.of("en", new PluginSet.Description("My Plugin Set 2", List.of("this is also awesome!"))),
ImmutableMap.of("standard", "base64image")
);
ImmutableSet<PluginSet> pluginSets = ImmutableSet.of(pluginSet, pluginSet2);
List<PluginSetDto> dtos = mapper.map(pluginSets, availablePlugins, Locale.ENGLISH);
assertThat(dtos).hasSize(2);
PluginSetDto first = dtos.get(0);
assertThat(first.getSequence()).isZero();
assertThat(first.getName()).isEqualTo("My Plugin Set 2");
assertThat(first.getFeatures()).contains("this is also awesome!");
assertThat(first.getImages()).isNotEmpty();
assertThat(first.getPlugins()).hasSize(2);
assertThat(dtos.get(1).getSequence()).isEqualTo(1);
}
}

View File

@@ -0,0 +1,186 @@
/*
* 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.
*/
package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links;
import org.apache.commons.lang.StringUtils;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.lifecycle.PluginWizardStartupAction;
import sonia.scm.lifecycle.PrivilegedStartupAction;
import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.PluginManager;
import sonia.scm.plugin.PluginSet;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.web.RestDispatcher;
import sonia.scm.web.security.AdministrationContext;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Locale;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static org.assertj.core.api.Assertions.assertThat;
import static org.jboss.resteasy.mock.MockHttpRequest.post;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static sonia.scm.plugin.PluginTestHelper.createAvailable;
@SubjectAware(
configuration = "classpath:sonia/scm/repository/shiro.ini"
)
@ExtendWith(MockitoExtension.class)
class PluginWizardStartupResourceTest {
@Mock
private PluginWizardStartupAction startupAction;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ResourceLinks resourceLinks;
@Mock
private PluginManager pluginManager;
@Mock
private AccessTokenCookieIssuer cookieIssuer;
@Mock
private PluginSetDtoMapper pluginSetDtoMapper;
@Mock
private AdministrationContext context;
private final RestDispatcher dispatcher = new RestDispatcher();
private final MockHttpResponse response = new MockHttpResponse();
@InjectMocks
private PluginWizardStartupResource resource;
@BeforeEach
void setUpMocks() {
dispatcher.addSingletonResource(new InitializationResource(singleton(resource)));
lenient().when(startupAction.name()).thenReturn("pluginWizard");
}
@Test
void shouldFailWhenActionIsDone() throws URISyntaxException {
when(startupAction.done()).thenReturn(true);
MockHttpRequest request =
post("/v2/initialization/pluginWizard")
.contentType("application/json")
.content(createInput("my-plugin-set"));
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
void shouldInstallPluginSets() throws URISyntaxException {
when(startupAction.done()).thenReturn(false);
MockHttpRequest request =
post("/v2/initialization/pluginWizard")
.contentType("application/json")
.content(createInput("my-plugin-set", "my-other-plugin-set"));
dispatcher.invoke(request, response);
verify(cookieIssuer).invalidate(any(), any());
verify(pluginManager).installPluginSets(ImmutableSet.of("my-plugin-set", "my-other-plugin-set"), true);
}
@Test
void shouldSetupIndex() {
AvailablePlugin git = createAvailable("scm-git-plugin");
AvailablePlugin svn = createAvailable("scm-svn-plugin");
AvailablePlugin hg = createAvailable("scm-hg-plugin");
List<AvailablePlugin> availablePlugins = List.of(git, svn, hg);
when(pluginManager.getAvailable()).thenReturn(availablePlugins);
doAnswer(invocation -> {
invocation.getArgument(0, PrivilegedStartupAction.class).run();
return null;
}).when(context).runAsAdmin(any(PrivilegedStartupAction.class));
PluginSet pluginSet = new PluginSet(
"my-plugin-set",
0,
ImmutableSet.of("scm-git-plugin", "scm-hg-plugin"),
ImmutableMap.of("en", new PluginSet.Description("My Plugin Set", List.of("this is awesome!"))),
ImmutableMap.of("standard", "base64image")
);
PluginSet pluginSet2 = new PluginSet(
"my-other-plugin-set",
0,
ImmutableSet.of("scm-svn-plugin", "scm-hg-plugin"),
ImmutableMap.of("en", new PluginSet.Description("My Plugin Set", List.of("this is awesome!"))),
ImmutableMap.of("standard", "base64image")
);
ImmutableSet<PluginSet> pluginSets = ImmutableSet.of(pluginSet, pluginSet2);
when(pluginManager.getPluginSets()).thenReturn(pluginSets);
when(pluginSetDtoMapper.map(pluginSets, availablePlugins, Locale.ENGLISH)).thenReturn(emptyList());
when(resourceLinks.pluginWizard().indexLink("pluginWizard")).thenReturn("http://index.link");
Embedded.Builder embeddedBuilder = new Embedded.Builder();
Links.Builder linksBuilder = new Links.Builder();
resource.setupIndex(linksBuilder, embeddedBuilder, Locale.ENGLISH);
Embedded embedded = embeddedBuilder.build();
Links links = linksBuilder.build();
assertThat(links.getLinkBy("installPluginSets")).isPresent();
assertThat(embedded.hasItem("pluginSets")).isTrue();
}
private byte[] createInput(String... pluginSetIds) {
String format = pluginSetIds.length > 0 ? "'%s'" : "%s";
return json(format("{'pluginSetIds': [" + format + "]}", StringUtils.join(pluginSetIds, "','")));
}
private byte[] json(String s) {
return s.replaceAll("'", "\"").getBytes(UTF_8);
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.
*/
package sonia.scm.initialization;
import org.apache.shiro.authc.AuthenticationException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilder;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.AccessTokenCookieIssuer;
import sonia.scm.web.security.AdministrationContext;
import sonia.scm.web.security.PrivilegedAction;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class InitializationAuthenticationServiceTest {
@Mock
private AccessTokenBuilderFactory tokenBuilderFactory;
@Mock(answer = Answers.RETURNS_SELF)
private AccessTokenBuilder tokenBuilder;
@Mock
private AccessToken token;
@Mock
private AccessTokenCookieIssuer cookieIssuer;
@Mock
private InitializationCookieIssuer initializationCookieIssuer;
@Mock
private AdministrationContext administrationContext;
@InjectMocks
private InitializationAuthenticationService service;
@Test
void shouldNotThrowExceptionIfTokenIsValid() {
when(token.getSubject()).thenReturn("SCM-INIT");
service.validateToken(token);
}
@Test
void shouldThrowExceptionIfTokenIsInvalid() {
when(token.getSubject()).thenReturn("FAKE");
assertThrows(AuthenticationException.class, () -> service.validateToken(token));
}
@Test
void shouldSetPermissionForVirtualInitializationUserInAdminContext() {
service.setPermissions();
verify(administrationContext).runAsAdmin(any(PrivilegedAction.class));
}
@Test
void shouldAuthenticate() {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
when(tokenBuilderFactory.create()).thenReturn(tokenBuilder);
AccessToken accessToken = mock(AccessToken.class);
when(tokenBuilder.build()).thenReturn(accessToken);
service.authenticate(request, response);
verify(initializationCookieIssuer)
.authenticateForInitialization(request, response, accessToken);
verify(tokenBuilder).subject("SCM-INIT");
}
@Test
void shouldInvalidateCookies() {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
service.invalidateCookies(request, response);
verify(cookieIssuer).invalidate(request, response);
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.
*/
package sonia.scm.initialization;
import org.apache.shiro.authc.AuthenticationToken;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static sonia.scm.initialization.InitializationWebTokenGenerator.INIT_TOKEN_HEADER;
@ExtendWith(MockitoExtension.class)
class InitializationWebTokenGeneratorTest {
private static final String INIT_TOKEN = "my_init_token";
private final InitializationWebTokenGenerator generator = new InitializationWebTokenGenerator();
@Test
void shouldReturnNullTokenIfCookieIsMissing() {
HttpServletRequest request = mock(HttpServletRequest.class);
AuthenticationToken token = generator.createToken(request);
assertThat(token).isNull();
}
@Test
void shouldGenerateCookieToken() {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getCookies()).thenReturn(new Cookie[]{new Cookie(INIT_TOKEN_HEADER, INIT_TOKEN)});
AuthenticationToken token = generator.createToken(request);
assertThat(token.getCredentials()).isEqualTo(INIT_TOKEN);
assertThat(token.getPrincipal()).isEqualTo("SCM_INIT");
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.
*/
package sonia.scm.lifecycle;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.plugin.PluginSetConfigStore;
import sonia.scm.plugin.PluginSetsConfig;
import java.util.Optional;
@ExtendWith(MockitoExtension.class)
class PluginWizardStartupActionTest {
@Mock
private PluginSetConfigStore pluginSetConfigStore;
@InjectMocks
private PluginWizardStartupAction startupAction;
@BeforeEach
void setup() {
System.clearProperty(AdminAccountStartupAction.INITIAL_PASSWORD_PROPERTY);
}
@Test
void shouldNotBeDoneByDefault() {
Assertions.assertThat(startupAction.done()).isFalse();
}
@Test
void shouldBeDoneIfInitialPasswordIsSet() {
System.setProperty(AdminAccountStartupAction.INITIAL_PASSWORD_PROPERTY, "secret");
Assertions.assertThat(startupAction.done()).isTrue();
}
@Test
void shouldBeDoneIfConfigIsAlreadySet() {
Mockito.when(pluginSetConfigStore.getPluginSets()).thenReturn(Optional.of(new PluginSetsConfig()));
Assertions.assertThat(startupAction.done()).isTrue();
}
}

View File

@@ -25,6 +25,7 @@
package sonia.scm.plugin; package sonia.scm.plugin;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
@@ -38,6 +39,7 @@ import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor; import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.ScmConstraintViolationException; import sonia.scm.ScmConstraintViolationException;
@@ -49,6 +51,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.singleton; import static java.util.Collections.singleton;
@@ -83,6 +86,9 @@ class DefaultPluginManagerTest {
@Mock @Mock
private Restarter restarter; private Restarter restarter;
@Mock
private PluginSetConfigStore pluginSetConfigStore;
@Mock @Mock
private ScmEventBus eventBus; private ScmEventBus eventBus;
@@ -110,7 +116,7 @@ class DefaultPluginManagerTest {
@BeforeEach @BeforeEach
void setUpObjectUnderTest() { void setUpObjectUnderTest() {
manager = new DefaultPluginManager( manager = new DefaultPluginManager(
loader, center, installer, restarter, eventBus, plugins -> context loader, center, installer, restarter, eventBus, plugins -> context, pluginSetConfigStore
); );
} }
@@ -162,7 +168,7 @@ class DefaultPluginManagerTest {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
AvailablePlugin git = createAvailable("scm-git-plugin"); AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review, git));
List<AvailablePlugin> available = manager.getAvailable(); List<AvailablePlugin> available = manager.getAvailable();
assertThat(available).containsOnly(review, git); assertThat(available).containsOnly(review, git);
@@ -175,7 +181,7 @@ class DefaultPluginManagerTest {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
AvailablePlugin git = createAvailable("scm-git-plugin"); AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review, git));
List<AvailablePlugin> available = manager.getAvailable(); List<AvailablePlugin> available = manager.getAvailable();
assertThat(available).containsOnly(review); assertThat(available).containsOnly(review);
@@ -185,7 +191,7 @@ class DefaultPluginManagerTest {
void shouldReturnAvailable() { void shouldReturnAvailable() {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
AvailablePlugin git = createAvailable("scm-git-plugin"); AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review, git));
Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin"); Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin");
assertThat(available).contains(git); assertThat(available).contains(git);
@@ -194,7 +200,7 @@ class DefaultPluginManagerTest {
@Test @Test
void shouldReturnEmptyForNonExistingAvailable() { void shouldReturnEmptyForNonExistingAvailable() {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review));
Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin"); Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin");
assertThat(available).isEmpty(); assertThat(available).isEmpty();
@@ -206,7 +212,7 @@ class DefaultPluginManagerTest {
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit)); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit));
AvailablePlugin git = createAvailable("scm-git-plugin"); AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(git));
Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin"); Optional<AvailablePlugin> available = manager.getAvailable("scm-git-plugin");
assertThat(available).isEmpty(); assertThat(available).isEmpty();
@@ -215,7 +221,7 @@ class DefaultPluginManagerTest {
@Test @Test
void shouldInstallThePlugin() { void shouldInstallThePlugin() {
AvailablePlugin git = createAvailable("scm-git-plugin"); AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(git));
manager.install("scm-git-plugin", false); manager.install("scm-git-plugin", false);
@@ -228,7 +234,7 @@ class DefaultPluginManagerTest {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
AvailablePlugin mail = createAvailable("scm-mail-plugin"); AvailablePlugin mail = createAvailable("scm-mail-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review, mail));
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
@@ -241,7 +247,7 @@ class DefaultPluginManagerTest {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
AvailablePlugin mail = createAvailable("scm-mail-plugin"); AvailablePlugin mail = createAvailable("scm-mail-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review, mail));
InstalledPlugin installedMail = createInstalled("scm-mail-plugin"); InstalledPlugin installedMail = createInstalled("scm-mail-plugin");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedMail)); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedMail));
@@ -259,7 +265,7 @@ class DefaultPluginManagerTest {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
AvailablePlugin mail = createAvailable("scm-mail-plugin", "1.1.0"); AvailablePlugin mail = createAvailable("scm-mail-plugin", "1.1.0");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review, mail));
InstalledPlugin installedMail = createInstalled("scm-mail-plugin", "1.0.0"); InstalledPlugin installedMail = createInstalled("scm-mail-plugin", "1.0.0");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedMail)); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedMail));
@@ -275,7 +281,7 @@ class DefaultPluginManagerTest {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(review.getDescriptor().getOptionalDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); when(review.getDescriptor().getOptionalDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
AvailablePlugin mail = createAvailable("scm-mail-plugin", "1.1.0"); AvailablePlugin mail = createAvailable("scm-mail-plugin", "1.1.0");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review, mail));
InstalledPlugin installedMail = createInstalled("scm-mail-plugin", "1.0.0"); InstalledPlugin installedMail = createInstalled("scm-mail-plugin", "1.0.0");
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedMail)); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedMail));
@@ -291,7 +297,7 @@ class DefaultPluginManagerTest {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(review.getDescriptor().getOptionalDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); when(review.getDescriptor().getOptionalDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
AvailablePlugin mail = createAvailable("scm-mail-plugin", "1.1.0"); AvailablePlugin mail = createAvailable("scm-mail-plugin", "1.1.0");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review, mail));
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
@@ -306,7 +312,7 @@ class DefaultPluginManagerTest {
AvailablePlugin mail = createAvailable("scm-mail-plugin"); AvailablePlugin mail = createAvailable("scm-mail-plugin");
when(mail.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-notification-plugin")); when(mail.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-notification-plugin"));
AvailablePlugin notification = createAvailable("scm-notification-plugin"); AvailablePlugin notification = createAvailable("scm-notification-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail, notification)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review, mail, notification));
PendingPluginInstallation pendingNotification = mock(PendingPluginInstallation.class); PendingPluginInstallation pendingNotification = mock(PendingPluginInstallation.class);
doReturn(pendingNotification).when(installer).install(context, notification); doReturn(pendingNotification).when(installer).install(context, notification);
@@ -328,7 +334,7 @@ class DefaultPluginManagerTest {
when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin"));
AvailablePlugin mail = createAvailable("scm-mail-plugin"); AvailablePlugin mail = createAvailable("scm-mail-plugin");
when(mail.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-notification-plugin")); when(mail.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-notification-plugin"));
when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review, mail));
assertThrows(NotFoundException.class, () -> manager.install("scm-review-plugin", false)); assertThrows(NotFoundException.class, () -> manager.install("scm-review-plugin", false));
@@ -338,7 +344,7 @@ class DefaultPluginManagerTest {
@Test @Test
void shouldSendRestartEventAfterInstallation() { void shouldSendRestartEventAfterInstallation() {
AvailablePlugin git = createAvailable("scm-git-plugin"); AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(git));
manager.install("scm-git-plugin", true); manager.install("scm-git-plugin", true);
@@ -358,7 +364,7 @@ class DefaultPluginManagerTest {
@Test @Test
void shouldNotInstallAlreadyPendingPlugins() { void shouldNotInstallAlreadyPendingPlugins() {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review));
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
@@ -369,7 +375,7 @@ class DefaultPluginManagerTest {
@Test @Test
void shouldSendRestartEvent() { void shouldSendRestartEvent() {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review));
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
manager.executePendingAndRestart(); manager.executePendingAndRestart();
@@ -387,7 +393,7 @@ class DefaultPluginManagerTest {
@Test @Test
void shouldReturnSingleAvailableAsPending() { void shouldReturnSingleAvailableAsPending() {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review));
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
@@ -398,7 +404,7 @@ class DefaultPluginManagerTest {
@Test @Test
void shouldReturnAvailableAsPending() { void shouldReturnAvailableAsPending() {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review));
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
@@ -514,7 +520,7 @@ class DefaultPluginManagerTest {
when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin")); when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin"));
when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin)); when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin));
when(center.getAvailable()).thenReturn(singleton(reviewPlugin)); when(center.getAvailablePlugins()).thenReturn(singleton(reviewPlugin));
manager.computeInstallationDependencies(); manager.computeInstallationDependencies();
@@ -546,7 +552,7 @@ class DefaultPluginManagerTest {
doNothing().when(mailPlugin).setMarkedForUninstall(uninstallCaptor.capture()); doNothing().when(mailPlugin).setMarkedForUninstall(uninstallCaptor.capture());
AvailablePlugin git = createAvailable("scm-git-plugin"); AvailablePlugin git = createAvailable("scm-git-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(git));
PendingPluginInstallation gitPendingPluginInformation = mock(PendingPluginInstallation.class); PendingPluginInstallation gitPendingPluginInformation = mock(PendingPluginInstallation.class);
when(installer.install(context, git)).thenReturn(gitPendingPluginInformation); when(installer.install(context, git)).thenReturn(gitPendingPluginInformation);
@@ -577,7 +583,7 @@ class DefaultPluginManagerTest {
AvailablePlugin newMailPlugin = createAvailable("scm-mail-plugin", "2.0.0"); AvailablePlugin newMailPlugin = createAvailable("scm-mail-plugin", "2.0.0");
AvailablePlugin newReviewPlugin = createAvailable("scm-review-plugin", "2.0.0"); AvailablePlugin newReviewPlugin = createAvailable("scm-review-plugin", "2.0.0");
when(center.getAvailable()).thenReturn(ImmutableSet.of(newMailPlugin, newReviewPlugin)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(newMailPlugin, newReviewPlugin));
manager.updateAll(); manager.updateAll();
@@ -593,7 +599,7 @@ class DefaultPluginManagerTest {
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(scriptPlugin)); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(scriptPlugin));
AvailablePlugin oldScriptPlugin = createAvailable("scm-script-plugin", "0.9"); AvailablePlugin oldScriptPlugin = createAvailable("scm-script-plugin", "0.9");
when(center.getAvailable()).thenReturn(ImmutableSet.of(oldScriptPlugin)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(oldScriptPlugin));
manager.updateAll(); manager.updateAll();
@@ -603,7 +609,7 @@ class DefaultPluginManagerTest {
@Test @Test
void shouldFirePluginEventOnInstallation() { void shouldFirePluginEventOnInstallation() {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review));
manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false);
@@ -616,7 +622,7 @@ class DefaultPluginManagerTest {
@Test @Test
void shouldFirePluginEventOnFailedInstallation() { void shouldFirePluginEventOnFailedInstallation() {
AvailablePlugin review = createAvailable("scm-review-plugin"); AvailablePlugin review = createAvailable("scm-review-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(review));
doThrow(new PluginDownloadException(review, new IOException())).when(installer).install(context, review); doThrow(new PluginDownloadException(review, new IOException())).when(installer).install(context, review);
assertThrows(PluginDownloadException.class, () -> manager.install("scm-review-plugin", false)); assertThrows(PluginDownloadException.class, () -> manager.install("scm-review-plugin", false));
@@ -637,7 +643,7 @@ class DefaultPluginManagerTest {
when(jenkins.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-el-plugin")); when(jenkins.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-el-plugin"));
when(webhook.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-el-plugin")); when(webhook.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-el-plugin"));
AvailablePlugin el = createAvailable("scm-el-plugin"); AvailablePlugin el = createAvailable("scm-el-plugin");
when(center.getAvailable()).thenReturn(ImmutableSet.of(jenkins, el, webhook)); when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(jenkins, el, webhook));
manager.install("scm-jenkins-plugin", false); manager.install("scm-jenkins-plugin", false);
manager.install("scm-webhook-plugin", false); manager.install("scm-webhook-plugin", false);
@@ -650,6 +656,55 @@ class DefaultPluginManagerTest {
assertThat(pluginInstallationContext.find("scm-webhook-plugin")).isPresent(); assertThat(pluginInstallationContext.find("scm-webhook-plugin")).isPresent();
assertThat(pluginInstallationContext.find("scm-el-plugin")).isPresent(); assertThat(pluginInstallationContext.find("scm-el-plugin")).isPresent();
} }
@Test
void shouldGetPluginSets() {
PluginSet pluginSet = new PluginSet(
"my-plugin-set",
0,
ImmutableSet.of("scm-jenkins-plugin", "scm-webhook-plugin", "scm-el-plugin"),
ImmutableMap.of("en", new PluginSet.Description("My Plugin Set", List.of("this is awesome!"))),
ImmutableMap.of("standard", "base64image")
);
when(center.getAvailablePluginSets()).thenReturn(ImmutableSet.of(pluginSet));
Set<PluginSet> pluginSets = manager.getPluginSets();
assertThat(pluginSets).containsExactly(pluginSet);
}
@Test
void shouldInstallPluginSets() {
AvailablePlugin git = createAvailable("scm-git-plugin");
AvailablePlugin svn = createAvailable("scm-svn-plugin");
AvailablePlugin hg = createAvailable("scm-hg-plugin");
when(center.getAvailablePlugins()).thenReturn(ImmutableSet.of(git, svn, hg));
PluginSet pluginSet = new PluginSet(
"my-plugin-set",
0,
ImmutableSet.of("scm-git-plugin", "scm-hg-plugin"),
ImmutableMap.of("en", new PluginSet.Description("My Plugin Set", List.of("this is awesome!"))),
ImmutableMap.of("standard", "base64image")
);
PluginSet pluginSet2 = new PluginSet(
"my-other-plugin-set",
0,
ImmutableSet.of("scm-svn-plugin", "scm-hg-plugin"),
ImmutableMap.of("en", new PluginSet.Description("My Plugin Set", List.of("this is awesome!"))),
ImmutableMap.of("standard", "base64image")
);
when(center.getAvailablePluginSets()).thenReturn(ImmutableSet.of(pluginSet, pluginSet2));
manager.installPluginSets(ImmutableSet.of("my-plugin-set", "my-other-plugin-set"), false);
verify(pluginSetConfigStore).setPluginSets(new PluginSetsConfig(ImmutableSet.of("my-plugin-set", "my-other-plugin-set")));
verify(installer, Mockito.times(1)).install(context, git);
verify(installer, Mockito.times(1)).install(context, hg);
verify(installer, Mockito.times(1)).install(context, svn);
verify(restarter, never()).restart(any(), any());
}
} }
@Nested @Nested
@@ -672,6 +727,7 @@ class DefaultPluginManagerTest {
assertThrows(AuthorizationException.class, () -> manager.getInstalled("test")); assertThrows(AuthorizationException.class, () -> manager.getInstalled("test"));
assertThrows(AuthorizationException.class, () -> manager.getAvailable()); assertThrows(AuthorizationException.class, () -> manager.getAvailable());
assertThrows(AuthorizationException.class, () -> manager.getAvailable("test")); assertThrows(AuthorizationException.class, () -> manager.getAvailable("test"));
assertThrows(AuthorizationException.class, () -> manager.getPluginSets());
} }
} }
@@ -695,6 +751,12 @@ class DefaultPluginManagerTest {
assertThrows(AuthorizationException.class, () -> manager.install("test", false)); assertThrows(AuthorizationException.class, () -> manager.install("test", false));
} }
@Test
void shouldThrowAuthorizationExceptionsForInstallPluginSetsMethod() {
ImmutableSet<String> pluginSetIds = ImmutableSet.of("test");
assertThrows(AuthorizationException.class, () -> manager.installPluginSets(pluginSetIds, false));
}
@Test @Test
void shouldThrowAuthorizationExceptionsForUninstallMethod() { void shouldThrowAuthorizationExceptionsForUninstallMethod() {
assertThrows(AuthorizationException.class, () -> manager.uninstall("test", false)); assertThrows(AuthorizationException.class, () -> manager.uninstall("test", false));

View File

@@ -33,17 +33,16 @@ import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sonia.scm.plugin.PluginCenterDto.Condition;
import static sonia.scm.plugin.PluginCenterDto.Link;
import static sonia.scm.plugin.PluginCenterDto.Plugin; import static sonia.scm.plugin.PluginCenterDto.Plugin;
import static sonia.scm.plugin.PluginCenterDto.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class PluginCenterDtoMapperTest { class PluginCenterDtoMapperTest {
@@ -72,8 +71,19 @@ class PluginCenterDtoMapperTest {
ImmutableMap.of("download", new Link("http://download.hitchhiker.com")) ImmutableMap.of("download", new Link("http://download.hitchhiker.com"))
); );
PluginCenterDto.PluginSet pluginSet = new PluginCenterDto.PluginSet(
"my-plugin-set",
">2.0.0",
0,
ImmutableSet.of("scm-review-plugin"),
ImmutableMap.of("en", new PluginCenterDto.Description("My Plugin Set", List.of("hello world"))),
ImmutableMap.of("standard", "base64image")
);
when(dto.getEmbedded().getPlugins()).thenReturn(Collections.singletonList(plugin)); when(dto.getEmbedded().getPlugins()).thenReturn(Collections.singletonList(plugin));
AvailablePluginDescriptor descriptor = mapper.map(dto).iterator().next().getDescriptor(); when(dto.getEmbedded().getPluginSets()).thenReturn(Collections.singletonList(pluginSet));
PluginCenterResult mapped = mapper.map(dto);
AvailablePluginDescriptor descriptor = mapped.getPlugins().iterator().next().getDescriptor();
PluginInformation information = descriptor.getInformation(); PluginInformation information = descriptor.getInformation();
PluginCondition condition = descriptor.getCondition(); PluginCondition condition = descriptor.getCondition();
@@ -88,6 +98,14 @@ class PluginCenterDtoMapperTest {
assertThat(condition.getOs().iterator().next()).isEqualTo(plugin.getConditions().getOs().iterator().next()); assertThat(condition.getOs().iterator().next()).isEqualTo(plugin.getConditions().getOs().iterator().next());
assertThat(information.getDescription()).isEqualTo(plugin.getDescription()); assertThat(information.getDescription()).isEqualTo(plugin.getDescription());
assertThat(information.getName()).isEqualTo(plugin.getName()); assertThat(information.getName()).isEqualTo(plugin.getName());
PluginSet mappedPluginSet = mapped.getPluginSets().iterator().next();
assertThat(mappedPluginSet.getId()).isEqualTo(pluginSet.getId());
assertThat(mappedPluginSet.getSequence()).isEqualTo(pluginSet.getSequence());
assertThat(mappedPluginSet.getPlugins()).hasSize(pluginSet.getPlugins().size());
assertThat(mappedPluginSet.getImages()).isNotEmpty();
assertThat(mappedPluginSet.getDescriptions()).isNotEmpty();
} }
@Test @Test
@@ -126,7 +144,8 @@ class PluginCenterDtoMapperTest {
when(dto.getEmbedded().getPlugins()).thenReturn(Arrays.asList(plugin1, plugin2)); when(dto.getEmbedded().getPlugins()).thenReturn(Arrays.asList(plugin1, plugin2));
Set<AvailablePlugin> resultSet = mapper.map(dto); PluginCenterResult pluginCenterResult = mapper.map(dto);
Set<AvailablePlugin> resultSet = pluginCenterResult.getPlugins();
PluginInformation pluginInformation1 = findPlugin(resultSet, plugin1.getName()); PluginInformation pluginInformation1 = findPlugin(resultSet, plugin1.getName());
PluginInformation pluginInformation2 = findPlugin(resultSet, plugin2.getName()); PluginInformation pluginInformation2 = findPlugin(resultSet, plugin2.getName());

View File

@@ -43,7 +43,6 @@ import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import static sonia.scm.plugin.Tracing.SPAN_KIND;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class PluginCenterLoaderTest { class PluginCenterLoaderTest {
@@ -71,12 +70,15 @@ class PluginCenterLoaderTest {
@Test @Test
void shouldFetch() throws IOException { void shouldFetch() throws IOException {
Set<AvailablePlugin> plugins = Collections.emptySet(); Set<AvailablePlugin> plugins = Collections.emptySet();
Set<PluginSet> pluginSets = Collections.emptySet();
PluginCenterDto dto = new PluginCenterDto(); PluginCenterDto dto = new PluginCenterDto();
PluginCenterResult pluginCenterResult = new PluginCenterResult(plugins, pluginSets);
when(request().contentFromJson(PluginCenterDto.class)).thenReturn(dto); when(request().contentFromJson(PluginCenterDto.class)).thenReturn(dto);
when(mapper.map(dto)).thenReturn(plugins); when(mapper.map(dto)).thenReturn(pluginCenterResult);
Set<AvailablePlugin> fetched = loader.load(PLUGIN_URL); PluginCenterResult fetched = loader.load(PLUGIN_URL);
assertThat(fetched).isSameAs(plugins); assertThat(fetched.getPlugins()).isSameAs(plugins);
assertThat(fetched.getPluginSets()).isSameAs(pluginSets);
} }
private AdvancedHttpResponse request() throws IOException { private AdvancedHttpResponse request() throws IOException {
@@ -91,8 +93,9 @@ class PluginCenterLoaderTest {
when(client.get(PLUGIN_URL)).thenReturn(request); when(client.get(PLUGIN_URL)).thenReturn(request);
when(request.request()).thenThrow(new IOException("failed to fetch")); when(request.request()).thenThrow(new IOException("failed to fetch"));
Set<AvailablePlugin> fetch = loader.load(PLUGIN_URL); PluginCenterResult fetch = loader.load(PLUGIN_URL);
assertThat(fetch).isEmpty(); assertThat(fetch.getPlugins()).isEmpty();
assertThat(fetch.getPluginSets()).isEmpty();
} }
@Test @Test
@@ -119,8 +122,9 @@ class PluginCenterLoaderTest {
private Set<AvailablePlugin> mockResponse() throws IOException { private Set<AvailablePlugin> mockResponse() throws IOException {
PluginCenterDto dto = new PluginCenterDto(); PluginCenterDto dto = new PluginCenterDto();
Set<AvailablePlugin> plugins = Collections.emptySet(); Set<AvailablePlugin> plugins = Collections.emptySet();
Set<PluginSet> pluginSets = Collections.emptySet();
when(request().contentFromJson(PluginCenterDto.class)).thenReturn(dto); when(request().contentFromJson(PluginCenterDto.class)).thenReturn(dto);
when(mapper.map(dto)).thenReturn(plugins); when(mapper.map(dto)).thenReturn(new PluginCenterResult(plugins, pluginSets));
return plugins; return plugins;
} }

View File

@@ -24,21 +24,16 @@
package sonia.scm.plugin; package sonia.scm.plugin;
import com.google.common.collect.ImmutableSet;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider; import sonia.scm.SCMContextProvider;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.cache.MapCacheManager; import sonia.scm.cache.MapCacheManager;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.util.SystemUtil;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@@ -81,36 +76,52 @@ class PluginCenterTest {
@Test @Test
void shouldFetchPlugins() { void shouldFetchPlugins() {
Set<AvailablePlugin> plugins = new HashSet<>(); Set<AvailablePlugin> plugins = new HashSet<>();
when(loader.load(PLUGIN_URL_BASE + "2.0.0")).thenReturn(plugins); Set<PluginSet> pluginSets = new HashSet<>();
assertThat(pluginCenter.getAvailable()).isSameAs(plugins); PluginCenterResult pluginCenterResult = new PluginCenterResult(plugins, pluginSets);
when(loader.load(PLUGIN_URL_BASE + "2.0.0")).thenReturn(pluginCenterResult);
assertThat(pluginCenter.getAvailablePlugins()).isSameAs(plugins);
assertThat(pluginCenter.getAvailablePluginSets()).isSameAs(pluginSets);
} }
@Test @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
void shouldCache() { void shouldCache() {
Set<AvailablePlugin> first = new HashSet<>(); Set<AvailablePlugin> plugins = new HashSet<>();
when(loader.load(anyString())).thenReturn(first, new HashSet<>()); Set<PluginSet> pluginSets = new HashSet<>();
assertThat(pluginCenter.getAvailable()).isSameAs(first); PluginCenterResult first = new PluginCenterResult(plugins, pluginSets);
assertThat(pluginCenter.getAvailable()).isSameAs(first); when(loader.load(anyString())).thenReturn(first, new PluginCenterResult(Collections.emptySet(), Collections.emptySet()));
assertThat(pluginCenter.getAvailablePlugins()).isSameAs(plugins);
assertThat(pluginCenter.getAvailablePlugins()).isSameAs(plugins);
assertThat(pluginCenter.getAvailablePluginSets()).isSameAs(pluginSets);
} }
@Test @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
void shouldClearCache() { void shouldClearCache() {
Set<AvailablePlugin> first = new HashSet<>(); Set<AvailablePlugin> plugins = new HashSet<>();
when(loader.load(anyString())).thenReturn(first, new HashSet<>()); Set<PluginSet> pluginSets = new HashSet<>();
assertThat(pluginCenter.getAvailable()).isSameAs(first); PluginCenterResult first = new PluginCenterResult(plugins, pluginSets);
when(loader.load(anyString())).thenReturn(first, new PluginCenterResult(Collections.emptySet(), Collections.emptySet()));
assertThat(pluginCenter.getAvailablePlugins()).isSameAs(plugins);
assertThat(pluginCenter.getAvailablePluginSets()).isSameAs(pluginSets);
pluginCenter.handle(new PluginCenterLoginEvent(null)); pluginCenter.handle(new PluginCenterLoginEvent(null));
assertThat(pluginCenter.getAvailable()).isNotSameAs(first); assertThat(pluginCenter.getAvailablePlugins()).isNotSameAs(plugins);
assertThat(pluginCenter.getAvailablePluginSets()).isNotSameAs(pluginSets);
} }
@Test @Test
void shouldLoadOnRefresh() { void shouldLoadOnRefresh() {
Set<AvailablePlugin> plugins = new HashSet<>(); Set<AvailablePlugin> plugins = new HashSet<>();
when(loader.load(PLUGIN_URL_BASE + "2.0.0")).thenReturn(plugins); Set<PluginSet> pluginSets = new HashSet<>();
PluginCenterResult pluginCenterResult = new PluginCenterResult(plugins, pluginSets);
when(loader.load(PLUGIN_URL_BASE + "2.0.0")).thenReturn(pluginCenterResult);
pluginCenter.refresh(); pluginCenter.refresh();