mirror of https://github.com/halo-dev/halo
Extract PAT operation with service (#7341)
#### What type of PR is this? /kind improvement /area core /milestone 2.20.x #### What this PR does / why we need it: This PR refactors UserScopedPatHandlerImpl with PAT service to make PAT operations flexible. #### Does this PR introduce a user-facing change? ```release-note None ```pull/7350/head
parent
067e3d58e1
commit
3a5e4f82b4
|
@ -20,6 +20,8 @@ public class PersonalAccessToken extends AbstractExtension {
|
|||
|
||||
public static final String KIND = "PersonalAccessToken";
|
||||
|
||||
public static final String PAT_TOKEN_PREFIX = "pat_";
|
||||
|
||||
private Spec spec = new Spec();
|
||||
|
||||
@Data
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
package run.halo.app.core.user.service;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.security.PersonalAccessToken;
|
||||
|
||||
/**
|
||||
* Service for personal access token.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public interface PatService {
|
||||
|
||||
/**
|
||||
* Create a new personal access token. We will automatically use the current user as the
|
||||
* owner of the token from the security context.
|
||||
*
|
||||
* @param patRequest the personal access token request
|
||||
* @return the created personal access token
|
||||
*/
|
||||
Mono<PersonalAccessToken> create(PersonalAccessToken patRequest);
|
||||
|
||||
/**
|
||||
* Create a new personal access token for the specified user.
|
||||
*
|
||||
* @param patRequest the personal access token request
|
||||
* @param username the username of the user
|
||||
* @return the created personal access token
|
||||
*/
|
||||
Mono<PersonalAccessToken> create(PersonalAccessToken patRequest, String username);
|
||||
|
||||
/**
|
||||
* Revoke a personal access token.
|
||||
*
|
||||
* @param patName the name of the personal access token
|
||||
* @param username the username of the user
|
||||
* @return the revoked personal access token
|
||||
*/
|
||||
Mono<PersonalAccessToken> revoke(String patName, String username);
|
||||
|
||||
/**
|
||||
* Restore a personal access token.
|
||||
*
|
||||
* @param patName the name of the personal access token
|
||||
* @param username the username of the user
|
||||
* @return the restored personal access token
|
||||
*/
|
||||
Mono<PersonalAccessToken> restore(String patName, String username);
|
||||
|
||||
/**
|
||||
* Delete a personal access token.
|
||||
*
|
||||
* @param patName the name of the personal access token
|
||||
* @param username the username of the user
|
||||
* @return the deleted personal access token
|
||||
*/
|
||||
Mono<PersonalAccessToken> delete(String patName, String username);
|
||||
|
||||
/**
|
||||
* Get a personal access token by name.
|
||||
*
|
||||
* @param patName the name of the personal access token
|
||||
* @param username the username of the user
|
||||
* @return the personal access token
|
||||
*/
|
||||
Mono<PersonalAccessToken> get(String patName, String username);
|
||||
|
||||
/**
|
||||
* Generate a personal access token.
|
||||
*
|
||||
* @param pat the personal access token
|
||||
* @return the generated token
|
||||
*/
|
||||
Mono<String> generateToken(PersonalAccessToken pat);
|
||||
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
package run.halo.app.core.user.service.impl;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import java.time.Clock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.AlternativeJdkIdGenerator;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.IdGenerator;
|
||||
import org.springframework.web.filter.reactive.ServerWebExchangeContextFilter;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.user.service.PatService;
|
||||
import run.halo.app.core.user.service.RoleService;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.security.PersonalAccessToken;
|
||||
import run.halo.app.security.authentication.CryptoService;
|
||||
import run.halo.app.security.authorization.AuthorityUtils;
|
||||
|
||||
/**
|
||||
* Service for managing personal access tokens (PATs).
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Service
|
||||
class PatServiceImpl implements PatService {
|
||||
|
||||
private final RoleService roleService;
|
||||
|
||||
private final IdGenerator idGenerator;
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final AuthenticationTrustResolver authTrustResolver =
|
||||
new AuthenticationTrustResolverImpl();
|
||||
|
||||
private final JwtEncoder jwtEncoder;
|
||||
|
||||
private final ExternalUrlSupplier externalUrl;
|
||||
|
||||
private final ReactiveUserDetailsService userDetailsService;
|
||||
|
||||
private final String keyId;
|
||||
|
||||
private Clock clock;
|
||||
|
||||
public PatServiceImpl(RoleService roleService,
|
||||
ReactiveExtensionClient client,
|
||||
ExternalUrlSupplier externalUrl,
|
||||
CryptoService cryptoService, ReactiveUserDetailsService userDetailsService) {
|
||||
this.roleService = roleService;
|
||||
this.client = client;
|
||||
this.externalUrl = externalUrl;
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.clock = Clock.systemUTC();
|
||||
idGenerator = new AlternativeJdkIdGenerator();
|
||||
var jwk = cryptoService.getJwk();
|
||||
this.jwtEncoder = new NimbusJwtEncoder(new ImmutableJWKSet<>(new JWKSet(jwk)));
|
||||
this.keyId = jwk.getKeyID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set clock for testing.
|
||||
*
|
||||
* @param clock the clock to set
|
||||
*/
|
||||
void setClock(Clock clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PersonalAccessToken> create(PersonalAccessToken patRequest) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
// TODO We only allow authenticated users to create PATs.
|
||||
.filter(authTrustResolver::isAuthenticated)
|
||||
.switchIfEmpty(
|
||||
Mono.error(() -> new ServerWebInputException("Authentication required."))
|
||||
)
|
||||
.flatMap(auth ->
|
||||
create(patRequest, auth.getName(), auth.getAuthorities())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PersonalAccessToken> create(PersonalAccessToken patRequest, String username) {
|
||||
return userDetailsService.findByUsername(username)
|
||||
.flatMap(userDetails ->
|
||||
create(patRequest, username, userDetails.getAuthorities())
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<PersonalAccessToken> create(PersonalAccessToken patRequest, String username,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
var patSpec = patRequest.getSpec();
|
||||
// preflight check
|
||||
var expiresAt = patSpec.getExpiresAt();
|
||||
if (expiresAt != null && expiresAt.isBefore(clock.instant())) {
|
||||
return Mono.error(new ServerWebInputException("Invalid expiresAt."));
|
||||
}
|
||||
var roles = patSpec.getRoles();
|
||||
return hasSufficientRoles(authorities, roles)
|
||||
.filter(has -> has)
|
||||
.switchIfEmpty(
|
||||
Mono.error(() -> new ServerWebInputException("Insufficient roles."))
|
||||
)
|
||||
.map(has -> {
|
||||
var pat = new PersonalAccessToken();
|
||||
pat.setMetadata(new Metadata());
|
||||
if (patRequest.getMetadata() != null) {
|
||||
var metadata = patRequest.getMetadata();
|
||||
if (metadata.getName() != null) {
|
||||
pat.getMetadata().setName(metadata.getName());
|
||||
}
|
||||
if (metadata.getGenerateName() != null) {
|
||||
pat.getMetadata().setGenerateName(metadata.getGenerateName());
|
||||
}
|
||||
if (metadata.getLabels() != null) {
|
||||
pat.getMetadata().setLabels(new HashMap<>());
|
||||
pat.getMetadata().getLabels().putAll(metadata.getLabels());
|
||||
}
|
||||
if (metadata.getAnnotations() != null) {
|
||||
pat.getMetadata().setAnnotations(new HashMap<>());
|
||||
pat.getMetadata().getAnnotations()
|
||||
.putAll(metadata.getAnnotations());
|
||||
}
|
||||
if (metadata.getFinalizers() != null) {
|
||||
pat.getMetadata().setFinalizers(new HashSet<>());
|
||||
pat.getMetadata().getFinalizers().addAll(metadata.getFinalizers());
|
||||
}
|
||||
}
|
||||
if (pat.getMetadata().getGenerateName() == null) {
|
||||
pat.getMetadata().setGenerateName("pat-" + username + "-");
|
||||
}
|
||||
pat.getSpec().setUsername(username);
|
||||
pat.getSpec().setName(patSpec.getName());
|
||||
pat.getSpec().setDescription(patSpec.getDescription());
|
||||
if (patSpec.getRoles() != null) {
|
||||
pat.getSpec().setRoles(new ArrayList<>());
|
||||
pat.getSpec().getRoles().addAll(patSpec.getRoles());
|
||||
}
|
||||
if (patSpec.getScopes() != null) {
|
||||
pat.getSpec().setScopes(new ArrayList<>());
|
||||
pat.getSpec().getScopes().addAll(patSpec.getScopes());
|
||||
}
|
||||
pat.getSpec().setExpiresAt(patSpec.getExpiresAt());
|
||||
pat.getSpec().setTokenId(idGenerator.generateId().toString());
|
||||
return pat;
|
||||
})
|
||||
.flatMap(client::create);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PersonalAccessToken> revoke(String patName, String username) {
|
||||
return get(patName, username)
|
||||
.filter(pat -> !pat.getSpec().isRevoked())
|
||||
.switchIfEmpty(Mono.error(
|
||||
() -> new ServerWebInputException("The token has been revoked before."))
|
||||
)
|
||||
.doOnNext(pat -> {
|
||||
pat.getSpec().setRevoked(true);
|
||||
pat.getSpec().setRevokesAt(clock.instant());
|
||||
})
|
||||
.flatMap(client::update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PersonalAccessToken> restore(String patName, String username) {
|
||||
return get(patName, username)
|
||||
.filter(pat -> pat.getSpec().isRevoked())
|
||||
.switchIfEmpty(Mono.error(
|
||||
() -> new ServerWebInputException("The token has not been revoked before."))
|
||||
)
|
||||
.doOnNext(pat -> {
|
||||
pat.getSpec().setRevoked(false);
|
||||
pat.getSpec().setRevokesAt(null);
|
||||
})
|
||||
.flatMap(client::update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PersonalAccessToken> delete(String patName, String username) {
|
||||
return get(patName, username)
|
||||
.flatMap(client::delete);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PersonalAccessToken> get(String patName, String username) {
|
||||
return client.fetch(PersonalAccessToken.class, patName)
|
||||
.filter(pat -> Objects.equals(pat.getSpec().getUsername(), username))
|
||||
.switchIfEmpty(Mono.error(() -> new NotFoundException(
|
||||
"The personal access token was not found or deleted."
|
||||
)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> generateToken(PersonalAccessToken pat) {
|
||||
return Mono.deferContextual(
|
||||
contextView -> {
|
||||
var externalUrl = ServerWebExchangeContextFilter.getExchange(contextView)
|
||||
.map(exchange -> this.externalUrl.getURL(exchange.getRequest()))
|
||||
.orElse(null);
|
||||
if (externalUrl == null) {
|
||||
return Mono.error(new ServerWebInputException("Server web exchange is "
|
||||
+ "required"));
|
||||
}
|
||||
var claimsBuilder = JwtClaimsSet.builder()
|
||||
.issuer(externalUrl.toString())
|
||||
.id(pat.getSpec().getTokenId())
|
||||
.subject(pat.getSpec().getUsername())
|
||||
.issuedAt(clock.instant())
|
||||
.claim("pat_name", pat.getMetadata().getName());
|
||||
var expiresAt = pat.getSpec().getExpiresAt();
|
||||
if (expiresAt != null) {
|
||||
claimsBuilder.expiresAt(expiresAt);
|
||||
}
|
||||
var headerBuilder = JwsHeader.with(SignatureAlgorithm.RS256)
|
||||
.keyId(this.keyId);
|
||||
var jwt = jwtEncoder.encode(JwtEncoderParameters.from(
|
||||
headerBuilder.build(),
|
||||
claimsBuilder.build()));
|
||||
return Mono.just(jwt);
|
||||
}
|
||||
)
|
||||
.map(jwt -> PersonalAccessToken.PAT_TOKEN_PREFIX + jwt.getTokenValue());
|
||||
}
|
||||
|
||||
private Mono<Boolean> hasSufficientRoles(
|
||||
Collection<? extends GrantedAuthority> grantedAuthorities, List<String> requestRoles) {
|
||||
if (CollectionUtils.isEmpty(requestRoles)) {
|
||||
return Mono.just(true);
|
||||
}
|
||||
var grantedRoles = AuthorityUtils.authoritiesToRoles(grantedAuthorities);
|
||||
return roleService.contains(grantedRoles, requestRoles);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package run.halo.app.security.authentication.pat;
|
||||
|
||||
import static run.halo.app.security.PersonalAccessToken.PAT_TOKEN_PREFIX;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
||||
|
@ -15,8 +17,6 @@ import reactor.core.publisher.Mono;
|
|||
*/
|
||||
class PatAuthenticationConverter extends ServerBearerTokenAuthenticationConverter {
|
||||
|
||||
public static final String PAT_TOKEN_PREFIX = "pat_";
|
||||
|
||||
@Override
|
||||
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
||||
return super.convert(exchange)
|
||||
|
|
|
@ -1,91 +1,41 @@
|
|||
package run.halo.app.security.authentication.pat;
|
||||
|
||||
import static run.halo.app.extension.Comparators.compareCreationTimestamp;
|
||||
import static run.halo.app.security.authentication.pat.PatAuthenticationConverter.PAT_TOKEN_PREFIX;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import java.time.Clock;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.AlternativeJdkIdGenerator;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.IdGenerator;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.user.service.RoleService;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.core.user.service.PatService;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.infra.exception.AccessDeniedException;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.security.PersonalAccessToken;
|
||||
import run.halo.app.security.authentication.CryptoService;
|
||||
import run.halo.app.security.authorization.AuthorityUtils;
|
||||
|
||||
@Service
|
||||
class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
||||
|
||||
private static final String ACCESS_TOKEN_ANNO_NAME = "security.halo.run/access-token";
|
||||
|
||||
private static final NotFoundException PAT_NOT_FOUND_EX =
|
||||
new NotFoundException("The personal access token was not found or deleted.");
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final JwtEncoder patEncoder;
|
||||
|
||||
private final ExternalUrlSupplier externalUrl;
|
||||
|
||||
private final RoleService roleService;
|
||||
|
||||
private final IdGenerator idGenerator;
|
||||
|
||||
private final String keyId;
|
||||
|
||||
private Clock clock;
|
||||
private final PatService patService;
|
||||
|
||||
private final AuthenticationTrustResolver authTrustResolver =
|
||||
new AuthenticationTrustResolverImpl();
|
||||
|
||||
public UserScopedPatHandlerImpl(ReactiveExtensionClient client,
|
||||
CryptoService cryptoService,
|
||||
ExternalUrlSupplier externalUrl,
|
||||
RoleService roleService) {
|
||||
PatService patService) {
|
||||
this.client = client;
|
||||
this.externalUrl = externalUrl;
|
||||
this.roleService = roleService;
|
||||
|
||||
var patJwk = cryptoService.getJwk();
|
||||
var jwkSet = new ImmutableJWKSet<>(new JWKSet(patJwk));
|
||||
this.patEncoder = new NimbusJwtEncoder(jwkSet);
|
||||
this.keyId = patJwk.getKeyID();
|
||||
this.idGenerator = new AlternativeJdkIdGenerator();
|
||||
this.clock = Clock.systemDefaultZone();
|
||||
}
|
||||
|
||||
public void setClock(Clock clock) {
|
||||
this.clock = clock;
|
||||
this.patService = patService;
|
||||
}
|
||||
|
||||
private Mono<Authentication> mustBeAuthenticated(Mono<Authentication> authentication) {
|
||||
|
@ -99,71 +49,20 @@ class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
|||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.transform(this::mustBeAuthenticated)
|
||||
.flatMap(auth -> request.bodyToMono(PersonalAccessToken.class)
|
||||
.switchIfEmpty(
|
||||
Mono.error(() -> new ServerWebInputException("Missing request body.")))
|
||||
.flatMap(patRequest -> {
|
||||
var patSpec = patRequest.getSpec();
|
||||
var roles = patSpec.getRoles();
|
||||
var rolesCheck = hasSufficientRoles(auth.getAuthorities(), roles)
|
||||
.filter(has -> has)
|
||||
.switchIfEmpty(
|
||||
Mono.error(() -> new ServerWebInputException("Insufficient roles.")))
|
||||
.then();
|
||||
|
||||
var expiresCheck = Mono.fromRunnable(() -> {
|
||||
var expiresAt = patSpec.getExpiresAt();
|
||||
var now = clock.instant();
|
||||
if (expiresAt != null && (now.isAfter(expiresAt))) {
|
||||
throw new ServerWebInputException("Invalid expiresAt.");
|
||||
}
|
||||
}).then();
|
||||
|
||||
var createPat = Mono.defer(() -> {
|
||||
var pat = new PersonalAccessToken();
|
||||
var spec = pat.getSpec();
|
||||
spec.setUsername(auth.getName());
|
||||
spec.setName(patSpec.getName());
|
||||
spec.setDescription(patSpec.getDescription());
|
||||
spec.setRoles(patSpec.getRoles());
|
||||
spec.setScopes(patSpec.getScopes());
|
||||
spec.setExpiresAt(patSpec.getExpiresAt());
|
||||
var tokenId = idGenerator.generateId().toString();
|
||||
spec.setTokenId(tokenId);
|
||||
var metadata = new Metadata();
|
||||
metadata.setGenerateName("pat-" + auth.getName() + "-");
|
||||
pat.setMetadata(metadata);
|
||||
return client.create(pat)
|
||||
.doOnNext(createdPat -> {
|
||||
var claimsBuilder = JwtClaimsSet.builder()
|
||||
.issuer(externalUrl.getURL(request.exchange().getRequest())
|
||||
.toString())
|
||||
.id(tokenId)
|
||||
.subject(auth.getName())
|
||||
.issuedAt(clock.instant())
|
||||
.claim("pat_name", createdPat.getMetadata().getName());
|
||||
var expiresAt = createdPat.getSpec().getExpiresAt();
|
||||
if (expiresAt != null) {
|
||||
claimsBuilder.expiresAt(expiresAt);
|
||||
}
|
||||
var headerBuilder = JwsHeader.with(SignatureAlgorithm.RS256)
|
||||
.keyId(this.keyId);
|
||||
var jwt = patEncoder.encode(JwtEncoderParameters.from(
|
||||
headerBuilder.build(),
|
||||
claimsBuilder.build()));
|
||||
var annotations =
|
||||
createdPat.getMetadata().getAnnotations();
|
||||
if (annotations == null) {
|
||||
annotations = new HashMap<>();
|
||||
createdPat.getMetadata().setAnnotations(annotations);
|
||||
}
|
||||
annotations.put(ACCESS_TOKEN_ANNO_NAME,
|
||||
PAT_TOKEN_PREFIX + jwt.getTokenValue());
|
||||
});
|
||||
});
|
||||
return rolesCheck.and(expiresCheck).then(createPat)
|
||||
.flatMap(createdPat -> ServerResponse.ok().bodyValue(createdPat));
|
||||
}));
|
||||
.flatMap(auth -> request.bodyToMono(PersonalAccessToken.class))
|
||||
.switchIfEmpty(Mono.error(() -> new ServerWebInputException("Missing request body.")))
|
||||
.flatMap(patService::create)
|
||||
.flatMap(pat -> patService.generateToken(pat)
|
||||
.doOnNext(token -> {
|
||||
if (pat.getMetadata().getAnnotations() == null) {
|
||||
pat.getMetadata().setAnnotations(new HashMap<>());
|
||||
}
|
||||
pat.getMetadata().getAnnotations()
|
||||
.put(ACCESS_TOKEN_ANNO_NAME, token);
|
||||
})
|
||||
.thenReturn(pat)
|
||||
)
|
||||
.flatMap(pat -> ServerResponse.ok().bodyValue(pat));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -185,9 +84,9 @@ class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
|||
.map(SecurityContext::getAuthentication)
|
||||
.flatMap(auth -> {
|
||||
var name = request.pathVariable("name");
|
||||
var pat = getPat(name, auth.getName());
|
||||
return ServerResponse.ok().body(pat, PersonalAccessToken.class);
|
||||
});
|
||||
return patService.get(name, auth.getName());
|
||||
})
|
||||
.flatMap(pat -> ServerResponse.ok().bodyValue(pat));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -196,18 +95,9 @@ class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
|||
.map(SecurityContext::getAuthentication)
|
||||
.flatMap(auth -> {
|
||||
var name = request.pathVariable("name");
|
||||
var revokedPat = getPat(name, auth.getName())
|
||||
.filter(pat -> !pat.getSpec().isRevoked())
|
||||
.switchIfEmpty(Mono.error(
|
||||
() -> new ServerWebInputException("The token has been revoked before.")))
|
||||
.doOnNext(pat -> {
|
||||
var spec = pat.getSpec();
|
||||
spec.setRevoked(true);
|
||||
spec.setRevokesAt(clock.instant());
|
||||
})
|
||||
.flatMap(client::update);
|
||||
return ServerResponse.ok().body(revokedPat, PersonalAccessToken.class);
|
||||
});
|
||||
return patService.revoke(name, auth.getName());
|
||||
})
|
||||
.flatMap(revokedPat -> ServerResponse.ok().bodyValue(revokedPat));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -216,48 +106,21 @@ class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
|||
.map(SecurityContext::getAuthentication)
|
||||
.flatMap(auth -> {
|
||||
var name = request.pathVariable("name");
|
||||
var deletedPat = getPat(name, auth.getName())
|
||||
.flatMap(client::delete);
|
||||
return ServerResponse.ok().body(deletedPat, PersonalAccessToken.class);
|
||||
});
|
||||
return patService.delete(name, auth.getName());
|
||||
})
|
||||
.flatMap(pat -> ServerResponse.ok().bodyValue(pat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ServerResponse> restore(ServerRequest request) {
|
||||
var restoredPat = ReactiveSecurityContextHolder.getContext()
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.transform(this::mustBeAuthenticated)
|
||||
.flatMap(auth -> {
|
||||
var name = request.pathVariable("name");
|
||||
return getPat(name, auth.getName());
|
||||
return patService.restore(name, auth.getName());
|
||||
})
|
||||
.filter(pat -> pat.getSpec().isRevoked())
|
||||
.switchIfEmpty(Mono.error(
|
||||
() -> new ServerWebInputException(
|
||||
"The token has not been revoked before.")))
|
||||
.doOnNext(pat -> {
|
||||
var spec = pat.getSpec();
|
||||
spec.setRevoked(false);
|
||||
spec.setRevokesAt(null);
|
||||
})
|
||||
.flatMap(client::update);
|
||||
return ServerResponse.ok().body(restoredPat, PersonalAccessToken.class);
|
||||
.flatMap(pat -> ServerResponse.ok().bodyValue(pat));
|
||||
}
|
||||
|
||||
private Mono<Boolean> hasSufficientRoles(
|
||||
Collection<? extends GrantedAuthority> grantedAuthorities, List<String> requestRoles) {
|
||||
if (CollectionUtils.isEmpty(requestRoles)) {
|
||||
return Mono.just(true);
|
||||
}
|
||||
var grantedRoles = AuthorityUtils.authoritiesToRoles(grantedAuthorities);
|
||||
return roleService.contains(grantedRoles, requestRoles);
|
||||
}
|
||||
|
||||
private Mono<PersonalAccessToken> getPat(String name, String username) {
|
||||
return client.get(PersonalAccessToken.class, name)
|
||||
.filter(pat -> Objects.equals(pat.getSpec().getUsername(), username)
|
||||
&& !ExtensionUtil.isDeleted(pat))
|
||||
.onErrorMap(ExtensionNotFoundException.class, t -> PAT_NOT_FOUND_EX)
|
||||
.switchIfEmpty(Mono.error(() -> PAT_NOT_FOUND_EX));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue