mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-17 21:12:09 +01:00
Merge branch 'develop' into bugfix/plugin_dependency_context
This commit is contained in:
@@ -5,7 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
### Fixed
|
||||
### Fixed
|
||||
- JWT token timeout is now handled properly ([#1297](https://github.com/scm-manager/scm-manager/pull/1297))
|
||||
- Fix text-overflow in danger zone ([#1298](https://github.com/scm-manager/scm-manager/pull/1298))
|
||||
- Fix plugin installation error if previously a plugin was installed with the same dependency which is still pending. ([#1300](https://github.com/scm-manager/scm-manager/pull/1300))
|
||||
|
||||
## [2.4.0] - 2020-08-14
|
||||
|
||||
6
Jenkinsfile
vendored
6
Jenkinsfile
vendored
@@ -75,7 +75,9 @@ node('docker') {
|
||||
integrationTest: {
|
||||
stage('Integration Test') {
|
||||
mvn 'verify -Pit -DskipUnitTests -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true'
|
||||
junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml'
|
||||
junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/cypress-reports/TEST-*.xml'
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'scm-ui/e2e-tests/cypress/videos/*.mp4'
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'scm-ui/e2e-tests/cypress/screenshots/**/*.png'
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -190,7 +192,7 @@ node('docker') {
|
||||
String mainBranch
|
||||
|
||||
Maven setupMavenBuild() {
|
||||
MavenWrapperInDocker mvn = new MavenWrapperInDocker(this, "scmmanager/java-build:11.0.7_10")
|
||||
MavenWrapperInDocker mvn = new MavenWrapperInDocker(this, "scmmanager/java-build:11.0.8_10")
|
||||
mvn.enableDockerHost = true
|
||||
|
||||
// disable logging durring the build
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
# SOFTWARE.
|
||||
#
|
||||
|
||||
FROM adoptopenjdk/openjdk11:x86_64-debian-jdk-11.0.7_10
|
||||
FROM adoptopenjdk/openjdk11:x86_64-debian-jdk-11.0.8_10
|
||||
|
||||
ENV DOCKER_VERSION=19.03.8 \
|
||||
DOCKER_CHANNEL=stable \
|
||||
@@ -34,11 +34,24 @@ COPY modprobe.sh /usr/local/bin/modprobe
|
||||
# install required packages
|
||||
RUN set -eux; \
|
||||
apt-get update \
|
||||
&& apt-get install -y \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
# mercurial is requried for integration tests of the scm-hg-plugin
|
||||
mercurial \
|
||||
# git is required by yarn install of scm-ui
|
||||
git \
|
||||
# the following dependencies are required for cypress tests and are copied from
|
||||
# https://github.com/cypress-io/cypress-docker-images/blob/master/base/12.18.3/Dockerfile
|
||||
libgtk2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libnotify-dev \
|
||||
libgconf-2-4 \
|
||||
libgbm-dev \
|
||||
libnss3 \
|
||||
libxss1 \
|
||||
libasound2 \
|
||||
libxtst6 \
|
||||
xauth \
|
||||
xvfb \
|
||||
# download docker
|
||||
&& curl -o docker-${DOCKER_VERSION}.tgz https://download.docker.com/linux/static/${DOCKER_CHANNEL}/x86_64/docker-${DOCKER_VERSION}.tgz \
|
||||
&& echo "${DOCKER_CHECKSUM} docker-${DOCKER_VERSION}.tgz" > docker-${DOCKER_VERSION}.sha256sum \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
VERSION:=11.0.7_10
|
||||
VERSION:=11.0.8_10
|
||||
|
||||
.PHONY:build
|
||||
build:
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"build": "webpack --mode=production --config=scm-ui/ui-scripts/src/webpack.config.js",
|
||||
"build:dev": "webpack --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js",
|
||||
"test": "lerna run --scope '@scm-manager/ui-*' --scope '@scm-manager/eslint-config' test",
|
||||
"e2e-tests": "lerna run --scope '@scm-manager/e2e-tests' ci",
|
||||
"typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck",
|
||||
"serve": "NODE_ENV=development webpack-dev-server --hot --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js",
|
||||
"deploy": "ui-scripts publish",
|
||||
|
||||
@@ -67,6 +67,7 @@ public class AuthenticationFilter extends HttpFilter {
|
||||
*/
|
||||
private static final String ATTRIBUTE_FAILED_AUTH = "sonia.scm.auth.failed";
|
||||
|
||||
|
||||
private final Set<WebTokenGenerator> tokenGenerators;
|
||||
protected ScmConfiguration configuration;
|
||||
|
||||
@@ -117,7 +118,7 @@ public class AuthenticationFilter extends HttpFilter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends status code 403 back to client, if the authentication has failed.
|
||||
* Sends status code 401 back to client, if the authentication has failed.
|
||||
* In all other cases the method will send status code 403 back to client.
|
||||
*
|
||||
* @param request servlet request
|
||||
@@ -209,12 +210,8 @@ public class AuthenticationFilter extends HttpFilter {
|
||||
subject.login(token);
|
||||
processChain(request, response, chain, subject);
|
||||
} catch (TokenExpiredException ex) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("{} expired", token.getClass(), ex);
|
||||
} else {
|
||||
logger.debug("{} expired", token.getClass());
|
||||
}
|
||||
handleUnauthorized(request, response, chain);
|
||||
// Rethrow to be caught by TokenExpiredFilter
|
||||
throw ex;
|
||||
} catch (AuthenticationException ex) {
|
||||
logger.warn("authentication failed", ex);
|
||||
handleUnauthorized(request, response, chain);
|
||||
|
||||
@@ -186,6 +186,36 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>buildfrontend-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<workingDirectory>${basedir}/..</workingDirectory>
|
||||
<node>
|
||||
<version>${nodejs.version}</version>
|
||||
</node>
|
||||
<pkgManager>
|
||||
<type>YARN</type>
|
||||
<version>${yarn.version}</version>
|
||||
</pkgManager>
|
||||
<script>e2e-tests</script>
|
||||
<!--
|
||||
we want not fail the build,
|
||||
we want to mark it as unstable and record the test results
|
||||
-->
|
||||
<ignoreFailure>true</ignoreFailure>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>e2e</id>
|
||||
<phase>integration-test</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
@@ -284,7 +314,7 @@
|
||||
<properties>
|
||||
<scm.stage>DEVELOPMENT</scm.stage>
|
||||
<scm.home>${project.parent.build.directory}/scm-it</scm.home>
|
||||
<scm-it.logbackConfiguration>${project.basedir}/../scm-webapp/src/main/resources/logback.default.xml</scm-it.logbackConfiguration>
|
||||
<scm-it.logbackConfiguration>${project.basedir}/../scm-webapp/src/main/resources/logback.ci.xml</scm-it.logbackConfiguration>
|
||||
</properties>
|
||||
|
||||
</profile>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:8081/scm"
|
||||
"baseUrl": "http://localhost:8081/scm",
|
||||
"videoUploadOnPasses": false,
|
||||
"videoCompression": false
|
||||
}
|
||||
|
||||
2
scm-ui/e2e-tests/cypress/.gitignore
vendored
Normal file
2
scm-ui/e2e-tests/cypress/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
videos
|
||||
screenshots
|
||||
@@ -6,8 +6,16 @@
|
||||
"author": "Eduard Heimbuch <eduard.heimbuch@cloudogu.com>",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
"scripts": {
|
||||
"headless": "cypress run",
|
||||
"ci": "node src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg-installer/ffmpeg": "^1.0.20",
|
||||
"cypress": "^4.12.0",
|
||||
"fluent-ffmpeg": "^2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-cypress": "^2.11.1"
|
||||
},
|
||||
"prettier": "@scm-manager/prettier-config",
|
||||
|
||||
86
scm-ui/e2e-tests/src/index.js
Normal file
86
scm-ui/e2e-tests/src/index.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const cypress = require("cypress");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ffmpegPath = require("@ffmpeg-installer/ffmpeg").path;
|
||||
const ffmpeg = require("fluent-ffmpeg");
|
||||
|
||||
ffmpeg.setFfmpegPath(ffmpegPath);
|
||||
|
||||
const options = {
|
||||
reporter: "junit",
|
||||
reporterOptions: {
|
||||
mochaFile: path.join("..", "target", "cypress-reports", "TEST-[hash].xml")
|
||||
}
|
||||
};
|
||||
|
||||
const createOutputFile = test => {
|
||||
const title = test.title.join(" -- ");
|
||||
return path.join("cypress", "videos", `${title}.mp4`);
|
||||
};
|
||||
|
||||
const cutVideo = (video, test) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const title = createOutputFile(test);
|
||||
ffmpeg(video)
|
||||
.setStartTime(test.videoTimestamp / 1000)
|
||||
.setDuration(test.wallClockDuration / 1000)
|
||||
.output(title)
|
||||
.on("end", err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.on("error", err => {
|
||||
reject(err);
|
||||
})
|
||||
.run();
|
||||
});
|
||||
};
|
||||
|
||||
cypress
|
||||
.run(options)
|
||||
.then(results => {
|
||||
results.runs.forEach(run => {
|
||||
// remove videos of successful runs
|
||||
if (!run.shouldUploadVideo) {
|
||||
fs.unlinkSync(run.video);
|
||||
} else {
|
||||
const cuts = [];
|
||||
run.tests.forEach(test => {
|
||||
if (test.state !== "passed") {
|
||||
cuts.push(cutVideo(run.video, test));
|
||||
}
|
||||
});
|
||||
Promise.all(cuts)
|
||||
.then(() => fs.unlinkSync(run.video))
|
||||
.catch(err => console.error("failed to cut video", err));
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
@@ -23,7 +23,14 @@
|
||||
*/
|
||||
|
||||
import { contextPath } from "./urls";
|
||||
import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError, BackendErrorContent } from "./errors";
|
||||
import {
|
||||
createBackendError,
|
||||
ForbiddenError,
|
||||
isBackendError,
|
||||
UnauthorizedError,
|
||||
BackendErrorContent,
|
||||
TOKEN_EXPIRED_ERROR_CODE
|
||||
} from "./errors";
|
||||
|
||||
type SubscriptionEvent = {
|
||||
type: string;
|
||||
@@ -120,7 +127,13 @@ function handleFailure(response: Response) {
|
||||
if (!response.ok) {
|
||||
if (isBackendError(response)) {
|
||||
return response.json().then((content: BackendErrorContent) => {
|
||||
throw createBackendError(content, response.status);
|
||||
if (content.errorCode === TOKEN_EXPIRED_ERROR_CODE) {
|
||||
window.location.replace(`${contextPath}/login`);
|
||||
// Throw error because if redirect is not instantaneous, we want to display something senseful
|
||||
throw new UnauthorizedError("Unauthorized", 401);
|
||||
} else {
|
||||
throw createBackendError(content, response.status);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (response.status === 401) {
|
||||
|
||||
@@ -25,31 +25,37 @@
|
||||
import { createAttributesForTesting, isDevBuild } from "./devBuild";
|
||||
|
||||
describe("devbuild tests", () => {
|
||||
let env: string | undefined;
|
||||
let stage: string | undefined;
|
||||
|
||||
const setStage = (s?: string) => {
|
||||
// @ts-ignore scmStage is set on the index page
|
||||
window.scmStage = s;
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
env = process.env.NODE_ENV;
|
||||
// @ts-ignore scmStage is set on the index page
|
||||
stage = window.scmStage;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.NODE_ENV = env;
|
||||
setStage(stage);
|
||||
});
|
||||
|
||||
describe("isDevBuild tests", () => {
|
||||
it("should return true for development", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
setStage("development");
|
||||
expect(isDevBuild()).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for production", () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
setStage("production");
|
||||
expect(isDevBuild()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAttributesForTesting in non development mode", () => {
|
||||
beforeAll(() => {
|
||||
process.env.NODE_ENV = "production";
|
||||
setStage("production");
|
||||
});
|
||||
|
||||
it("should return undefined for non development", () => {
|
||||
@@ -60,7 +66,7 @@ describe("devbuild tests", () => {
|
||||
|
||||
describe("createAttributesForTesting in development mode", () => {
|
||||
beforeAll(() => {
|
||||
process.env.NODE_ENV = "development";
|
||||
setStage("development");
|
||||
});
|
||||
|
||||
it("should return undefined for non development", () => {
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
export const isDevBuild = () => process.env.NODE_ENV === "development";
|
||||
// @ts-ignore scmStage is set on the index page
|
||||
export const isDevBuild = () => (window.scmStage || "").toUpperCase() === "DEVELOPMENT";
|
||||
|
||||
export const createAttributesForTesting = (testId?: string) => {
|
||||
if (!testId || !isDevBuild()) {
|
||||
|
||||
@@ -103,3 +103,5 @@ export function createBackendError(content: BackendErrorContent, statusCode: num
|
||||
export function isBackendError(response: Response) {
|
||||
return response.headers.get("Content-Type") === "application/vnd.scmm-error+json;v=2";
|
||||
}
|
||||
|
||||
export const TOKEN_EXPIRED_ERROR_CODE = "DDS8D8unr1";
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
const path = require("path");
|
||||
const createIndexMiddleware = require("./middleware/IndexMiddleware");
|
||||
const createContextPathMiddleware = require("./middleware/ContextPathMiddleware");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||
const WorkerPlugin = require("worker-plugin");
|
||||
|
||||
const createIndexMiddleware = require("./middleware/IndexMiddleware");
|
||||
const createContextPathMiddleware = require("./middleware/ContextPathMiddleware");
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
const root = path.resolve(process.cwd(), "scm-ui");
|
||||
|
||||
@@ -39,6 +40,8 @@ let mode = "production";
|
||||
if (isDevelopment) {
|
||||
mode = "development";
|
||||
babelPlugins.push(require.resolve("react-refresh/babel"));
|
||||
// it is ok to use require here, because we want to load the package conditionally
|
||||
// eslint-disable-next-line global-require
|
||||
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
|
||||
webpackPlugins.push(new ReactRefreshWebpackPlugin());
|
||||
}
|
||||
@@ -113,13 +116,15 @@ module.exports = [
|
||||
historyApiFallback: true,
|
||||
overlay: true,
|
||||
port: 3000,
|
||||
before: function(app) {
|
||||
before: app => {
|
||||
app.use(createContextPathMiddleware("/scm"));
|
||||
},
|
||||
after: function(app) {
|
||||
after: app => {
|
||||
const templatePath = path.join(root, "ui-webapp", "public", "index.mustache");
|
||||
const stage = process.env.NODE_ENV || "DEVELOPMENT";
|
||||
const renderParams = {
|
||||
contextPath: "/scm"
|
||||
contextPath: "/scm",
|
||||
scmStage: stage.toUpperCase()
|
||||
};
|
||||
app.use(createIndexMiddleware(templatePath, renderParams));
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
-->
|
||||
<script>
|
||||
window.ctxPath = "{{ contextPath }}";
|
||||
window.scmStage = "{{ scmStage }}";
|
||||
</script>
|
||||
<script src="{{ contextPath }}/assets/runtime.bundle.js"></script>
|
||||
<script src="{{ contextPath }}/assets/vendors~webapp.bundle.js"></script>
|
||||
|
||||
@@ -36,9 +36,20 @@ type Props = {
|
||||
};
|
||||
|
||||
const DangerZoneContainer = styled.div`
|
||||
padding: 1rem;
|
||||
padding: 1.5rem 1rem;
|
||||
border: 1px solid #ff6a88;
|
||||
border-radius: 5px;
|
||||
> .level {
|
||||
flex-flow: wrap;
|
||||
|
||||
.level-left {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.level-right {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
> *:not(:last-child) {
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: solid 2px whitesmoke;
|
||||
|
||||
@@ -26,9 +26,8 @@ import { connect } from "react-redux";
|
||||
import { compose } from "redux";
|
||||
import { RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { History } from "history";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { confirmAlert, DeleteButton, ErrorNotification, Level, ButtonGroup } from "@scm-manager/ui-components";
|
||||
import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
|
||||
import { deleteRepo, getDeleteRepoFailure, isDeleteRepoPending } from "../modules/repos";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
@@ -89,10 +88,11 @@ class DeleteRepo extends React.Component<Props> {
|
||||
<ErrorNotification error={error} />
|
||||
<Level
|
||||
left={
|
||||
<div>
|
||||
<p>
|
||||
<strong>{t("deleteRepo.subtitle")}</strong>
|
||||
<p>{t("deleteRepo.description")}</p>
|
||||
</div>
|
||||
<br />
|
||||
{t("deleteRepo.description")}
|
||||
</p>
|
||||
}
|
||||
right={<DeleteButton label={t("deleteRepo.button")} action={action} loading={loading} />}
|
||||
/>
|
||||
|
||||
@@ -38,7 +38,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const RenameRepository: FC<Props> = ({ repository, indexLinks }) => {
|
||||
let history = useHistory();
|
||||
const history = useHistory();
|
||||
const [t] = useTranslation("repos");
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -156,10 +156,11 @@ const RenameRepository: FC<Props> = ({ repository, indexLinks }) => {
|
||||
/>
|
||||
<Level
|
||||
left={
|
||||
<div>
|
||||
<p>
|
||||
<strong>{t("renameRepo.subtitle")}</strong>
|
||||
<p>{t("renameRepo.description")}</p>
|
||||
</div>
|
||||
<br />
|
||||
{t("renameRepo.description")}
|
||||
</p>
|
||||
}
|
||||
right={
|
||||
<Button
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
@@ -46,15 +46,17 @@ public class TemplatingPushStateDispatcher implements PushStateDispatcher {
|
||||
static final String TEMPLATE = "/index.mustache";
|
||||
|
||||
private final TemplateEngine templateEngine;
|
||||
private final SCMContextProvider context;
|
||||
|
||||
@Inject
|
||||
public TemplatingPushStateDispatcher(TemplateEngineFactory templateEngineFactory) {
|
||||
this(templateEngineFactory.getDefaultEngine());
|
||||
public TemplatingPushStateDispatcher(TemplateEngineFactory templateEngineFactory, SCMContextProvider context) {
|
||||
this(templateEngineFactory.getDefaultEngine(), context);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
TemplatingPushStateDispatcher(TemplateEngine templateEngine) {
|
||||
TemplatingPushStateDispatcher(TemplateEngine templateEngine, SCMContextProvider context) {
|
||||
this.templateEngine = templateEngine;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,7 +66,7 @@ public class TemplatingPushStateDispatcher implements PushStateDispatcher {
|
||||
|
||||
Template template = templateEngine.getTemplate(TEMPLATE);
|
||||
try (Writer writer = response.getWriter()) {
|
||||
template.execute(writer, new IndexHtmlModel(request));
|
||||
template.execute(writer, new IndexHtmlModel(request, context.getStage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,15 +74,21 @@ public class TemplatingPushStateDispatcher implements PushStateDispatcher {
|
||||
static class IndexHtmlModel {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
private final Stage scmStage;
|
||||
|
||||
private IndexHtmlModel(HttpServletRequest request) {
|
||||
private IndexHtmlModel(HttpServletRequest request, Stage scmStage) {
|
||||
this.request = request;
|
||||
this.scmStage = scmStage;
|
||||
}
|
||||
|
||||
public String getContextPath() {
|
||||
return request.getContextPath();
|
||||
}
|
||||
|
||||
public String getScmStage() {
|
||||
return scmStage.name();
|
||||
}
|
||||
|
||||
public String getLiveReloadURL() {
|
||||
return System.getProperty("livereload.url");
|
||||
}
|
||||
|
||||
@@ -28,8 +28,11 @@ package sonia.scm.lifecycle.modules;
|
||||
|
||||
import com.google.inject.name.Names;
|
||||
|
||||
import org.apache.shiro.authc.Authenticator;
|
||||
import org.apache.shiro.authc.credential.DefaultPasswordService;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import org.apache.shiro.authc.pam.AuthenticationStrategy;
|
||||
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
|
||||
import org.apache.shiro.crypto.hash.DefaultHashService;
|
||||
import org.apache.shiro.guice.web.ShiroWebModule;
|
||||
import org.apache.shiro.realm.Realm;
|
||||
@@ -44,6 +47,7 @@ import sonia.scm.plugin.ExtensionProcessor;
|
||||
import javax.servlet.ServletContext;
|
||||
import org.apache.shiro.mgt.RememberMeManager;
|
||||
import sonia.scm.security.DisabledRememberMeManager;
|
||||
import sonia.scm.security.ScmAtLeastOneSuccessfulStrategy;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -94,6 +98,11 @@ public class ScmSecurityModule extends ShiroWebModule
|
||||
// disable remember me cookie generation
|
||||
bind(RememberMeManager.class).to(DisabledRememberMeManager.class);
|
||||
|
||||
// bind authentication strategy
|
||||
bind(ModularRealmAuthenticator.class);
|
||||
bind(Authenticator.class).to(ModularRealmAuthenticator.class);
|
||||
bind(AuthenticationStrategy.class).to(ScmAtLeastOneSuccessfulStrategy.class);
|
||||
|
||||
// bind realm
|
||||
for (Class<? extends Realm> realm : extensionProcessor.byExtensionPoint(Realm.class))
|
||||
{
|
||||
|
||||
@@ -51,6 +51,10 @@ public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIs
|
||||
* the logger for DefaultAccessTokenCookieIssuer
|
||||
*/
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultAccessTokenCookieIssuer.class);
|
||||
|
||||
private static final int DEFAULT_COOKIE_EXPIRATION_AMOUNT = 365;
|
||||
private static final TimeUnit DEFAULT_COOKIE_EXPIRATION_UNIT = TimeUnit.DAYS;
|
||||
private static final int DEFAULT_COOKIE_EXPIRATION = (int) TimeUnit.SECONDS.convert(DEFAULT_COOKIE_EXPIRATION_AMOUNT, DEFAULT_COOKIE_EXPIRATION_UNIT);
|
||||
|
||||
private final ScmConfiguration configuration;
|
||||
|
||||
@@ -75,7 +79,7 @@ public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIs
|
||||
LOG.trace("create and attach cookie for access token {}", accessToken.getId());
|
||||
Cookie c = new Cookie(HttpUtil.COOKIE_BEARER_AUTHENTICATION, accessToken.compact());
|
||||
c.setPath(contextPath(request));
|
||||
c.setMaxAge(getMaxAge(accessToken));
|
||||
c.setMaxAge(DEFAULT_COOKIE_EXPIRATION);
|
||||
c.setHttpOnly(isHttpOnly());
|
||||
c.setSecure(isSecure(request));
|
||||
|
||||
@@ -111,11 +115,6 @@ public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIs
|
||||
return contextPath;
|
||||
}
|
||||
|
||||
private int getMaxAge(AccessToken accessToken){
|
||||
long maxAgeMs = accessToken.getExpiration().getTime() - new Date().getTime();
|
||||
return (int) TimeUnit.MILLISECONDS.toSeconds(maxAgeMs);
|
||||
}
|
||||
|
||||
private boolean isSecure(HttpServletRequest request){
|
||||
boolean secure = request.isSecure();
|
||||
if (!secure) {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.security;
|
||||
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.pam.AbstractAuthenticationStrategy;
|
||||
import org.apache.shiro.realm.Realm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ScmAtLeastOneSuccessfulStrategy extends AbstractAuthenticationStrategy {
|
||||
|
||||
final ThreadLocal<List<Throwable>> threadLocal = new ThreadLocal<>();
|
||||
|
||||
@Override
|
||||
public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
|
||||
this.threadLocal.set(new ArrayList<>());
|
||||
return super.beforeAllAttempts(realms, token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException {
|
||||
if (t != null) {
|
||||
this.threadLocal.get().add(t);
|
||||
}
|
||||
return super.afterAttempt(realm, token, singleRealmInfo, aggregateInfo, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
|
||||
final List<Throwable> throwables = threadLocal.get();
|
||||
threadLocal.remove();
|
||||
if (isAuthenticationSuccessful(aggregate)) {
|
||||
return aggregate;
|
||||
}
|
||||
Optional<TokenExpiredException> tokenExpiredException = findTokenExpiredException(throwables);
|
||||
|
||||
if (tokenExpiredException.isPresent()) {
|
||||
throw tokenExpiredException.get();
|
||||
} else {
|
||||
throw createAuthenticationException(token);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAuthenticationSuccessful(AuthenticationInfo aggregate) {
|
||||
return aggregate != null && isNotEmpty(aggregate.getPrincipals());
|
||||
}
|
||||
|
||||
private static boolean isNotEmpty(PrincipalCollection pc) {
|
||||
return pc != null && !pc.isEmpty();
|
||||
}
|
||||
|
||||
private static Optional<TokenExpiredException> findTokenExpiredException(List<Throwable> throwables) {
|
||||
return throwables.stream().filter(t -> t instanceof TokenExpiredException).findFirst().map(t -> (TokenExpiredException) t);
|
||||
}
|
||||
|
||||
private static AuthenticationException createAuthenticationException(AuthenticationToken token) {
|
||||
return new AuthenticationException("Authentication token of type [" + token.getClass() + "] " +
|
||||
"could not be authenticated by any configured realms. Please ensure that at least one realm can " +
|
||||
"authenticate these tokens.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.security;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.MDC;
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.api.v2.resources.ErrorDto;
|
||||
import sonia.scm.filter.Filters;
|
||||
import sonia.scm.filter.WebElement;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
import sonia.scm.web.filter.HttpFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
@WebElement("/*")
|
||||
@Priority(Filters.PRIORITY_PRE_AUTHENTICATION)
|
||||
@Singleton
|
||||
public class TokenExpiredFilter extends HttpFilter {
|
||||
static final String TOKEN_EXPIRED_ERROR_CODE = "DDS8D8unr1";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TokenExpiredFilter.class);
|
||||
|
||||
private final AccessTokenCookieIssuer accessTokenCookieIssuer;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Inject
|
||||
public TokenExpiredFilter(AccessTokenCookieIssuer accessTokenCookieIssuer, ObjectMapper objectMapper) {
|
||||
this.accessTokenCookieIssuer = accessTokenCookieIssuer;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
try {
|
||||
chain.doFilter(request, response);
|
||||
} catch (TokenExpiredException ex) {
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("Token expired", ex);
|
||||
} else {
|
||||
LOG.debug("Token expired");
|
||||
}
|
||||
handleTokenExpired(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleTokenExpired(HttpServletRequest request,
|
||||
HttpServletResponse response) throws IOException {
|
||||
accessTokenCookieIssuer.invalidate(request, response);
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType(VndMediaType.ERROR_TYPE);
|
||||
final ErrorDto errorDto = new ErrorDto();
|
||||
errorDto.setMessage("Token Expired");
|
||||
errorDto.setErrorCode(TOKEN_EXPIRED_ERROR_CODE);
|
||||
errorDto.setTransactionId(MDC.get("transaction_id"));
|
||||
try (ServletOutputStream stream = response.getOutputStream()) {
|
||||
objectMapper.writeValue(stream, errorDto);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm;
|
||||
|
||||
import com.google.inject.util.Providers;
|
||||
@@ -39,8 +39,11 @@ public class PushStateDispatcherProviderTest {
|
||||
@Mock
|
||||
private TemplateEngine templateEngine;
|
||||
|
||||
@Mock
|
||||
private SCMContextProvider context;
|
||||
|
||||
private PushStateDispatcherProvider provider = new PushStateDispatcherProvider(
|
||||
Providers.of(new TemplatingPushStateDispatcher(templateEngine))
|
||||
Providers.of(new TemplatingPushStateDispatcher(templateEngine, context))
|
||||
);
|
||||
|
||||
@Test
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm;
|
||||
|
||||
import org.junit.Before;
|
||||
@@ -61,18 +61,24 @@ public class TemplatingPushStateDispatcherTest {
|
||||
@Mock
|
||||
private Template template;
|
||||
|
||||
@Mock
|
||||
private SCMContextProvider context;
|
||||
|
||||
private TemplatingPushStateDispatcher dispatcher;
|
||||
|
||||
@Before
|
||||
public void setUpMocks() {
|
||||
dispatcher = new TemplatingPushStateDispatcher(templateEngine);
|
||||
dispatcher = new TemplatingPushStateDispatcher(templateEngine, context);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDispatch() throws IOException {
|
||||
when(context.getStage()).thenReturn(Stage.DEVELOPMENT);
|
||||
|
||||
TemplatingPushStateDispatcher.IndexHtmlModel model = dispatch();
|
||||
assertEquals("/scm", model.getContextPath());
|
||||
assertNull(model.getLiveReloadURL());
|
||||
assertEquals("DEVELOPMENT", model.getScmStage());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -139,7 +139,6 @@ public class AuthenticationResourceTest {
|
||||
dispatcher.addSingletonResource(authenticationResource);
|
||||
|
||||
AccessToken accessToken = mock(AccessToken.class);
|
||||
when(accessToken.getExpiration()).thenReturn(new Date(Long.MAX_VALUE));
|
||||
when(accessTokenBuilder.build()).thenReturn(accessToken);
|
||||
|
||||
when(accessTokenBuilderFactory.create()).thenReturn(accessTokenBuilder);
|
||||
|
||||
@@ -121,8 +121,6 @@ public class DefaultAccessTokenCookieIssuerTest {
|
||||
}
|
||||
|
||||
private Cookie authenticate() {
|
||||
when(accessToken.getExpiration()).thenReturn(new Date());
|
||||
|
||||
issuer.authenticate(request, response, accessToken);
|
||||
|
||||
verify(response).addCookie(cookieArgumentCaptor.capture());
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.security;
|
||||
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.MergableAuthenticationInfo;
|
||||
import org.apache.shiro.realm.Realm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ScmAtLeastOneSuccessfulStrategyTest {
|
||||
|
||||
@Mock
|
||||
private Realm realm;
|
||||
|
||||
@Mock
|
||||
private AuthenticationToken token;
|
||||
|
||||
@Mock
|
||||
MergableAuthenticationInfo singleRealmInfo;
|
||||
|
||||
@Mock
|
||||
MergableAuthenticationInfo aggregateInfo;
|
||||
|
||||
@Mock
|
||||
TokenExpiredException tokenExpiredException;
|
||||
|
||||
@Mock
|
||||
AuthenticationException authenticationException;
|
||||
|
||||
@Mock
|
||||
PrincipalCollection principalCollection;
|
||||
|
||||
@Test
|
||||
public void shouldAddNonNullThrowableToList() {
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy();
|
||||
strategy.threadLocal.set(new ArrayList<>());
|
||||
|
||||
strategy.afterAttempt(realm, token, singleRealmInfo, aggregateInfo, tokenExpiredException);
|
||||
|
||||
assertThat(strategy.threadLocal.get()).hasSize(1);
|
||||
assertThat(strategy.threadLocal.get().get(0)).isEqualTo(tokenExpiredException);
|
||||
}
|
||||
|
||||
@Test(expected = TokenExpiredException.class)
|
||||
public void shouldRethrowException() {
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy();
|
||||
strategy.threadLocal.set(singletonList(tokenExpiredException));
|
||||
|
||||
strategy.afterAllAttempts(token, aggregateInfo);
|
||||
}
|
||||
|
||||
@Test(expected = AuthenticationException.class)
|
||||
public void shouldThrowGenericErrorIfNonTokenExpiredExceptionWasCaught() {
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy();
|
||||
strategy.threadLocal.set(singletonList(authenticationException));
|
||||
|
||||
strategy.afterAllAttempts(token, aggregateInfo);
|
||||
}
|
||||
|
||||
@Test()
|
||||
public void shouldNotRethrowExceptionIfAuthenticationSuccessful() {
|
||||
final ScmAtLeastOneSuccessfulStrategy strategy = new ScmAtLeastOneSuccessfulStrategy();
|
||||
strategy.threadLocal.set(singletonList(tokenExpiredException));
|
||||
when(aggregateInfo.getPrincipals()).thenReturn(principalCollection);
|
||||
when(principalCollection.isEmpty()).thenReturn(false);
|
||||
|
||||
final AuthenticationInfo authenticationInfo = strategy.afterAllAttempts(token, aggregateInfo);
|
||||
|
||||
assertThat(authenticationInfo).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.security;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.api.v2.resources.ErrorDto;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static sonia.scm.security.TokenExpiredFilter.TOKEN_EXPIRED_ERROR_CODE;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class TokenExpiredFilterTest {
|
||||
|
||||
@Mock
|
||||
private FilterChain chain;
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
@Mock
|
||||
private HttpServletResponse response;
|
||||
@Mock
|
||||
private ObjectMapper objectMapper;
|
||||
@Mock
|
||||
private AccessTokenCookieIssuer accessTokenCookieIssuer;
|
||||
|
||||
@Test
|
||||
public void shouldReturnSpecificErrorResponseAndInvalidateCookie() throws IOException, ServletException {
|
||||
final TokenExpiredFilter filter = new TokenExpiredFilter(accessTokenCookieIssuer, objectMapper);
|
||||
doThrow(TokenExpiredException.class).when(chain).doFilter(request, response);
|
||||
|
||||
filter.doFilter(request, response, chain);
|
||||
|
||||
verify(chain, atLeastOnce()).doFilter(request, response);
|
||||
verify(accessTokenCookieIssuer, atLeastOnce()).invalidate(request, response);
|
||||
verify(response, atLeastOnce()).setContentType(VndMediaType.ERROR_TYPE);
|
||||
verify(objectMapper).writeValue((ServletOutputStream) any(), argThat((ErrorDto errorDto) -> {
|
||||
assertEquals(TOKEN_EXPIRED_ERROR_CODE, errorDto.getErrorCode());
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
97
yarn.lock
97
yarn.lock
@@ -1337,6 +1337,54 @@
|
||||
unique-filename "^1.1.1"
|
||||
which "^1.3.1"
|
||||
|
||||
"@ffmpeg-installer/darwin-x64@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz#48e1706c690e628148482bfb64acb67472089aaa"
|
||||
integrity sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==
|
||||
|
||||
"@ffmpeg-installer/ffmpeg@^1.0.20":
|
||||
version "1.0.20"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.0.20.tgz#d3c9c2bbcd76149468fb0886c2b3fe9e4795490b"
|
||||
integrity sha512-wbgd//6OdwbFXYgV68ZyKrIcozEQpUKlvV66XHaqO2h3sFbX0jYLzx62Q0v8UcFWN21LoxT98NU2P+K0OWsKNA==
|
||||
optionalDependencies:
|
||||
"@ffmpeg-installer/darwin-x64" "4.1.0"
|
||||
"@ffmpeg-installer/linux-arm" "4.1.3"
|
||||
"@ffmpeg-installer/linux-arm64" "4.1.4"
|
||||
"@ffmpeg-installer/linux-ia32" "4.1.0"
|
||||
"@ffmpeg-installer/linux-x64" "4.1.0"
|
||||
"@ffmpeg-installer/win32-ia32" "4.1.0"
|
||||
"@ffmpeg-installer/win32-x64" "4.1.0"
|
||||
|
||||
"@ffmpeg-installer/linux-arm64@4.1.4":
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz#7219f3f901bb67f7926cb060b56b6974a6cad29f"
|
||||
integrity sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==
|
||||
|
||||
"@ffmpeg-installer/linux-arm@4.1.3":
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz#c554f105ed5f10475ec25d7bec94926ce18db4c1"
|
||||
integrity sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==
|
||||
|
||||
"@ffmpeg-installer/linux-ia32@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz#adad70b0d0d9d8d813983d6e683c5a338a75e442"
|
||||
integrity sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==
|
||||
|
||||
"@ffmpeg-installer/linux-x64@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz#b4a5d89c4e12e6d9306dbcdc573df716ec1c4323"
|
||||
integrity sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==
|
||||
|
||||
"@ffmpeg-installer/win32-ia32@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz#6eac4fb691b64c02e7a116c1e2d167f3e9b40638"
|
||||
integrity sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==
|
||||
|
||||
"@ffmpeg-installer/win32-x64@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz#17e8699b5798d4c60e36e2d6326a8ebe5e95a2c5"
|
||||
integrity sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==
|
||||
|
||||
"@fortawesome/fontawesome-free@^5.11.2":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9"
|
||||
@@ -2482,17 +2530,17 @@
|
||||
dependencies:
|
||||
"@types/node" ">= 8"
|
||||
|
||||
"@pmmmwh/react-refresh-webpack-plugin@^0.3.0":
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.3.3.tgz#40a3d674f42a011b7f30a9609aa8fb68ec3c39c9"
|
||||
integrity sha512-uc6FmPEegAZawSHjUMFQwU7EjaDn7zy1iD/KD/wBROL9F4378OES8MKMYHoRAKT61Fk7LxVKZSDR5VespMQiqw==
|
||||
"@pmmmwh/react-refresh-webpack-plugin@^0.4.0":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.1.tgz#a4db0ed8e58c2f8566161c9a8cdf1d095c9a891b"
|
||||
integrity sha512-MzM87WdX2r2KRFfhEho7oGyK1XRE/J9WwjB3v6oLQHN0dzBypBZxSWjnoYx+RWneRCsg8Sin1myf+EjX1fqIbQ==
|
||||
dependencies:
|
||||
ansi-html "^0.0.7"
|
||||
error-stack-parser "^2.0.6"
|
||||
html-entities "^1.2.1"
|
||||
lodash.debounce "^4.0.8"
|
||||
native-url "^0.2.6"
|
||||
schema-utils "^2.6.5"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@reach/router@^1.2.1":
|
||||
version "1.3.3"
|
||||
@@ -4175,6 +4223,11 @@ async-limiter@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
|
||||
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
|
||||
|
||||
async@>=0.2.9, async@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||
|
||||
async@^2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||
@@ -4182,11 +4235,6 @@ async@^2.6.2:
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
async@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
@@ -4239,11 +4287,6 @@ babel-code-frame@^6.22.0:
|
||||
esutils "^2.0.2"
|
||||
js-tokens "^3.0.2"
|
||||
|
||||
babel-core@7.0.0-bridge.0:
|
||||
version "7.0.0-bridge.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece"
|
||||
integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==
|
||||
|
||||
babel-eslint@^10.0.3:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
|
||||
@@ -7743,7 +7786,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
fault@^1.0.0:
|
||||
fault@^1.0.0, fault@^1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
|
||||
integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
|
||||
@@ -7978,6 +8021,14 @@ flatted@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
|
||||
integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
|
||||
|
||||
fluent-ffmpeg@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz#c952de2240f812ebda0aa8006d7776ee2acf7d74"
|
||||
integrity sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=
|
||||
dependencies:
|
||||
async ">=0.2.9"
|
||||
which "^1.1.1"
|
||||
|
||||
flush-write-stream@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
|
||||
@@ -8311,7 +8362,7 @@ gitconfiglocal@^1.0.0:
|
||||
dependencies:
|
||||
ini "^1.3.2"
|
||||
|
||||
gitdiff-parser@^0.1.2, "gitdiff-parser@https://github.com/scm-manager/gitdiff-parser#617747460280bf4522bb84d217a9064ac8eb6d3d":
|
||||
gitdiff-parser@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://github.com/scm-manager/gitdiff-parser#617747460280bf4522bb84d217a9064ac8eb6d3d"
|
||||
|
||||
@@ -11299,7 +11350,7 @@ lower-case@^2.0.1:
|
||||
dependencies:
|
||||
tslib "^1.10.0"
|
||||
|
||||
lowlight@1.13.1, lowlight@^1.13.0, lowlight@~1.11.0:
|
||||
lowlight@^1.13.0:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.13.1.tgz#c4f0e03906ebd23fedf2d258f6ab2f6324cf90eb"
|
||||
integrity sha512-kQ71/T6RksEVz9AlPq07/2m+SU/1kGvt9k39UtvHX760u4SaWakaYH7hYgH5n6sTsCWk4MVYzUzLU59aN5CSmQ==
|
||||
@@ -11307,6 +11358,14 @@ lowlight@1.13.1, lowlight@^1.13.0, lowlight@~1.11.0:
|
||||
fault "^1.0.0"
|
||||
highlight.js "~9.16.0"
|
||||
|
||||
lowlight@~1.11.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc"
|
||||
integrity sha512-xrGGN6XLL7MbTMdPD6NfWPwY43SNkjf/d0mecSx/CW36fUZTjRHEq0/Cdug3TWKtRXLWi7iMl1eP0olYxj/a4A==
|
||||
dependencies:
|
||||
fault "^1.0.2"
|
||||
highlight.js "~9.13.0"
|
||||
|
||||
lru-cache@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||
@@ -17044,7 +17103,7 @@ which-pm-runs@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
|
||||
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
|
||||
|
||||
which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
|
||||
which@^1.1.1, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
|
||||
|
||||
Reference in New Issue
Block a user