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 KIND = "PersonalAccessToken";
|
||||||
|
|
||||||
|
public static final String PAT_TOKEN_PREFIX = "pat_";
|
||||||
|
|
||||||
private Spec spec = new Spec();
|
private Spec spec = new Spec();
|
||||||
|
|
||||||
@Data
|
@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;
|
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.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
||||||
|
@ -15,8 +17,6 @@ import reactor.core.publisher.Mono;
|
||||||
*/
|
*/
|
||||||
class PatAuthenticationConverter extends ServerBearerTokenAuthenticationConverter {
|
class PatAuthenticationConverter extends ServerBearerTokenAuthenticationConverter {
|
||||||
|
|
||||||
public static final String PAT_TOKEN_PREFIX = "pat_";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
||||||
return super.convert(exchange)
|
return super.convert(exchange)
|
||||||
|
|
|
@ -1,91 +1,41 @@
|
||||||
package run.halo.app.security.authentication.pat;
|
package run.halo.app.security.authentication.pat;
|
||||||
|
|
||||||
import static run.halo.app.extension.Comparators.compareCreationTimestamp;
|
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.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||||
import org.springframework.security.core.Authentication;
|
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.ReactiveSecurityContextHolder;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
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.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.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.user.service.RoleService;
|
import run.halo.app.core.user.service.PatService;
|
||||||
import run.halo.app.extension.ExtensionUtil;
|
|
||||||
import run.halo.app.extension.Metadata;
|
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
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.AccessDeniedException;
|
||||||
import run.halo.app.infra.exception.NotFoundException;
|
|
||||||
import run.halo.app.security.PersonalAccessToken;
|
import run.halo.app.security.PersonalAccessToken;
|
||||||
import run.halo.app.security.authentication.CryptoService;
|
|
||||||
import run.halo.app.security.authorization.AuthorityUtils;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
||||||
|
|
||||||
private static final String ACCESS_TOKEN_ANNO_NAME = "security.halo.run/access-token";
|
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 ReactiveExtensionClient client;
|
||||||
|
|
||||||
private final JwtEncoder patEncoder;
|
private final PatService patService;
|
||||||
|
|
||||||
private final ExternalUrlSupplier externalUrl;
|
|
||||||
|
|
||||||
private final RoleService roleService;
|
|
||||||
|
|
||||||
private final IdGenerator idGenerator;
|
|
||||||
|
|
||||||
private final String keyId;
|
|
||||||
|
|
||||||
private Clock clock;
|
|
||||||
|
|
||||||
private final AuthenticationTrustResolver authTrustResolver =
|
private final AuthenticationTrustResolver authTrustResolver =
|
||||||
new AuthenticationTrustResolverImpl();
|
new AuthenticationTrustResolverImpl();
|
||||||
|
|
||||||
public UserScopedPatHandlerImpl(ReactiveExtensionClient client,
|
public UserScopedPatHandlerImpl(ReactiveExtensionClient client,
|
||||||
CryptoService cryptoService,
|
PatService patService) {
|
||||||
ExternalUrlSupplier externalUrl,
|
|
||||||
RoleService roleService) {
|
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.externalUrl = externalUrl;
|
this.patService = patService;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Authentication> mustBeAuthenticated(Mono<Authentication> authentication) {
|
private Mono<Authentication> mustBeAuthenticated(Mono<Authentication> authentication) {
|
||||||
|
@ -99,71 +49,20 @@ class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
||||||
return ReactiveSecurityContextHolder.getContext()
|
return ReactiveSecurityContextHolder.getContext()
|
||||||
.map(SecurityContext::getAuthentication)
|
.map(SecurityContext::getAuthentication)
|
||||||
.transform(this::mustBeAuthenticated)
|
.transform(this::mustBeAuthenticated)
|
||||||
.flatMap(auth -> request.bodyToMono(PersonalAccessToken.class)
|
.flatMap(auth -> request.bodyToMono(PersonalAccessToken.class))
|
||||||
.switchIfEmpty(
|
.switchIfEmpty(Mono.error(() -> new ServerWebInputException("Missing request body.")))
|
||||||
Mono.error(() -> new ServerWebInputException("Missing request body.")))
|
.flatMap(patService::create)
|
||||||
.flatMap(patRequest -> {
|
.flatMap(pat -> patService.generateToken(pat)
|
||||||
var patSpec = patRequest.getSpec();
|
.doOnNext(token -> {
|
||||||
var roles = patSpec.getRoles();
|
if (pat.getMetadata().getAnnotations() == null) {
|
||||||
var rolesCheck = hasSufficientRoles(auth.getAuthorities(), roles)
|
pat.getMetadata().setAnnotations(new HashMap<>());
|
||||||
.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();
|
pat.getMetadata().getAnnotations()
|
||||||
|
.put(ACCESS_TOKEN_ANNO_NAME, token);
|
||||||
var createPat = Mono.defer(() -> {
|
})
|
||||||
var pat = new PersonalAccessToken();
|
.thenReturn(pat)
|
||||||
var spec = pat.getSpec();
|
)
|
||||||
spec.setUsername(auth.getName());
|
.flatMap(pat -> ServerResponse.ok().bodyValue(pat));
|
||||||
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));
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -185,9 +84,9 @@ class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
||||||
.map(SecurityContext::getAuthentication)
|
.map(SecurityContext::getAuthentication)
|
||||||
.flatMap(auth -> {
|
.flatMap(auth -> {
|
||||||
var name = request.pathVariable("name");
|
var name = request.pathVariable("name");
|
||||||
var pat = getPat(name, auth.getName());
|
return patService.get(name, auth.getName());
|
||||||
return ServerResponse.ok().body(pat, PersonalAccessToken.class);
|
})
|
||||||
});
|
.flatMap(pat -> ServerResponse.ok().bodyValue(pat));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -196,18 +95,9 @@ class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
||||||
.map(SecurityContext::getAuthentication)
|
.map(SecurityContext::getAuthentication)
|
||||||
.flatMap(auth -> {
|
.flatMap(auth -> {
|
||||||
var name = request.pathVariable("name");
|
var name = request.pathVariable("name");
|
||||||
var revokedPat = getPat(name, auth.getName())
|
return patService.revoke(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);
|
.flatMap(revokedPat -> ServerResponse.ok().bodyValue(revokedPat));
|
||||||
return ServerResponse.ok().body(revokedPat, PersonalAccessToken.class);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -216,48 +106,21 @@ class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
||||||
.map(SecurityContext::getAuthentication)
|
.map(SecurityContext::getAuthentication)
|
||||||
.flatMap(auth -> {
|
.flatMap(auth -> {
|
||||||
var name = request.pathVariable("name");
|
var name = request.pathVariable("name");
|
||||||
var deletedPat = getPat(name, auth.getName())
|
return patService.delete(name, auth.getName());
|
||||||
.flatMap(client::delete);
|
})
|
||||||
return ServerResponse.ok().body(deletedPat, PersonalAccessToken.class);
|
.flatMap(pat -> ServerResponse.ok().bodyValue(pat));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ServerResponse> restore(ServerRequest request) {
|
public Mono<ServerResponse> restore(ServerRequest request) {
|
||||||
var restoredPat = ReactiveSecurityContextHolder.getContext()
|
return ReactiveSecurityContextHolder.getContext()
|
||||||
.map(SecurityContext::getAuthentication)
|
.map(SecurityContext::getAuthentication)
|
||||||
.transform(this::mustBeAuthenticated)
|
.transform(this::mustBeAuthenticated)
|
||||||
.flatMap(auth -> {
|
.flatMap(auth -> {
|
||||||
var name = request.pathVariable("name");
|
var name = request.pathVariable("name");
|
||||||
return getPat(name, auth.getName());
|
return patService.restore(name, auth.getName());
|
||||||
})
|
})
|
||||||
.filter(pat -> pat.getSpec().isRevoked())
|
.flatMap(pat -> ServerResponse.ok().bodyValue(pat));
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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