mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-12-21 15:59:48 +01:00
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:
committed by
Eduard Heimbuch
parent
6216945f0d
commit
1b18191c57
@@ -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"
|
||||||
|
|||||||
BIN
docs/en/first-startup/assets/plugin-wizard.png
Normal file
BIN
docs/en/first-startup/assets/plugin-wizard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -33,4 +33,15 @@ The password of the administration user cannot be recovered.
|
|||||||
For automated processes, you might want to bypass the initial user creation. To do so, you can set the initial password
|
For automated processes, you might want to bypass the initial user creation. To do so, you can set the initial password
|
||||||
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.
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
2
gradle/changelog/plugin_wizard.yaml
Normal file
2
gradle/changelog/plugin_wizard.yaml
Normal 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))
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
55
scm-core/src/main/java/sonia/scm/plugin/PluginSet.java
Normal file
55
scm-core/src/main/java/sonia/scm/plugin/PluginSet.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
* 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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> => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -21,10 +21,9 @@
|
|||||||
* 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
import de.otto.edison.hal.HalRepresentation;
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user