From 19930804a07415e74498d3202fc449c0d1b37332 Mon Sep 17 00:00:00 2001 From: Viktor Egorov Date: Tue, 25 Feb 2025 13:16:27 +0100 Subject: [PATCH] Add Configuration to JWT lifetime length --- docs/de/user/admin/settings.md | 6 ++ docs/en/user/admin/settings.md | 6 ++ gradle/changelog/jwt_settings.yaml | 2 + .../sonia/scm/config/ScmConfiguration.java | 58 +++++++++++++++++ .../sonia/scm/it/AnonymousAccessITCase.java | 2 + .../java/sonia/scm/it/utils/TestData.java | 4 +- scm-ui/e2e-tests/package.json | 4 +- scm-ui/ui-api/src/config.test.ts | 2 + scm-ui/ui-components/src/forms/InputField.tsx | 4 ++ scm-ui/ui-types/src/Config.ts | 2 + .../ui-webapp/public/locales/de/config.json | 7 ++ .../ui-webapp/public/locales/en/config.json | 7 ++ .../src/admin/components/form/ConfigForm.tsx | 11 ++++ .../src/admin/components/form/JwtSettings.tsx | 64 +++++++++++++++++++ .../sonia/scm/api/v2/resources/ConfigDto.java | 2 + .../scm/api/v2/resources/ConfigResource.java | 9 ++- .../sonia/scm/config/SecureKeyService.java | 35 ++++++++++ .../scm/security/JwtAccessTokenBuilder.java | 13 ++-- .../JwtAccessTokenBuilderFactory.java | 11 ++-- .../java/sonia/scm/security/JwtConfig.java | 2 +- .../sonia/scm/security/SecureKeyResolver.java | 3 + ...ConfigDtoToScmConfigurationMapperTest.java | 4 ++ .../api/v2/resources/ConfigResourceTest.java | 23 +++++-- ...ScmConfigurationToConfigDtoMapperTest.java | 4 ++ .../scm/config/SecureKeyServiceTest.java | 44 +++++++++++++ .../security/JwtAccessTokenBuilderTest.java | 36 +++++++++-- .../security/JwtAccessTokenRefresherTest.java | 19 +++++- ...tageJwtAccessTokenRefreshStrategyTest.java | 22 ++++--- .../scm/security/SecureKeyResolverTest.java | 58 ++++++++--------- yarn.lock | 8 +-- 30 files changed, 400 insertions(+), 72 deletions(-) create mode 100644 gradle/changelog/jwt_settings.yaml create mode 100644 scm-ui/ui-webapp/src/admin/components/form/JwtSettings.tsx create mode 100644 scm-webapp/src/main/java/sonia/scm/config/SecureKeyService.java create mode 100644 scm-webapp/src/test/java/sonia/scm/config/SecureKeyServiceTest.java diff --git a/docs/de/user/admin/settings.md b/docs/de/user/admin/settings.md index c3437e3309..4105744c03 100644 --- a/docs/de/user/admin/settings.md +++ b/docs/de/user/admin/settings.md @@ -38,6 +38,12 @@ So können über das Plugin-Center besondere cloudogu platform-Plugins bezogen w Eine bestehende Verbindung zwischen dem SCM-Manager und der cloudogu platform kann hier aufgehoben werden. ![Einstellungen, Plugin-Center mit der cloudogu platform verbunden, Button zum Lösen der Verbindung](assets/administration-settings-connected.png) +### JWT Einstellungen +Benutzer erhalten einen JWT als Authentifizierungstoken nach einem erfolgreichen login. +Administratoren können die Lebensdauer dieser JWTs konfigurieren. +Falls die Lebensdauer verringert wird, wird jeder bisher ausgestellter JWT ungültig. +Sollte in der `config.yml` des Servers die Option "endless JWT" aktiviert sein, dann wird diese Einstellung ignoriert. + #### Anonyme Zugriff Der SCM-Manager 2 hat das Konzept für anonyme Zugriffe über einen "_anonymous"-Benutzer realisiert. Beim Aktivieren des anonymen Zugriffs wird ein neuer Benutzer erstellt mit dem Namen "_anonymous". Dieser Nutzer kann wie ein gewöhnlicher Benutzer für unterschiedliche Aktionen berechtigt werden. Bei einem Zugriff auf den SCM-Manager ohne Zugangsdaten wird dieser anonyme Benutzer verwendet. Ist der anonyme Zugriff nur für Protokoll aktiviert, können die REST API und die VCS Protokolle anonym genutzt werden. Wurde der anonyme Zugriff vollständig aktiviert, ist auch ein Zugriff über den Webclient anonym möglich. diff --git a/docs/en/user/admin/settings.md b/docs/en/user/admin/settings.md index de35a1fad5..cc6131e574 100644 --- a/docs/en/user/admin/settings.md +++ b/docs/en/user/admin/settings.md @@ -37,6 +37,12 @@ Plugin Center Authentication URL: https://plugin-center-api.scm-manager.org/api/ An existing connection between a SCM-Manager and the cloudogu platform may be severed here. ![Plugin center settings, button sever connection to the cloudogu platform](assets/administration-settings-connected.png) +#### JWT settings +Users receive a JWT as an authentication token, after a successful login. +Administrators can configure the amount of hours until a JWT expires. +If the amount of hours get reduced, each created JWT will be invalidated. +This setting will be ignored, if the endless JWT option is set to true in the server `config.yml`. + #### Anonymous Access In SCM-Manager 2 the access for anonymous access is realized by using an "_anonymous" user. When the feature is activated, a new user with the name "_anonymous" is created. This user can be authorized just like any other user. This user is used for access to SCM-Manager without login credentials. If the anonymous mode is protocol only you may access the SCM-Manager via the REST API and VCS protocols. With fully enabled anonymous access you can also use the webclient without credentials. diff --git a/gradle/changelog/jwt_settings.yaml b/gradle/changelog/jwt_settings.yaml new file mode 100644 index 0000000000..6684af7a7e --- /dev/null +++ b/gradle/changelog/jwt_settings.yaml @@ -0,0 +1,2 @@ +- type: added + description: JWT expiration time in general settings diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index e6ac897a3f..aa9d28698b 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -229,6 +229,22 @@ public class ScmConfiguration implements Configuration { @XmlElement(name = "mail-domain-name") private String mailDomainName = DEFAULT_MAIL_DOMAIN_NAME; + /** + * Time in hours for jwt expiration. + * + * @since 3.8.0 + */ + @XmlElement(name = "jwt-expiration-time") + private int jwtExpirationInH = 1; + + /** + * Enables endless jwt. + * + * @since 3.8.0 + */ + @XmlElement(name = "jwt-expiration-endless") + private boolean enabledJwtEndless = false; + /** * List of users that will be notified of administrative incidents. * @@ -278,6 +294,8 @@ public class ScmConfiguration implements Configuration { this.emergencyContacts = other.emergencyContacts; this.enabledUserConverter = other.enabledUserConverter; this.enabledApiKeys = other.enabledApiKeys; + this.jwtExpirationInH = other.jwtExpirationInH; + this.enabledJwtEndless = other.enabledJwtEndless; } /** @@ -448,6 +466,26 @@ public class ScmConfiguration implements Configuration { return anonymousMode; } + /** + * Returns Jwt expiration in {@code n} . + * + * @return Jwt expiration in {@code number} + * @since 3.8.0 + */ + public int getJwtExpirationInH() { + return jwtExpirationInH; + } + + /** + * Returns {@code true} if the cookie xsrf protection is enabled. + * + * @return {@code true} if the cookie xsrf protection is enabled + * @since 3.8.0 + */ + public boolean isJwtEndless() { + return enabledJwtEndless; + } + /** * Returns {@code true} if anonymous mode is enabled. * @@ -728,6 +766,26 @@ public class ScmConfiguration implements Configuration { this.enabledFileSearch = enabledFileSearch; } + /** + * Set {@code n} to configure jwt expiration time in hours + * + * @param jwtExpirationInH {@code n} to configure jwt expiration time in hours + * @since 3.8.0 + */ + public void setJwtExpirationInH(int jwtExpirationInH) { + this.jwtExpirationInH = jwtExpirationInH; + } + + /** + * Set {@code true} to enable endless jwt. + * + * @param enabledJwtEndless {@code true} to enable endless jwt. + * @since 2.45.0 + */ + public void setEnabledJwtExpiration(boolean enabledJwtEndless) { + this.enabledJwtEndless = enabledJwtEndless; + } + public void setNamespaceStrategy(String namespaceStrategy) { this.namespaceStrategy = namespaceStrategy; } diff --git a/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java index 4d1d634d34..091617841d 100644 --- a/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java @@ -241,6 +241,8 @@ class AnonymousAccessITCase { .addNull("proxyUser") .add("realmDescription", "SONIA :: SCM Manager") .add("skipFailedAuthenticators", false) + .add("jwtExpirationInH", 1) + .add("enabledJwtEndless", false) .build().toString(); } } diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java index 21bf88ae9a..00ae1bdc18 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -281,7 +281,9 @@ public class TestData { " \"loginInfoUrl\": \"https://login-info.scm-manager.org/api/v1/login-info\",\n" + " \"releaseFeedUrl\": \"https://scm-manager.org/download/rss.xml\",\n" + " \"mailDomainName\": \"scm-manager.local\", \n" + - " \"enabledApiKeys\": \"true\"\n" + + " \"enabledApiKeys\": \"true\",\n" + + " \"jwtExpirationInH\": 1,\n" + + " \"enabledJwtEndless\": false\n" + "}") .put(createResourceUrl("config")) .then() diff --git a/scm-ui/e2e-tests/package.json b/scm-ui/e2e-tests/package.json index 38646a1f4f..73b1966a37 100644 --- a/scm-ui/e2e-tests/package.json +++ b/scm-ui/e2e-tests/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@ffmpeg-installer/ffmpeg": "^1.0.20", - "@scm-manager/integration-test-runner": "^3.4.3", + "@scm-manager/integration-test-runner": "^3.5.0", "fluent-ffmpeg": "^2.1.2" }, "devDependencies": { @@ -26,4 +26,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/scm-ui/ui-api/src/config.test.ts b/scm-ui/ui-api/src/config.test.ts index 5112405244..bb38793a85 100644 --- a/scm-ui/ui-api/src/config.test.ts +++ b/scm-ui/ui-api/src/config.test.ts @@ -53,6 +53,8 @@ describe("Test config hooks", () => { alertsUrl: "", releaseFeedUrl: "", skipFailedAuthenticators: false, + jwtExpirationInH: 1, + enabledJwtEndless: false, _links: { update: { href: "/config", diff --git a/scm-ui/ui-components/src/forms/InputField.tsx b/scm-ui/ui-components/src/forms/InputField.tsx index e20d98da1c..b0027004fa 100644 --- a/scm-ui/ui-components/src/forms/InputField.tsx +++ b/scm-ui/ui-components/src/forms/InputField.tsx @@ -21,6 +21,7 @@ import { createAttributesForTesting } from "../devBuild"; import useAutofocus from "./useAutofocus"; import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes"; import { createA11yId } from "../createA11yId"; +import FieldMessage from "@scm-manager/ui-core/src/base/forms/base/field-message/FieldMessage"; type BaseProps = { label?: string; @@ -40,6 +41,7 @@ type BaseProps = { defaultValue?: string | number; readOnly?: boolean; required?: boolean; + warning?: string; }; export const InnerInputField: FC> = ({ @@ -60,6 +62,7 @@ export const InnerInputField: FC defaultValue, readOnly, required, + warning, ...props }) => { const field = useAutofocus(autofocus, props.innerRef); @@ -123,6 +126,7 @@ export const InnerInputField: FC {...createAttributesForTesting(testId)} /> + {warning ? {warning} : null} {helper} ); diff --git a/scm-ui/ui-types/src/Config.ts b/scm-ui/ui-types/src/Config.ts index d909022b87..7d8ded89a1 100644 --- a/scm-ui/ui-types/src/Config.ts +++ b/scm-ui/ui-types/src/Config.ts @@ -47,4 +47,6 @@ export type Config = HalRepresentation & { emergencyContacts: string[]; enabledApiKeys: boolean; enabledFileSearch: boolean; + jwtExpirationInH?: number; + enabledJwtEndless?: boolean; }; diff --git a/scm-ui/ui-webapp/public/locales/de/config.json b/scm-ui/ui-webapp/public/locales/de/config.json index fd0798b7c1..ba5bb5739b 100644 --- a/scm-ui/ui-webapp/public/locales/de/config.json +++ b/scm-ui/ui-webapp/public/locales/de/config.json @@ -25,6 +25,13 @@ "notConfiguredHint": "Authentifizierungs URL ist nicht gesetzt" } }, + "jwtSettings": { + "subtitle": "JWT Einstellungen", + "label": "Ablaufzeit", + "help": "Legen Sie die Ablaufzeit des JWT in Stunden fest. Wenn Sie die Zeit auf 'endlos' setzen möchten, verwenden Sie die Option 'endlessJwt' in der 'config.yml'.", + "hoursWarning": "Es wird nicht empfohlen, die Ablaufzeit auf mehr als 24 Stunden einzustellen", + "endlessWarning": "Die Ablaufzeit ist auf endlos eingestellt. Siehe 'config.yml'." + }, "proxySettings": { "subtitle": "Proxy Einstellungen", "enable": "Proxy aktivieren", diff --git a/scm-ui/ui-webapp/public/locales/en/config.json b/scm-ui/ui-webapp/public/locales/en/config.json index e320746c3d..fa2b13651a 100644 --- a/scm-ui/ui-webapp/public/locales/en/config.json +++ b/scm-ui/ui-webapp/public/locales/en/config.json @@ -25,6 +25,13 @@ "notConfiguredHint": "Authentication URL is not configured" } }, + "jwtSettings": { + "subtitle": "JWT Settings", + "label": "Expiration time", + "help": "Set the JWT expiration time in hours. If you want to set the time to endless consider the 'endlessJWT' option inside the 'config.yml'.", + "hoursWarning": "It is not recommended to set the expiration time over 24 hours.", + "endlessWarning": "The expiration time is set to endless." + }, "proxySettings": { "subtitle": "Proxy Settings", "enable": "Enable Proxy", diff --git a/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx b/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx index 0f0ac35168..045e6e0acf 100644 --- a/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx +++ b/scm-ui/ui-webapp/src/admin/components/form/ConfigForm.tsx @@ -26,6 +26,7 @@ import PluginSettings from "./PluginSettings"; import FunctionSettings from "./FunctionSettings"; import InvalidateCaches from "./InvalidateCaches"; import InvalidateSearchIndex from "./InvalidateSearchIndex"; +import JwtSettings from "./JwtSettings"; type Props = { submitForm: (p: Config) => void; @@ -60,6 +61,7 @@ const ConfigForm: FC = ({ dateFormat: "", anonymousAccessEnabled: false, anonymousMode: "OFF", + enabledFileSearch: true, baseUrl: "", forceBaseUrl: false, loginAttemptLimit: 0, @@ -77,6 +79,8 @@ const ConfigForm: FC = ({ mailDomainName: "", emergencyContacts: [], enabledApiKeys: true, + jwtExpirationInH: 1, + enabledJwtEndless: false, _links: {}, }); const [showNotification, setShowNotification] = useState(false); @@ -184,6 +188,13 @@ const ConfigForm: FC = ({ hasUpdatePermission={configUpdatePermission} />
+ +
= ({ jwtExpirationInH, onChange, hasUpdatePermission, enabledJwtEndless }) => { + const { t } = useTranslation("config"); + const [warning, setWarning] = useState(undefined); + const jwtOverTwentyFourWarning = t("jwtSettings.hoursWarning"); + const jwtIsSetToEndless = t("jwtSettings.endlessWarning"); + + const handleJwtTimeChange = (value: string) => { + if (Number(value) > 24) { + setWarning(jwtOverTwentyFourWarning); + } else { + setWarning(undefined); + } + onChange(true, Number(value), "jwtExpirationInH"); + }; + + return ( +
+ +
+
+ +
+
+
+ ); +}; + +export default JwtSettings; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java index 883b0f77d7..ad429cdb6d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java @@ -57,6 +57,8 @@ public class ConfigDto extends HalRepresentation implements UpdateConfigDto { private String alertsUrl; private String releaseFeedUrl; private String mailDomainName; + private int jwtExpirationInH; + private boolean enabledJwtEndless; private Set emergencyContacts; @Override diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java index 02e1507dd0..25e62959d5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java @@ -35,6 +35,7 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Response; import sonia.scm.admin.ScmConfigurationStore; +import sonia.scm.config.SecureKeyService; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.NamespaceStrategyValidator; @@ -57,18 +58,21 @@ public class ConfigResource { private final NamespaceStrategyValidator namespaceStrategyValidator; private final JsonMerger jsonMerger; + private final SecureKeyService secureKeyService; @Inject public ConfigResource(ScmConfigurationStore store, ConfigDtoToScmConfigurationMapper dtoToConfigMapper, ScmConfigurationToConfigDtoMapper configToDtoMapper, NamespaceStrategyValidator namespaceStrategyValidator, - JsonMerger jsonMerger) { + JsonMerger jsonMerger, + SecureKeyService secureKeyService) { this.dtoToConfigMapper = dtoToConfigMapper; this.configToDtoMapper = configToDtoMapper; this.store = store; this.namespaceStrategyValidator = namespaceStrategyValidator; this.jsonMerger = jsonMerger; + this.secureKeyService = secureKeyService; } /** @@ -201,6 +205,9 @@ public class ConfigResource { private void updateConfig(ConfigDto updatedConfigDto) { // ensure the namespace strategy is valid namespaceStrategyValidator.check(updatedConfigDto.getNamespaceStrategy()); + if (store.get().getJwtExpirationInH() > updatedConfigDto.getJwtExpirationInH()) { + secureKeyService.clearAllTokens(); + } store.store(dtoToConfigMapper.map(updatedConfigDto)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/config/SecureKeyService.java b/scm-webapp/src/main/java/sonia/scm/config/SecureKeyService.java new file mode 100644 index 0000000000..f279258936 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/config/SecureKeyService.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 - present Cloudogu GmbH + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package sonia.scm.config; + +import jakarta.inject.Inject; +import sonia.scm.security.SecureKeyResolver; + + +public class SecureKeyService { + + private final SecureKeyResolver secureKeyResolver; + + @Inject + public SecureKeyService(SecureKeyResolver secureKeyResolver) { + this.secureKeyResolver = secureKeyResolver; + } + + public void clearAllTokens() { + secureKeyResolver.deleteStore(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java index 0d111c3139..4158e8ebbb 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java @@ -29,6 +29,7 @@ import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.config.ScmConfiguration; import java.time.Clock; import java.time.Instant; @@ -57,10 +58,11 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { private final SecureKeyResolver keyResolver; private final JwtConfig jwtConfig; private final Clock clock; + private final ScmConfiguration scmConfiguration; private String subject; private String issuer; - private long expiresIn = 1; + private long expiresIn; private TimeUnit expiresInUnit = TimeUnit.HOURS; private long refreshableFor = DEFAULT_REFRESHABLE; private TimeUnit refreshableForUnit = DEFAULT_REFRESHABLE_UNIT; @@ -70,11 +72,13 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { private final Map custom = Maps.newHashMap(); - JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver, JwtConfig jwtConfig, Clock clock) { + JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver, JwtConfig jwtConfig, Clock clock, ScmConfiguration scmConfiguration) { this.keyGenerator = keyGenerator; this.keyResolver = keyResolver; this.jwtConfig = jwtConfig; this.clock = clock; + this.scmConfiguration = scmConfiguration; + this.expiresIn = scmConfiguration.getJwtExpirationInH(); } @Override @@ -179,6 +183,8 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { if(!jwtConfig.isEndlessJwtEnabled()) { claims.setExpiration(new Date(now.toEpochMilli() + expiration)); + } else { + scmConfiguration.setEnabledJwtExpiration(true); } if (refreshableFor > 0) { @@ -196,8 +202,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { if ( issuer != null ) { claims.setIssuer(issuer); } - - + // sign token and create compact version String compact = Jwts.builder() .setClaims(claims) diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java index 96c0ecb19c..fde02a9c7f 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilderFactory.java @@ -17,6 +17,7 @@ package sonia.scm.security; import jakarta.inject.Inject; +import sonia.scm.config.ScmConfiguration; import sonia.scm.plugin.Extension; import java.time.Clock; @@ -35,25 +36,27 @@ public final class JwtAccessTokenBuilderFactory implements AccessTokenBuilderFac private final JwtConfig jwtConfig; private final Set enrichers; private final Clock clock; + private final ScmConfiguration scmConfiguration; @Inject public JwtAccessTokenBuilderFactory( - KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers, JwtConfig jwtConfig) { - this(keyGenerator, keyResolver, jwtConfig, enrichers, Clock.systemDefaultZone()); + KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set enrichers, JwtConfig jwtConfig, ScmConfiguration scmConfiguration) { + this(keyGenerator, keyResolver, jwtConfig, enrichers, Clock.systemDefaultZone(), scmConfiguration); } JwtAccessTokenBuilderFactory( - KeyGenerator keyGenerator, SecureKeyResolver keyResolver, JwtConfig jwtConfig, Set enrichers, Clock clock) { + KeyGenerator keyGenerator, SecureKeyResolver keyResolver, JwtConfig jwtConfig, Set enrichers, Clock clock, ScmConfiguration scmConfiguration) { this.keyGenerator = keyGenerator; this.keyResolver = keyResolver; this.jwtConfig = jwtConfig; this.enrichers = enrichers; this.clock = clock; + this.scmConfiguration = scmConfiguration; } @Override public JwtAccessTokenBuilder create() { - JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver, jwtConfig, clock); + JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver, jwtConfig, clock, scmConfiguration); // enrich access token builder enrichers.forEach((enricher) -> { diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtConfig.java b/scm-webapp/src/main/java/sonia/scm/security/JwtConfig.java index 2376c945c5..5071e49b73 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtConfig.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtConfig.java @@ -30,7 +30,7 @@ public class JwtConfig { @ConfigValue( key = "endlessJwt", defaultValue = "false", - description = "The lifespan of the issued JWT tokens should be endless. Logged-in users are no longer automatically logged out.") + description = "The lifespan of the issued JWT tokens should be endless. Logged-in users are no longer automatically logged out. Any other expiration time will be overridden") boolean endlessJwt) { this.endlessJwt = endlessJwt; } diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java b/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java index 3597648308..a89f8a58e5 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java @@ -131,4 +131,7 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter return new SecureKey(bytes, System.currentTimeMillis()); } + public void deleteStore() { + store.clear(); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java index a8dc7d5749..dd4f9b74c3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java @@ -63,6 +63,8 @@ class ConfigDtoToScmConfigurationMapperTest { assertThat(config.getLoginInfoUrl()).isEqualTo("https://scm-manager.org/login-info"); assertThat(config.getMailDomainName()).isEqualTo("hitchhiker.mail"); assertThat(config.getEmergencyContacts()).contains(expectedUsers); + assertThat(config.getJwtExpirationInH()).isEqualTo(10); + assertThat(config.isJwtEndless()).isFalse(); } @Test @@ -105,6 +107,8 @@ class ConfigDtoToScmConfigurationMapperTest { configDto.setMailDomainName("hitchhiker.mail"); configDto.setEmergencyContacts(Sets.newSet(expectedUsers)); configDto.setEnabledUserConverter(false); + configDto.setJwtExpirationInH(10); + configDto.setEnabledJwtEndless(false); return configDto; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index cf3868276f..97c149f671 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -30,6 +30,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.admin.ScmConfigurationStore; +import sonia.scm.config.SecureKeyService; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.NamespaceStrategyValidator; import sonia.scm.store.InMemoryConfigurationStoreFactory; @@ -69,10 +70,22 @@ class ConfigResourceTest { private ConfigDtoToScmConfigurationMapperImpl dtoToConfigMapper; @InjectMocks private ScmConfigurationToConfigDtoMapperImpl configToDtoMapper; + @Mock + private SecureKeyService secureKeyService; @BeforeEach void prepareEnvironment() { - ConfigResource configResource = new ConfigResource(new ScmConfigurationStore(new InMemoryConfigurationStoreFactory(), new ScmConfiguration()), dtoToConfigMapper, configToDtoMapper, namespaceStrategyValidator, jsonMerger); + ConfigResource configResource = new ConfigResource( + new ScmConfigurationStore( + new InMemoryConfigurationStoreFactory(), + new ScmConfiguration() + ), + dtoToConfigMapper, + configToDtoMapper, + namespaceStrategyValidator, + jsonMerger, + secureKeyService + ); dispatcher.addSingletonResource(configResource); } @@ -108,7 +121,7 @@ class ConfigResourceTest { permissions = "configuration:read,write:global" ) void shouldUpdateConfig() throws URISyntaxException, IOException { - MockHttpRequest request = put("sonia/scm/api/v2/config-test-update.json"); + MockHttpRequest request = put(); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -128,7 +141,7 @@ class ConfigResourceTest { @Test void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, IOException { - MockHttpRequest request = put("sonia/scm/api/v2/config-test-update.json"); + MockHttpRequest request = put(); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -185,8 +198,8 @@ class ConfigResourceTest { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); } - private MockHttpRequest put(String resourceName) throws IOException, URISyntaxException { - URL url = Resources.getResource(resourceName); + private MockHttpRequest put() throws IOException, URISyntaxException { + URL url = Resources.getResource("sonia/scm/api/v2/config-test-update.json"); byte[] configJson = Resources.toByteArray(url); return MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2) .contentType(VndMediaType.CONFIG) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java index dede3435a4..51525be0f2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java @@ -98,6 +98,8 @@ class ScmConfigurationToConfigDtoMapperTest { assertThat(dto.getReleaseFeedUrl()).isEqualTo("https://www.scm-manager.org/download/rss.xml"); assertThat(dto.getMailDomainName()).isEqualTo("scm-manager.local"); assertThat(dto.getEmergencyContacts()).contains(expectedUsers); + assertThat(dto.getJwtExpirationInH()).isEqualTo(10); + assertThat(dto.isEnabledJwtEndless()).isFalse(); assertLinks(dto); } @@ -161,6 +163,8 @@ class ScmConfigurationToConfigDtoMapperTest { config.setAlertsUrl("https://alerts.scm-manager.org/api/v1/alerts"); config.setReleaseFeedUrl("https://www.scm-manager.org/download/rss.xml"); config.setEmergencyContacts(Sets.newSet(expectedUsers)); + config.setJwtExpirationInH(10); + config.setEnabledJwtExpiration(false); return config; } diff --git a/scm-webapp/src/test/java/sonia/scm/config/SecureKeyServiceTest.java b/scm-webapp/src/test/java/sonia/scm/config/SecureKeyServiceTest.java new file mode 100644 index 0000000000..a16507e546 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/config/SecureKeyServiceTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 - present Cloudogu GmbH + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package sonia.scm.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.mockito.Mockito.verify; +import sonia.scm.security.SecureKeyResolver; + +@ExtendWith(MockitoExtension.class) +class SecureKeyServiceTest { + + private SecureKeyService secureKeyService; + @Mock + private SecureKeyResolver configRepository; + + @BeforeEach + void prepareEnvironment() { + this.secureKeyService = new SecureKeyService(configRepository); + } + + @Test + void shouldDeleteStore() { + this.secureKeyService.clearAllTokens(); + verify(configRepository).deleteStore(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java index 6ec9ccfef6..a6353f38c2 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenBuilderTest.java @@ -19,6 +19,7 @@ package sonia.scm.security; import com.google.common.collect.Sets; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; +import org.apache.commons.lang.time.DateUtils; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; @@ -30,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.config.ScmConfiguration; import java.time.Clock; import java.time.Instant; @@ -48,7 +50,6 @@ import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; /** * Unit test for {@link JwtAccessTokenBuilder}. - * */ @ExtendWith(MockitoExtension.class) class JwtAccessTokenBuilderTest { @@ -62,6 +63,8 @@ class JwtAccessTokenBuilderTest { @Mock private JwtConfig jwtConfig; + private ScmConfiguration scmConfiguration; + private Set enrichers; private JwtAccessTokenBuilderFactory factory; @@ -85,10 +88,12 @@ class JwtAccessTokenBuilderTest { @BeforeEach void setUpDependencies() { + this.scmConfiguration = new ScmConfiguration(); + this.scmConfiguration.setJwtExpirationInH(10); lenient().when(keyGenerator.createKey()).thenReturn("42"); lenient().when(secureKeyResolver.getSecureKey(anyString())).thenReturn(createSecureKey()); enrichers = Sets.newHashSet(); - factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers, jwtConfig); + factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers, jwtConfig, scmConfiguration); } @Nested @@ -99,7 +104,7 @@ class JwtAccessTokenBuilderTest { */ @BeforeEach void setUpObjectUnderTest() { - factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers, jwtConfig); + factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers, jwtConfig, scmConfiguration); } /** @@ -149,7 +154,7 @@ class JwtAccessTokenBuilderTest { @BeforeEach void setUpObjectUnderTest() { - factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, jwtConfig, enrichers, clock); + factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, jwtConfig, enrichers, clock, scmConfiguration); } @Test @@ -189,11 +194,9 @@ class JwtAccessTokenBuilderTest { @Nested class FromApiKeyRealm { - private Scope scope; - @BeforeEach void mockApiKeyRealm() { - scope = Scope.valueOf("dummy:scope:*"); + Scope scope = Scope.valueOf("dummy:scope:*"); lenient().when(principalCollection.getRealmNames()).thenReturn(singleton("ApiTokenRealm")); lenient().when(principalCollection.oneByType(Scope.class)).thenReturn(scope); } @@ -276,6 +279,7 @@ class JwtAccessTokenBuilderTest { assertThat(token.getSubject()).isEqualTo("Red"); assertThat(token.getIssuer()).isNotEmpty(); assertThat(token.getIssuer()).contains("https://scm-manager.org"); + assertThat(scmConfiguration.isJwtEndless()).isEqualTo(true); } @Test @@ -291,6 +295,24 @@ class JwtAccessTokenBuilderTest { assertThat(token.getSubject()).isEqualTo("Red"); assertThat(token.getIssuer()).isNotEmpty(); assertThat(token.getIssuer()).contains("https://scm-manager.org"); + assertThat(scmConfiguration.isJwtEndless()).isEqualTo(false); + } + } + + @Nested + class WithJwtSetTimeFeature { + + @Test + void testBuildWithDefaultJwtTime() { + JwtAccessToken token = factory.create().subject("Red").issuer("https://scm-manager.org").build(); + + assertThat(token.getId()).isNotEmpty(); + assertThat(token.getIssuedAt()).isNotNull(); + assertThat(token.getExpiration()).isNotNull(); + assertThat(token.getSubject()).isEqualTo("Red"); + assertThat(token.getIssuer()).isNotEmpty(); + assertThat(token.getIssuer()).contains("https://scm-manager.org"); + assertThat(token.getExpiration()).isEqualTo(DateUtils.addHours(token.getIssuedAt(), 10)); } } } diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java index 9bd56e1f31..8698b0a4b3 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Answers; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.config.ScmConfiguration; import sonia.scm.user.User; import java.sql.Date; @@ -53,6 +54,8 @@ class JwtAccessTokenRefresherTest { private SecureKeyResolver keyResolver; @Mock private JwtConfig jwtConfig; + + private ScmConfiguration scmConfiguration; @Mock private JwtAccessTokenRefreshStrategy refreshStrategy; @Mock @@ -71,9 +74,16 @@ class JwtAccessTokenRefresherTest { Clock creationClock = mock(Clock.class); when(creationClock.instant()).thenReturn(TOKEN_CREATION); - tokenBuilder = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, jwtConfig, Collections.emptySet(), creationClock).create(); + tokenBuilder = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, jwtConfig, Collections.emptySet(), creationClock, scmConfiguration).create(); - JwtAccessTokenBuilderFactory refreshBuilderFactory = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, jwtConfig, Collections.emptySet(), refreshClock); + JwtAccessTokenBuilderFactory refreshBuilderFactory = new JwtAccessTokenBuilderFactory( + keyGenerator, + keyResolver, + jwtConfig, + Collections.emptySet(), + refreshClock, + scmConfiguration + ); refresher = new JwtAccessTokenRefresher(refreshBuilderFactory, refreshStrategy, refreshClock); when(refreshClock.instant()).thenReturn(NOW); lenient().when(refreshStrategy.shouldBeRefreshed(any())).thenReturn(true); @@ -91,6 +101,11 @@ class JwtAccessTokenRefresherTest { when(subject.getPrincipal()).thenReturn(new User("trillian")); } + @BeforeEach + void initConfig() { + this.scmConfiguration = new ScmConfiguration(); + } + @AfterEach void tearDownSubject() { ThreadContext.unbindSubject(); diff --git a/scm-webapp/src/test/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategyTest.java index 01b8034018..25867c9d84 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategyTest.java @@ -21,6 +21,7 @@ import com.github.sdorra.shiro.SubjectAware; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import sonia.scm.config.ScmConfiguration; import java.time.Clock; import java.time.Instant; @@ -43,15 +44,11 @@ import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; public class PercentageJwtAccessTokenRefreshStrategyTest { private static final Instant TOKEN_CREATION = Instant.now().truncatedTo(SECONDS); - + private final KeyGenerator keyGenerator = () -> "key"; + private final JwtConfig jwtConfig = mock(JwtConfig.class); + private final Clock refreshClock = mock(Clock.class); @Rule public ShiroRule shiro = new ShiroRule(); - - private KeyGenerator keyGenerator = () -> "key"; - private JwtConfig jwtConfig = mock(JwtConfig.class); - - private Clock refreshClock = mock(Clock.class); - private JwtAccessTokenBuilder tokenBuilder; private PercentageJwtAccessTokenRefreshStrategy refreshStrategy; @@ -59,11 +56,18 @@ public class PercentageJwtAccessTokenRefreshStrategyTest { public void initToken() { SecureKeyResolver keyResolver = mock(SecureKeyResolver.class); when(keyResolver.getSecureKey(any())).thenReturn(createSecureKey()); - + ScmConfiguration scmConfiguration = new ScmConfiguration(); Clock creationClock = mock(Clock.class); when(creationClock.instant()).thenReturn(TOKEN_CREATION); - tokenBuilder = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, jwtConfig, Collections.emptySet(), creationClock).create(); + tokenBuilder = new JwtAccessTokenBuilderFactory( + keyGenerator, + keyResolver, + jwtConfig, + Collections.emptySet(), + creationClock, + scmConfiguration + ).create(); tokenBuilder.expiresIn(1, HOURS); tokenBuilder.refreshableFor(1, HOURS); diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java index d616f880f9..057b84b91e 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java @@ -32,7 +32,6 @@ import java.util.Arrays; import java.util.Random; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.not; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; @@ -40,16 +39,22 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) -public class SecureKeyResolverTest -{ +public class SecureKeyResolverTest { - @Test - public void testGetSecureKey() - { + private SecureKeyResolver resolver; + @Mock + private ConfigurationEntryStore store; + @Mock + private JwtSettingsStore jwtSettingsStore; + private final JwtSettings settings = new JwtSettings(false, 100); + + @Test + public void testGetSecureKey() { SecureKey key = resolver.getSecureKey("test"); assertNotNull(key); @@ -61,6 +66,12 @@ public class SecureKeyResolverTest assertSame(key, sameKey); } + @Test + public void clearSecureKey() { + resolver.deleteStore(); + verify(store).clear(); + } + @Test public void shouldReturnRegeneratedKey() { when(jwtSettingsStore.get()).thenReturn(settings); @@ -78,37 +89,32 @@ public class SecureKeyResolverTest assertThat(sameRegeneratedKey.getCreationDate()).isEqualTo(regeneratedKey.getCreationDate()); } - @Test - public void testResolveSigningKeyBytes() - { + @Test + public void testResolveSigningKeyBytes() { SecureKey key = resolver.getSecureKey("test"); when(store.get("test")).thenReturn(key); when(jwtSettingsStore.get()).thenReturn(settings); byte[] bytes = resolver.resolveSigningKeyBytes(null, - Jwts.claims().setSubject("test")); + Jwts.claims().setSubject("test")); assertArrayEquals(key.getBytes(), bytes); } - @Test - public void testResolveSigningKeyBytesWithoutKey() - { + @Test + public void testResolveSigningKeyBytesWithoutKey() { byte[] bytes = resolver.resolveSigningKeyBytes(null, Jwts.claims().setSubject("test")); assertThat(bytes[0]).isEqualTo((byte) 42); } - @Test(expected = IllegalArgumentException.class) - public void testResolveSigningKeyBytesWithoutSubject() - { + @Test(expected = IllegalArgumentException.class) + public void testResolveSigningKeyBytesWithoutSubject() { resolver.resolveSigningKeyBytes(null, Jwts.claims()); } - - @Before - public void setUp() - { + @Before + public void setUp() { ConfigurationEntryStoreFactory factory = mock(ConfigurationEntryStoreFactory.class); when(factory.withType(any())).thenCallRealMethod(); @@ -121,16 +127,4 @@ public class SecureKeyResolverTest doAnswer(invocation -> ((byte[]) invocation.getArguments()[0])[0] = 42).when(random).nextBytes(any()); resolver = new SecureKeyResolver(factory, jwtSettingsStore, random); } - - //~--- fields --------------------------------------------------------------- - - private SecureKeyResolver resolver; - - @Mock - private ConfigurationEntryStore store; - - @Mock - private JwtSettingsStore jwtSettingsStore; - - private JwtSettings settings = new JwtSettings(false, 100); } diff --git a/yarn.lock b/yarn.lock index 171b4e735a..1b8d00be64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3059,10 +3059,10 @@ eslint-plugin-react-hooks "^2.1.2" jest "^26.6.3" -"@scm-manager/integration-test-runner@^3.4.3": - version "3.4.3" - resolved "https://registry.yarnpkg.com/@scm-manager/integration-test-runner/-/integration-test-runner-3.4.3.tgz#6a2e44f5c360fb1c40c3701cf9e8ddadd5031666" - integrity sha512-tA3B5iDAsNWQgXUiMhnrz7sX5dc0674R5Xb+Fch5kSysxMjwn5gMeDUIXA6j5S6OXsp8jlIj/y70m5foplO2WQ== +"@scm-manager/integration-test-runner@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@scm-manager/integration-test-runner/-/integration-test-runner-3.5.0.tgz#fdc9b1392d764a3db4aefff041c47a7c4f5bedce" + integrity sha512-/Aj8FldRdv3boOQJyFlT6AJb4vtxeiuXj7EF578QpT2DqbD/fyUC3qeZnfo78wS7KvSzJKhxrJrLIoeFpg+4iw== dependencies: "@ffmpeg-installer/ffmpeg" "^1.0.20" "@octokit/rest" "^18.0.9"