mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-11 07:55:47 +01:00
First steps for JWT refresh
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,70 +39,77 @@ import java.util.Optional;
|
|||||||
* be issued from a restful webservice endpoint by providing credentials. After the token was issued, the token must be
|
* be issued from a restful webservice endpoint by providing credentials. After the token was issued, the token must be
|
||||||
* send along with every request. The token should be send in its compact representation as bearer authorization header
|
* send along with every request. The token should be send in its compact representation as bearer authorization header
|
||||||
* or as cookie.
|
* or as cookie.
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public interface AccessToken {
|
public interface AccessToken {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns unique id of the access token.
|
* Returns unique id of the access token.
|
||||||
*
|
*
|
||||||
* @return unique id
|
* @return unique id
|
||||||
*/
|
*/
|
||||||
String getId();
|
String getId();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns name of subject which identifies the principal.
|
* Returns name of subject which identifies the principal.
|
||||||
*
|
*
|
||||||
* @return name of subject
|
* @return name of subject
|
||||||
*/
|
*/
|
||||||
String getSubject();
|
String getSubject();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns optional issuer. The issuer identifies the principal that issued the token.
|
* Returns optional issuer. The issuer identifies the principal that issued the token.
|
||||||
*
|
*
|
||||||
* @return optional issuer
|
* @return optional issuer
|
||||||
*/
|
*/
|
||||||
Optional<String> getIssuer();
|
Optional<String> getIssuer();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns time at which the token was issued.
|
* Returns time at which the token was issued.
|
||||||
*
|
*
|
||||||
* @return time at which the token was issued
|
* @return time at which the token was issued
|
||||||
*/
|
*/
|
||||||
Date getIssuedAt();
|
Date getIssuedAt();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the expiration time of token.
|
* Returns the expiration time of token.
|
||||||
*
|
*
|
||||||
* @return expiration time
|
* @return expiration time
|
||||||
*/
|
*/
|
||||||
Date getExpiration();
|
Date getExpiration();
|
||||||
|
|
||||||
|
Date getRefreshExpiration();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this
|
* Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this
|
||||||
* token. For example we could issue a token which can only be used to read a single repository. for more informations
|
* token. For example we could issue a token which can only be used to read a single repository. for more informations
|
||||||
* please have a look at {@link Scope}.
|
* please have a look at {@link Scope}.
|
||||||
*
|
*
|
||||||
* @return scope of token.
|
* @return scope of token.
|
||||||
*/
|
*/
|
||||||
Scope getScope();
|
Scope getScope();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an optional value of a custom token field.
|
* Returns an optional value of a custom token field.
|
||||||
*
|
*
|
||||||
* @param <T> type of field
|
* @param <T> type of field
|
||||||
* @param key key of token field
|
* @param key key of token field
|
||||||
*
|
*
|
||||||
* @return optional value of custom field
|
* @return optional value of custom field
|
||||||
*/
|
*/
|
||||||
<T> Optional<T> getCustom(String key);
|
<T> Optional<T> getCustom(String key);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns compact representation of token.
|
* Returns compact representation of token.
|
||||||
*
|
*
|
||||||
* @return compact representation
|
* @return compact representation
|
||||||
*/
|
*/
|
||||||
String compact();
|
String compact();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns read only map of all claim keys with their values.
|
||||||
|
*/
|
||||||
|
Map<String, Object> getClaims();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,11 +74,21 @@ public interface AccessTokenBuilder {
|
|||||||
* Sets the expiration for the token.
|
* Sets the expiration for the token.
|
||||||
*
|
*
|
||||||
* @param count expiration count
|
* @param count expiration count
|
||||||
* @param unit expirtation unit
|
* @param unit expiration unit
|
||||||
*
|
*
|
||||||
* @return {@code this}
|
* @return {@code this}
|
||||||
*/
|
*/
|
||||||
AccessTokenBuilder expiresIn(long count, TimeUnit unit);
|
AccessTokenBuilder expiresIn(long count, TimeUnit unit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the time how long this token may be refreshed. Set this to 0 (zero) to disable automatic refresh.
|
||||||
|
*
|
||||||
|
* @param count Time unit count. If set to 0, automatic refresh is disabled.
|
||||||
|
* @param unit time unit
|
||||||
|
*
|
||||||
|
* @return {@code this}
|
||||||
|
*/
|
||||||
|
AccessTokenBuilder refreshableFor(long count, TimeUnit unit);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduces the permissions of the token by providing a scope.
|
* Reduces the permissions of the token by providing a scope.
|
||||||
|
|||||||
@@ -31,7 +31,10 @@
|
|||||||
package sonia.scm.security;
|
package sonia.scm.security;
|
||||||
|
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,6 +78,11 @@ public final class JwtAccessToken implements AccessToken {
|
|||||||
return claims.getExpiration();
|
return claims.getExpiration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getRefreshExpiration() {
|
||||||
|
return claims.get("scm-manager.refreshableUntil", Date.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Scope getScope() {
|
public Scope getScope() {
|
||||||
return Scopes.fromClaims(claims);
|
return Scopes.fromClaims(claims);
|
||||||
@@ -90,5 +98,9 @@ public final class JwtAccessToken implements AccessToken {
|
|||||||
public String compact() {
|
public String compact() {
|
||||||
return compact;
|
return compact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getClaims() {
|
||||||
|
return Collections.unmodifiableMap(claims);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import io.jsonwebtoken.SignatureAlgorithm;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
@@ -48,7 +47,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Jwt implementation of {@link AccessTokenBuilder}.
|
* Jwt implementation of {@link AccessTokenBuilder}.
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@@ -58,18 +57,20 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
|||||||
* the logger for JwtAccessTokenBuilder
|
* the logger for JwtAccessTokenBuilder
|
||||||
*/
|
*/
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(JwtAccessTokenBuilder.class);
|
private static final Logger LOG = LoggerFactory.getLogger(JwtAccessTokenBuilder.class);
|
||||||
|
|
||||||
private final KeyGenerator keyGenerator;
|
private final KeyGenerator keyGenerator;
|
||||||
private final SecureKeyResolver keyResolver;
|
private final SecureKeyResolver keyResolver;
|
||||||
|
|
||||||
private String subject;
|
private String subject;
|
||||||
private String issuer;
|
private String issuer;
|
||||||
private long expiresIn = 60l;
|
private long expiresIn = 1;
|
||||||
private TimeUnit expiresInUnit = TimeUnit.MINUTES;
|
private TimeUnit expiresInUnit = TimeUnit.HOURS;
|
||||||
|
private long refreshableFor = 12;
|
||||||
|
private TimeUnit refreshableForUnit = TimeUnit.HOURS;
|
||||||
private Scope scope = Scope.empty();
|
private Scope scope = Scope.empty();
|
||||||
|
|
||||||
private final Map<String,Object> custom = Maps.newHashMap();
|
private final Map<String,Object> custom = Maps.newHashMap();
|
||||||
|
|
||||||
JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver) {
|
JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver) {
|
||||||
this.keyGenerator = keyGenerator;
|
this.keyGenerator = keyGenerator;
|
||||||
this.keyResolver = keyResolver;
|
this.keyResolver = keyResolver;
|
||||||
@@ -81,7 +82,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
|||||||
this.subject = subject;
|
this.subject = subject;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtAccessTokenBuilder custom(String key, Object value) {
|
public JwtAccessTokenBuilder custom(String key, Object value) {
|
||||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "null or empty value not allowed");
|
Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "null or empty value not allowed");
|
||||||
@@ -92,11 +93,11 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtAccessTokenBuilder scope(Scope scope) {
|
public JwtAccessTokenBuilder scope(Scope scope) {
|
||||||
Preconditions.checkArgument(scope != null, "scope can not be null");
|
Preconditions.checkArgument(scope != null, "scope cannot be null");
|
||||||
this.scope = scope;
|
this.scope = scope;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtAccessTokenBuilder issuer(String issuer) {
|
public JwtAccessTokenBuilder issuer(String issuer) {
|
||||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(issuer), "null or empty value not allowed");
|
Preconditions.checkArgument(!Strings.isNullOrEmpty(issuer), "null or empty value not allowed");
|
||||||
@@ -106,15 +107,26 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtAccessTokenBuilder expiresIn(long count, TimeUnit unit) {
|
public JwtAccessTokenBuilder expiresIn(long count, TimeUnit unit) {
|
||||||
Preconditions.checkArgument(count > 0, "expires in must be greater than 0");
|
Preconditions.checkArgument(count > 0, "count must be greater than 0");
|
||||||
Preconditions.checkArgument(unit != null, "unit can not be null");
|
Preconditions.checkArgument(unit != null, "unit cannot be null");
|
||||||
|
|
||||||
this.expiresIn = count;
|
this.expiresIn = count;
|
||||||
this.expiresInUnit = unit;
|
this.expiresInUnit = unit;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JwtAccessTokenBuilder refreshableFor(long count, TimeUnit unit) {
|
||||||
|
Preconditions.checkArgument(count >= 0, "count must be greater or equal to 0");
|
||||||
|
Preconditions.checkArgument(unit != null, "unit cannot be null");
|
||||||
|
|
||||||
|
this.refreshableFor = count;
|
||||||
|
this.refreshableForUnit = unit;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
private String getSubject(){
|
private String getSubject(){
|
||||||
if (subject == null) {
|
if (subject == null) {
|
||||||
Subject currentSubject = SecurityUtils.getSubject();
|
Subject currentSubject = SecurityUtils.getSubject();
|
||||||
@@ -130,35 +142,40 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
|||||||
String id = keyGenerator.createKey();
|
String id = keyGenerator.createKey();
|
||||||
|
|
||||||
String sub = getSubject();
|
String sub = getSubject();
|
||||||
|
|
||||||
LOG.trace("create new token {} for user {}", id, subject);
|
LOG.trace("create new token {} for user {}", id, subject);
|
||||||
SecureKey key = keyResolver.getSecureKey(sub);
|
SecureKey key = keyResolver.getSecureKey(sub);
|
||||||
|
|
||||||
Map<String,Object> customClaims = new HashMap<>(custom);
|
Map<String,Object> customClaims = new HashMap<>(custom);
|
||||||
|
|
||||||
// add scope to custom claims
|
// add scope to custom claims
|
||||||
Scopes.toClaims(customClaims, scope);
|
Scopes.toClaims(customClaims, scope);
|
||||||
|
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
long expiration = expiresInUnit.toMillis(expiresIn);
|
long expiration = expiresInUnit.toMillis(expiresIn);
|
||||||
|
|
||||||
Claims claims = Jwts.claims(customClaims)
|
Claims claims = Jwts.claims(customClaims)
|
||||||
.setSubject(sub)
|
.setSubject(sub)
|
||||||
.setId(id)
|
.setId(id)
|
||||||
.setIssuedAt(now)
|
.setIssuedAt(now)
|
||||||
.setExpiration(new Date(now.getTime() + expiration));
|
.setExpiration(new Date(now.getTime() + expiration));
|
||||||
|
|
||||||
|
if (refreshableFor > 0) {
|
||||||
|
long refreshExpiration = refreshableForUnit.toMillis(refreshableFor);
|
||||||
|
claims.put("scm-manager.refreshableUntil", new Date(now.getTime() + refreshExpiration).getTime() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
if ( issuer != null ) {
|
if ( issuer != null ) {
|
||||||
claims.setIssuer(issuer);
|
claims.setIssuer(issuer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// sign token and create compact version
|
// sign token and create compact version
|
||||||
String compact = Jwts.builder()
|
String compact = Jwts.builder()
|
||||||
.setClaims(claims)
|
.setClaims(claims)
|
||||||
.signWith(SignatureAlgorithm.HS256, key.getBytes())
|
.signWith(SignatureAlgorithm.HS256, key.getBytes())
|
||||||
.compact();
|
.compact();
|
||||||
|
|
||||||
return new JwtAccessToken(claims, compact);
|
return new JwtAccessToken(claims, compact);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
public interface JwtAccessTokenRefreshStrategy {
|
||||||
|
boolean shouldBeRefreshed(JwtAccessToken oldToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class JwtAccessTokenRefresher {
|
||||||
|
|
||||||
|
private final JwtAccessTokenBuilderFactory builderFactory;
|
||||||
|
private final JwtAccessTokenRefreshStrategy refreshStrategy;
|
||||||
|
|
||||||
|
public JwtAccessTokenRefresher(JwtAccessTokenBuilderFactory builderFactory, JwtAccessTokenRefreshStrategy refreshStrategy) {
|
||||||
|
this.builderFactory = builderFactory;
|
||||||
|
this.refreshStrategy = refreshStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<JwtAccessToken> refresh(JwtAccessToken oldToken) {
|
||||||
|
JwtAccessTokenBuilder builder = builderFactory.create();
|
||||||
|
Map<String, Object> claims = oldToken.getClaims();
|
||||||
|
claims.forEach(builder::custom);
|
||||||
|
|
||||||
|
if (canBeRefreshed(oldToken) && shouldBeRefreshed(oldToken)) {
|
||||||
|
builder.expiresIn(1, TimeUnit.HOURS);
|
||||||
|
// builder.custom("scm-manager.parentTokenId")
|
||||||
|
return Optional.of(builder.build());
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canBeRefreshed(JwtAccessToken oldToken) {
|
||||||
|
return tokenIsValid(oldToken) || tokenCanBeRefreshed(oldToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldBeRefreshed(JwtAccessToken oldToken) {
|
||||||
|
return refreshStrategy.shouldBeRefreshed(oldToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean tokenCanBeRefreshed(JwtAccessToken oldToken) {
|
||||||
|
Date refreshExpiration = oldToken.getRefreshExpiration();
|
||||||
|
return refreshExpiration != null && isBeforeNow(refreshExpiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean tokenIsValid(JwtAccessToken oldToken) {
|
||||||
|
return isBeforeNow(oldToken.getExpiration());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBeforeNow(Date expiration) {
|
||||||
|
return expiration.toInstant().isBefore(Instant.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package sonia.scm.security;
|
||||||
|
|
||||||
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@SubjectAware(
|
||||||
|
username = "user",
|
||||||
|
password = "secret",
|
||||||
|
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
||||||
|
)
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class JwtAccessTokenRefresherTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SecureKeyResolver keyResolver;
|
||||||
|
@Mock
|
||||||
|
private JwtAccessTokenRefreshStrategy refreshStrategy;
|
||||||
|
private JwtAccessTokenBuilderFactory builderFactory;
|
||||||
|
private JwtAccessTokenRefresher refresher;
|
||||||
|
private JwtAccessTokenBuilder tokenBuilder;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void initKeyResolver() {
|
||||||
|
byte[] bytes = new byte[256];
|
||||||
|
new Random().nextBytes(bytes);
|
||||||
|
SecureKey secureKey = new SecureKey(bytes, System.currentTimeMillis());
|
||||||
|
when(keyResolver.getSecureKey(any())).thenReturn(secureKey);
|
||||||
|
|
||||||
|
builderFactory = new JwtAccessTokenBuilderFactory(new DefaultKeyGenerator(), keyResolver, Collections.emptySet());
|
||||||
|
refresher = new JwtAccessTokenRefresher(builderFactory, refreshStrategy);
|
||||||
|
tokenBuilder = builderFactory.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotRefreshTokenWithDisabledRefresh() {
|
||||||
|
JwtAccessToken oldToken = tokenBuilder
|
||||||
|
.refreshableFor(0, TimeUnit.MINUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Optional<JwtAccessToken> refreshedToken = refresher.refresh(oldToken);
|
||||||
|
|
||||||
|
Assertions.assertThat(refreshedToken).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotRefreshTokenWhenStrategyDoesNotSaySo() {
|
||||||
|
JwtAccessToken oldToken = tokenBuilder
|
||||||
|
.refreshableFor(10, TimeUnit.MINUTES)
|
||||||
|
.build();
|
||||||
|
when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(false);
|
||||||
|
|
||||||
|
Optional<JwtAccessToken> refreshedToken = refresher.refresh(oldToken);
|
||||||
|
|
||||||
|
Assertions.assertThat(refreshedToken).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRefreshTokenWithEnabledRefresh() {
|
||||||
|
JwtAccessToken oldToken = tokenBuilder
|
||||||
|
.refreshableFor(1, TimeUnit.MINUTES)
|
||||||
|
.build();
|
||||||
|
when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(true);
|
||||||
|
|
||||||
|
Optional<JwtAccessToken> refreshedToken = refresher.refresh(oldToken);
|
||||||
|
|
||||||
|
Assertions.assertThat(refreshedToken).isNotEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ dent = secret, creator, heartOfGold, puzzle42
|
|||||||
unpriv = secret
|
unpriv = secret
|
||||||
crato = secret, creator
|
crato = secret, creator
|
||||||
community = secret, oss
|
community = secret, oss
|
||||||
|
user = secret, user
|
||||||
|
|
||||||
[roles]
|
[roles]
|
||||||
admin = *
|
admin = *
|
||||||
@@ -11,3 +12,4 @@ creator = repository:create
|
|||||||
heartOfGold = "repository:read,modify,delete:hof"
|
heartOfGold = "repository:read,modify,delete:hof"
|
||||||
puzzle42 = "repository:read,write:p42"
|
puzzle42 = "repository:read,write:p42"
|
||||||
oss = "repository:pull"
|
oss = "repository:pull"
|
||||||
|
user = *
|
||||||
|
|||||||
Reference in New Issue
Block a user