Make auth expiration for LFS configurable (#1697)

When SCM-Manager is used behind a reverse proxy like
Nginx it may be the case, that lfs PUT requests are
buffered by the reverse proxy and will be sent to the
SCM-Manager after the whole file has been received. Due
to the expiration time of 5 minutes for the authentivation
token that had been requested by Git before the upload
has been started, this request from the proxy to
SCM-Manager fails if the upload from the client to the
reverse proxy took longer than these 5 minutes.

To solve this, we make this expiration time configurable,
so that whenever you have very large files or small
bandwidth the expiration timeout can be increased.
This commit is contained in:
René Pfeuffer
2021-06-16 09:14:52 +02:00
committed by GitHub
parent 97b32f3918
commit b6d343bf09
15 changed files with 325 additions and 152 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 175 KiB

View File

@@ -20,4 +20,12 @@ Unter dem Eintrag Git können die folgenden Git-spezifischen Einstellungen vorge
Bitte beachten Sie, dass dieser Name aufgrund von Git-Spezifika nicht bei leeren Repositories genutzt
werden kann (hier wird immer der Git-interne Default Name genutzt, derzeit also `master`).
- LFS Autorisierungsablaufzeit
Ablaufzeit für den Autorisierungstoken in Minuten, der für LFS Speicheranfragen ausgestellt wird.
Wenn der SCM-Manager hinter einem Reverse-Proxy mit Zwischenspeicherung (z. B. Nginx) betrieben wird,
sollte dieser Wert auf die Zeit gesetzt werden, die ein LFS-Upload maximal benötigen kann. Treten
während eines länger laufenden LFS "Pushs" Autorisierungsfehler auf, sollte dieser Wert erhöht werden.
Der Default-Wert beträgt 5 Minuten.
![Administration-Plugins-Installed](assets/administration-settings-git.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -19,4 +19,11 @@ In the git section there are the following git specific settings:
Please mind, that due to git internals this cannot work for empty repositories (here git
will always use its internal default branch, so at the time being `master`).
- LFS authorization expiration
Sets the expiration time of the authorization token generated for LFS put requests in minutes.
If SCM-Manager is run behind a reverse proxy that buffers http requests (eg. Nginx), this
should set up to the time, an LFS upload may take at maximum. If you experience errors during
long-running LFS push requests, this may have to be increased. The default value is 5 minutes.
![Administration-Plugins-Installed](assets/administration-settings-git.png)

View File

@@ -0,0 +1,2 @@
- type: fixed
description: Added option to increase LFS authorization token timeout ([#1697](https://github.com/scm-manager/scm-manager/pull/1697))

View File

@@ -185,7 +185,7 @@ public class GitNonFastForwardITCase {
}
private static void setNonFastForwardDisallowed(boolean nonFastForwardDisallowed) {
String config = String.format("{'disabled': false, 'gcExpression': null, 'defaultBranch': 'main', 'nonFastForwardDisallowed': %s}", nonFastForwardDisallowed)
String config = String.format("{'disabled': false, 'gcExpression': null, 'defaultBranch': 'main', 'lfsWriteAuthorizationExpirationInMinutes': 5, 'nonFastForwardDisallowed': %s}", nonFastForwardDisallowed)
.replace('\'', '"');
given(VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX)

View File

@@ -31,6 +31,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@@ -52,6 +53,9 @@ public class GitConfigDto extends HalRepresentation implements UpdateGitConfigDt
@Pattern(regexp = VALID_BRANCH_NAMES)
private String defaultBranch;
@Min(1)
private int lfsWriteAuthorizationExpirationInMinutes;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {

View File

@@ -55,6 +55,9 @@ public class GitConfig extends RepositoryConfig {
@XmlElement(name = "default-branch")
private String defaultBranch = FALLBACK_BRANCH;
@XmlElement(name = "lfs-write-authorization-expiration")
private int lfsWriteAuthorizationExpirationInMinutes = 5;
public String getGcExpression() {
return gcExpression;
}
@@ -82,6 +85,14 @@ public class GitConfig extends RepositoryConfig {
this.defaultBranch = defaultBranch;
}
public int getLfsWriteAuthorizationExpirationInMinutes() {
return lfsWriteAuthorizationExpirationInMinutes;
}
public void setLfsWriteAuthorizationExpirationInMinutes(int lfsWriteAuthorizationExpirationInMinutes) {
this.lfsWriteAuthorizationExpirationInMinutes = lfsWriteAuthorizationExpirationInMinutes;
}
@Override
@XmlTransient // Only for permission checks, don't serialize to XML
public String getId() {

View File

@@ -27,6 +27,8 @@ package sonia.scm.web.lfs;
import com.github.sdorra.ssp.PermissionCheck;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.security.AccessToken;
@@ -43,10 +45,12 @@ public class LfsAccessTokenFactory {
private static final Logger LOG = LoggerFactory.getLogger(LfsAccessTokenFactory.class);
private final AccessTokenBuilderFactory tokenBuilderFactory;
private final GitRepositoryHandler handler;
@Inject
LfsAccessTokenFactory(AccessTokenBuilderFactory tokenBuilderFactory) {
LfsAccessTokenFactory(AccessTokenBuilderFactory tokenBuilderFactory, GitRepositoryHandler handler) {
this.tokenBuilderFactory = tokenBuilderFactory;
this.handler = handler;
}
AccessToken createReadAccessToken(Repository repository) {
@@ -67,7 +71,7 @@ public class LfsAccessTokenFactory {
permissions.add(push.asShiroString());
}
return createToken(Scope.valueOf(permissions));
return createToken(Scope.valueOf(permissions), 5);
}
AccessToken createWriteAccessToken(Repository repository) {
@@ -80,15 +84,22 @@ public class LfsAccessTokenFactory {
PermissionCheck push = RepositoryPermissions.push(repository);
push.check();
return createToken(Scope.valueOf(read.asShiroString(), pull.asShiroString(), push.asShiroString()));
int lfsAuthorizationTimeoutInMinutes = getConfiguredLfsAuthorizationTimeoutInMinutes();
return createToken(Scope.valueOf(read.asShiroString(), pull.asShiroString(), push.asShiroString()), lfsAuthorizationTimeoutInMinutes);
}
private AccessToken createToken(Scope scope) {
private AccessToken createToken(Scope scope, int expiration) {
LOG.trace("create access token with scope: {}", scope);
return tokenBuilderFactory
.create()
.expiresIn(5, TimeUnit.MINUTES)
.expiresIn(expiration, TimeUnit.MINUTES)
.scope(scope)
.build();
}
private int getConfiguredLfsAuthorizationTimeoutInMinutes() {
GitConfig repositoryConfig = handler.getConfig();
return repositoryConfig.getLfsWriteAuthorizationExpirationInMinutes();
}
}

View File

@@ -1,122 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Links } from "@scm-manager/ui-types";
import { InputField, Checkbox, validation as validator } from "@scm-manager/ui-components";
type Configuration = {
repositoryDirectory?: string;
gcExpression?: string;
nonFastForwardDisallowed: boolean;
defaultBranch: string;
_links: Links;
};
type Props = WithTranslation & {
initialConfiguration: Configuration;
readOnly: boolean;
onConfigurationChange: (p1: Configuration, p2: boolean) => void;
};
type State = Configuration & {};
class GitConfigurationForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
...props.initialConfiguration
};
}
onGcExpressionChange = (value: string) => {
this.setState(
{
gcExpression: value
},
() => this.props.onConfigurationChange(this.state, true)
);
};
onNonFastForwardDisallowed = (value: boolean) => {
this.setState(
{
nonFastForwardDisallowed: value
},
() => this.props.onConfigurationChange(this.state, true)
);
};
onDefaultBranchChange = (value: string) => {
this.setState(
{
defaultBranch: value
},
() => this.props.onConfigurationChange(this.state, this.isValidDefaultBranch())
);
};
isValidDefaultBranch = () => {
return validator.isBranchValid(this.state.defaultBranch);
};
render() {
const { gcExpression, nonFastForwardDisallowed, defaultBranch } = this.state;
const { readOnly, t } = this.props;
return (
<>
<InputField
name="gcExpression"
label={t("scm-git-plugin.config.gcExpression")}
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}
value={gcExpression}
onChange={this.onGcExpressionChange}
disabled={readOnly}
/>
<Checkbox
name="nonFastForwardDisallowed"
label={t("scm-git-plugin.config.nonFastForwardDisallowed")}
helpText={t("scm-git-plugin.config.nonFastForwardDisallowedHelpText")}
checked={nonFastForwardDisallowed}
onChange={this.onNonFastForwardDisallowed}
disabled={readOnly}
/>
<InputField
name="defaultBranch"
label={t("scm-git-plugin.config.defaultBranch")}
helpText={t("scm-git-plugin.config.defaultBranchHelpText")}
value={defaultBranch}
onChange={this.onDefaultBranchChange}
disabled={readOnly}
validationError={!this.isValidDefaultBranch()}
errorMessage={t("scm-git-plugin.config.defaultBranchValidationError")}
/>
</>
);
}
}
export default withTranslation("plugins")(GitConfigurationForm);

View File

@@ -21,26 +21,80 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Title, Configuration } from "@scm-manager/ui-components";
import GitConfigurationForm from "./GitConfigurationForm";
import React, { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Title, ConfigurationForm, InputField, Checkbox, validation } from "@scm-manager/ui-components";
import { useConfigLink } from "@scm-manager/ui-api";
import { HalRepresentation } from "@scm-manager/ui-types";
import { useForm } from "react-hook-form";
type Props = WithTranslation & {
type Props = {
link: string;
};
class GitGlobalConfiguration extends React.Component<Props> {
render() {
const { link, t } = this.props;
type Configuration = HalRepresentation & {
repositoryDirectory?: string;
gcExpression?: string;
nonFastForwardDisallowed: boolean;
defaultBranch: string;
lfsWriteAuthorizationExpirationInMinutes: number;
};
return (
<div>
<Title title={t("scm-git-plugin.config.title")} />
<Configuration link={link} render={(props: any) => <GitConfigurationForm {...props} />} />
</div>
);
}
}
const GitGlobalConfiguration: FC<Props> = ({ link }) => {
const [t] = useTranslation("plugins");
export default withTranslation("plugins")(GitGlobalConfiguration);
const { initialConfiguration, isReadOnly, update, ...formProps } = useConfigLink(link);
const { formState, handleSubmit, register, reset } = useForm<Configuration>({ mode: "onChange" });
useEffect(() => {
if (initialConfiguration) {
reset(initialConfiguration);
}
}, [initialConfiguration]);
const isValidDefaultBranch = (value: string) => {
return validation.isBranchValid(value);
};
return (
<ConfigurationForm
isValid={formState.isValid}
isReadOnly={isReadOnly}
onSubmit={handleSubmit(update)}
{...formProps}
>
<Title title={t("scm-git-plugin.config.title")} />
<InputField
label={t("scm-git-plugin.config.gcExpression")}
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}
disabled={isReadOnly}
{...register("gcExpression")}
/>
<Checkbox
label={t("scm-git-plugin.config.nonFastForwardDisallowed")}
helpText={t("scm-git-plugin.config.nonFastForwardDisallowedHelpText")}
disabled={isReadOnly}
{...register("nonFastForwardDisallowed")}
/>
<InputField
label={t("scm-git-plugin.config.defaultBranch")}
helpText={t("scm-git-plugin.config.defaultBranchHelpText")}
disabled={isReadOnly}
validationError={!!formState.errors.defaultBranch}
errorMessage={t("scm-git-plugin.config.defaultBranchValidationError")}
{...register("defaultBranch", { validate: isValidDefaultBranch })}
/>
<InputField
type="number"
label={t("scm-git-plugin.config.lfsWriteAuthorizationExpirationInMinutes")}
helpText={t("scm-git-plugin.config.lfsWriteAuthorizationExpirationInMinutesHelpText")}
disabled={isReadOnly}
validationError={!!formState.errors.lfsWriteAuthorizationExpirationInMinutes}
errorMessage={t("scm-git-plugin.config.lfsWriteAuthorizationExpirationInMinutesValidationError")}
{...register("lfsWriteAuthorizationExpirationInMinutes", { min: 1, required: true })}
/>
</ConfigurationForm>
);
};
export default GitGlobalConfiguration;

View File

@@ -27,6 +27,9 @@
"defaultBranch": "Default Branch",
"defaultBranchHelpText": "Dieser Name wird bei der Initialisierung neuer Git Repositories genutzt. Er hat keine weiteren Auswirkungen (insbesondere hat er keinen Einfluss auf den Branchnamen bei leeren Repositories).",
"defaultBranchValidationError": "Dies ist kein valider Branchname",
"lfsWriteAuthorizationExpirationInMinutes": "Ablaufzeit für LFS Autorisierung",
"lfsWriteAuthorizationExpirationInMinutesHelpText": "Ablaufzeit für den Autorisierungstoken in Minuten, der für LFS Speicheranfragen ausgestellt wird. Wenn der SCM-Manager hinter einem Reverse-Proxy mit Zwischenspeicherung (z. B. Nginx) betrieben wird, sollte dieser Wert auf die Zeit gesetzt werden, die ein LFS-Upload maximal benötigen kann.",
"lfsWriteAuthorizationExpirationInMinutesValidationError": "Has to be at least 1 minute",
"disabled": "Deaktiviert",
"disabledHelpText": "Aktiviere oder deaktiviere das Git Plugin",
"submit": "Speichern"

View File

@@ -27,6 +27,9 @@
"defaultBranch": "Default Branch",
"defaultBranchHelpText": "This name will be used for the initialization of new git repositories. It has no effect otherwise (especially this cannot change the initial branch name for empty repositories).",
"defaultBranchValidationError": "This is not a valid branch name",
"lfsWriteAuthorizationExpirationInMinutes": "LFS authorization expiration",
"lfsWriteAuthorizationExpirationInMinutesHelpText": "Expiration time of the authorization token generated for LFS put requests in minutes. If SCM-Manager is run behind a reverse proxy that buffers http requests (eg. Nginx), this should set up to the time, an LFS upload may take at maximum.",
"lfsWriteAuthorizationExpirationInMinutesValidationError": "Has to be at least 1 minute",
"disabled": "Disabled",
"disabledHelpText": "Enable or disable the Git plugin",
"submit": "Submit"

View File

@@ -48,6 +48,7 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.web.GitVndMediaType;
import sonia.scm.web.JsonMockHttpRequest;
import sonia.scm.web.RestDispatcher;
import javax.servlet.http.HttpServletResponse;
@@ -127,10 +128,12 @@ public class GitConfigResourceTest {
String responseString = response.getContentAsString();
assertTrue(responseString.contains("\"disabled\":false"));
assertTrue(responseString.contains("\"gcExpression\":\"valid Git GC Cron Expression\""));
assertTrue(responseString.contains("\"self\":{\"href\":\"/v2/config/git"));
assertTrue(responseString.contains("\"update\":{\"href\":\"/v2/config/git"));
assertThat(responseString)
.contains("\"disabled\":false")
.contains("\"gcExpression\":\"valid Git GC Cron Expression\"")
.contains("\"self\":{\"href\":\"/v2/config/git")
.contains("\"update\":{\"href\":\"/v2/config/git")
.contains("\"lfsWriteAuthorizationExpirationInMinutes\":5");
}
@Test
@@ -324,9 +327,9 @@ public class GitConfigResourceTest {
}
private MockHttpResponse put() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.put("/" + GitConfigResource.GIT_CONFIG_PATH_V2)
JsonMockHttpRequest request = JsonMockHttpRequest.put("/" + GitConfigResource.GIT_CONFIG_PATH_V2)
.contentType(GitVndMediaType.GIT_CONFIG)
.content("{\"disabled\":true, \"defaultBranch\":\"main\"}".getBytes());
.json("{'disabled':true, 'defaultBranch':'main', 'lfsWriteAuthorizationExpirationInMinutes':5}");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
@@ -337,6 +340,7 @@ public class GitConfigResourceTest {
GitConfig config = new GitConfig();
config.setGcExpression("valid Git GC Cron Expression");
config.setDisabled(false);
config.setLfsWriteAuthorizationExpirationInMinutes(5);
return config;
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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.web.lfs;
import org.apache.shiro.authz.UnauthorizedException;
import org.github.sdorra.jse.ShiroExtension;
import org.github.sdorra.jse.SubjectAware;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilder;
import sonia.scm.security.AccessTokenBuilderFactory;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@ExtendWith(ShiroExtension.class)
class LfsAccessTokenFactoryTest {
@Mock
private AccessTokenBuilderFactory tokenBuilderFactory;
@Mock(answer = Answers.RETURNS_SELF)
private AccessTokenBuilder tokenBuilder;
@Mock
private GitRepositoryHandler handler;
@InjectMocks
private LfsAccessTokenFactory factory;
@Mock
private AccessToken createdTokenFromMock;
private Repository repository = RepositoryTestData.createHeartOfGold();
@BeforeEach
void initRepository() {
repository.setId("42");
}
@Nested
class WithPermissions {
@BeforeEach
void initTokenBuilder() {
when(tokenBuilderFactory.create()).thenReturn(tokenBuilder);
when(tokenBuilder.build()).thenReturn(createdTokenFromMock);
}
@Test
@SubjectAware(
value = "trillian",
permissions = "repository:read,pull:42"
)
void shouldCreateReadToken() {
AccessToken readAccessToken = factory.createReadAccessToken(repository);
assertThat(readAccessToken).isSameAs(createdTokenFromMock);
verify(tokenBuilder).expiresIn(5, TimeUnit.MINUTES);
verify(tokenBuilder).scope(argThat(scope -> {
assertThat(scope.iterator()).toIterable()
.containsExactly("repository:read:42", "repository:pull:42");
return true;
}));
}
@Test
@SubjectAware(
value = "trillian",
permissions = "repository:read,pull,push:42"
)
void shouldCreateReadTokenWithPushIfPermitted() {
AccessToken readAccessToken = factory.createReadAccessToken(repository);
assertThat(readAccessToken).isSameAs(createdTokenFromMock);
verify(tokenBuilder).expiresIn(5, TimeUnit.MINUTES);
verify(tokenBuilder).scope(argThat(scope -> {
assertThat(scope.iterator()).toIterable()
.containsExactly("repository:read:42", "repository:pull:42", "repository:push:42");
return true;
}));
}
@Test
@SubjectAware(
value = "trillian",
permissions = "repository:read,pull,push:42"
)
void shouldCreateWriteToken() {
GitConfig config = new GitConfig();
config.setLfsWriteAuthorizationExpirationInMinutes(23);
when(handler.getConfig()).thenReturn(config);
AccessToken writeAccessToken = factory.createWriteAccessToken(repository);
assertThat(writeAccessToken).isSameAs(createdTokenFromMock);
verify(tokenBuilder).expiresIn(23, TimeUnit.MINUTES);
verify(tokenBuilder).scope(argThat(scope -> {
assertThat(scope.iterator()).toIterable()
.containsExactly("repository:read:42", "repository:pull:42", "repository:push:42");
return true;
}));
}
}
@Test
@SubjectAware(
value = "trillian",
permissions = "repository:read:42"
)
void shouldFailToCreateReadTokenWithoutPullPermission() {
assertThrows(UnauthorizedException.class, () -> factory.createReadAccessToken(repository));
}
@Test
@SubjectAware(
value = "trillian",
permissions = "repository:pull:42"
)
void shouldFailToCreateReadTokenWithoutReadPermission() {
assertThrows(UnauthorizedException.class, () -> factory.createReadAccessToken(repository));
}
@Test
@SubjectAware(
value = "trillian",
permissions = "repository:pull,push:42"
)
void shouldFailToCreateWriteTokenWithoutReadPermission() {
assertThrows(UnauthorizedException.class, () -> factory.createWriteAccessToken(repository));
}
@Test
@SubjectAware(
value = "trillian",
permissions = "repository:read,push:42"
)
void shouldFailToCreateWriteTokenWithoutPullPermission() {
assertThrows(UnauthorizedException.class, () -> factory.createWriteAccessToken(repository));
}
@Test
@SubjectAware(
value = "trillian",
permissions = "repository:read,pull:42"
)
void shouldFailToCreateWriteTokenWithoutPushPermission() {
assertThrows(UnauthorizedException.class, () -> factory.createWriteAccessToken(repository));
}
}