mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-02-17 15:01:42 +08:00
feat: implement @RequiredAal and @Privileged annotations to secure controllers
This commit is contained in:
parent
1ba8f4fd2b
commit
049a882958
@ -40,6 +40,8 @@ import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.exceptions.HangarResponseException;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
import io.papermc.hangar.security.annotations.Anyone;
|
||||
import io.papermc.hangar.security.annotations.aal.RequireAal;
|
||||
import io.papermc.hangar.security.annotations.privileged.Privileged;
|
||||
import io.papermc.hangar.security.annotations.ratelimit.RateLimit;
|
||||
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
|
||||
import io.papermc.hangar.service.internal.users.UserService;
|
||||
@ -106,7 +108,8 @@ public class CredentialController extends HangarComponent {
|
||||
* WEBAUTHN
|
||||
*/
|
||||
|
||||
@Unlocked
|
||||
@Privileged
|
||||
@RequireAal(1)
|
||||
@PostMapping(value = "/webauthn/setup", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.TEXT_PLAIN_VALUE)
|
||||
public String setupWebauthn(@RequestBody final String authenticatorName) throws JsonProcessingException {
|
||||
// TODO verify that backup codes exist
|
||||
@ -130,7 +133,8 @@ public class CredentialController extends HangarComponent {
|
||||
return response.publicKeyCredentialCreationOptions().toCredentialsCreateJson();
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@Privileged
|
||||
@RequireAal(1)
|
||||
@PostMapping(value = "/webauthn/register", consumes = MediaType.TEXT_PLAIN_VALUE)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public void registerWebauthn(@RequestBody final String publicKeyCredentialJson, @RequestHeader(value = "X-Hangar-Verify", required = false) final String header) throws IOException {
|
||||
@ -176,7 +180,8 @@ public class CredentialController extends HangarComponent {
|
||||
return assertionRequest.toCredentialsGetJson();
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@Privileged
|
||||
@RequireAal(1)
|
||||
@PostMapping(value = "/webauthn/unregister", consumes = MediaType.TEXT_PLAIN_VALUE)
|
||||
public void unregisterWebauthnDevice(@RequestBody final String id) {
|
||||
this.webAuthNService.removeDevice(this.getHangarPrincipal().getUserId(), id);
|
||||
@ -187,7 +192,8 @@ public class CredentialController extends HangarComponent {
|
||||
* TOTP
|
||||
*/
|
||||
|
||||
@Unlocked
|
||||
@Privileged
|
||||
@RequireAal(1)
|
||||
@PostMapping("/totp/setup")
|
||||
public TotpSetupResponse setupTotp() throws QrGenerationException {
|
||||
final String secret = this.secretGenerator.generate();
|
||||
@ -206,7 +212,8 @@ public class CredentialController extends HangarComponent {
|
||||
return new TotpSetupResponse(secret, qrCodeImage);
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@Privileged
|
||||
@RequireAal(1)
|
||||
@PostMapping("/totp/register")
|
||||
public ResponseEntity<?> registerTotp(@RequestBody final TotpForm form, @RequestHeader(value = "X-Hangar-Verify", required = false) final String header) {
|
||||
final boolean confirmCodes = this.verifyBackupCodes(header);
|
||||
@ -230,7 +237,8 @@ public class CredentialController extends HangarComponent {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@Privileged
|
||||
@RequireAal(1)
|
||||
@PostMapping("/totp/remove")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public void removeTotp() {
|
||||
@ -239,7 +247,8 @@ public class CredentialController extends HangarComponent {
|
||||
this.credentialsService.checkRemoveBackupCodes();
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@Privileged
|
||||
@RequireAal(1)
|
||||
@PostMapping("/totp/verify")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public void verifyTotp(@RequestBody final String code) {
|
||||
@ -298,7 +307,8 @@ public class CredentialController extends HangarComponent {
|
||||
return codes;
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@Privileged
|
||||
@RequireAal(1)
|
||||
@PostMapping("/codes/show")
|
||||
public List<BackupCodeCredential.BackupCode> showBackupCodes() {
|
||||
// TODO security protection
|
||||
@ -309,7 +319,8 @@ public class CredentialController extends HangarComponent {
|
||||
return cred.backupCodes();
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@Privileged
|
||||
@RequireAal(1)
|
||||
@PostMapping("/codes/regenerate")
|
||||
public List<BackupCodeCredential.BackupCode> regenerateBackupCodes() {
|
||||
// TODO security protection
|
||||
|
@ -0,0 +1,24 @@
|
||||
package io.papermc.hangar.security.annotations.aal;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import org.springframework.security.access.ConfigAttribute;
|
||||
import org.springframework.security.access.annotation.AnnotationMetadataExtractor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AalMetadataExtractor implements AnnotationMetadataExtractor<RequireAal> {
|
||||
|
||||
@Override
|
||||
public Collection<? extends ConfigAttribute> extractAttributes(final RequireAal annotation) {
|
||||
return Set.of(new AalAttribute(annotation.value()));
|
||||
}
|
||||
|
||||
record AalAttribute(int aal) implements ConfigAttribute {
|
||||
|
||||
@Override
|
||||
public String getAttribute() {
|
||||
return String.valueOf(this.aal);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package io.papermc.hangar.security.annotations.aal;
|
||||
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.security.annotations.HangarDecisionVoter;
|
||||
import io.papermc.hangar.security.authentication.HangarAuthenticationToken;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AalUnlockedVoter extends HangarDecisionVoter<AalMetadataExtractor.AalAttribute> {
|
||||
|
||||
public AalUnlockedVoter() {
|
||||
super(AalMetadataExtractor.AalAttribute.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int vote(final Authentication authentication, final MethodInvocation object, final @NotNull AalMetadataExtractor.AalAttribute attribute) {
|
||||
if (!(authentication instanceof HangarAuthenticationToken)) {
|
||||
return ACCESS_DENIED;
|
||||
}
|
||||
final int aal = ((HangarAuthenticationToken) authentication).getPrincipal().getAal();
|
||||
if (aal < attribute.aal()) {
|
||||
if (attribute.aal() == 1) {
|
||||
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "error.aal1");
|
||||
} else {
|
||||
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "error.aal2");
|
||||
}
|
||||
}
|
||||
return ACCESS_GRANTED;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package io.papermc.hangar.security.annotations.aal;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
|
||||
/**
|
||||
* Require the user be logged in AND have a certain aal
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Secured("ROLE_USER")
|
||||
public @interface RequireAal {
|
||||
|
||||
int value();
|
||||
}
|
@ -22,8 +22,8 @@ public class CurrentUserMetadataExtractor implements AnnotationMetadataExtractor
|
||||
record CurrentUserAttribute(Expression expression) implements ConfigAttribute {
|
||||
|
||||
@Override
|
||||
public String getAttribute() {
|
||||
return null;
|
||||
}
|
||||
public String getAttribute() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ public class CurrentUserVoter extends HangarDecisionVoter<CurrentUserMetadataExt
|
||||
|
||||
@Override
|
||||
public int vote(final Authentication authentication, final MethodInvocation methodInvocation, final @NotNull CurrentUserMetadataExtractor.CurrentUserAttribute attribute) {
|
||||
if (!(authentication instanceof HangarAuthenticationToken hangarAuthenticationToken)) {
|
||||
if (!(authentication instanceof final HangarAuthenticationToken hangarAuthenticationToken)) {
|
||||
return ACCESS_DENIED;
|
||||
}
|
||||
if (hangarAuthenticationToken.getPrincipal().isAllowedGlobal(Permission.EditAllUserSettings)) {
|
||||
|
@ -29,8 +29,8 @@ public class PermissionRequiredMetadataExtractor implements AnnotationMetadataEx
|
||||
record PermissionRequiredAttribute(PermissionType permissionType, NamedPermission[] permissions, Expression expression) implements ConfigAttribute {
|
||||
|
||||
@Override
|
||||
public String getAttribute() {
|
||||
return null;
|
||||
}
|
||||
public String getAttribute() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public class PermissionRequiredVoter extends HangarDecisionVoter<PermissionRequi
|
||||
|
||||
@Override
|
||||
public int vote(final Authentication authentication, final MethodInvocation methodInvocation, final Set<PermissionRequiredMetadataExtractor.PermissionRequiredAttribute> attributes) {
|
||||
if (!(authentication instanceof HangarAuthenticationToken hangarAuthenticationToken)) {
|
||||
if (!(authentication instanceof final HangarAuthenticationToken hangarAuthenticationToken)) {
|
||||
throw new HangarApiException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
for (final PermissionRequiredMetadataExtractor.PermissionRequiredAttribute attribute : attributes) {
|
||||
|
@ -0,0 +1,16 @@
|
||||
package io.papermc.hangar.security.annotations.privileged;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
|
||||
/**
|
||||
* Require the user be logged in AND unlocked
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Secured("ROLE_USER")
|
||||
public @interface Privileged {
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package io.papermc.hangar.security.annotations.privileged;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import org.springframework.security.access.ConfigAttribute;
|
||||
import org.springframework.security.access.annotation.AnnotationMetadataExtractor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PrivilegedMetadataExtractor implements AnnotationMetadataExtractor<Privileged> {
|
||||
|
||||
@Override
|
||||
public Collection<? extends ConfigAttribute> extractAttributes(final Privileged securityAnnotation) {
|
||||
return Set.of(PrivilegedUnlockedAttribute.INSTANCE);
|
||||
}
|
||||
|
||||
static final class PrivilegedUnlockedAttribute implements ConfigAttribute {
|
||||
|
||||
static final PrivilegedUnlockedAttribute INSTANCE = new PrivilegedUnlockedAttribute();
|
||||
|
||||
private PrivilegedUnlockedAttribute() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttribute() {
|
||||
return "PRIVILEGED";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package io.papermc.hangar.security.annotations.privileged;
|
||||
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.security.annotations.HangarDecisionVoter;
|
||||
import io.papermc.hangar.security.authentication.HangarAuthenticationToken;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PrivilegedVoter extends HangarDecisionVoter<PrivilegedMetadataExtractor.PrivilegedUnlockedAttribute> {
|
||||
|
||||
public PrivilegedVoter() {
|
||||
super(PrivilegedMetadataExtractor.PrivilegedUnlockedAttribute.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int vote(final Authentication authentication, final MethodInvocation object, final @NotNull PrivilegedMetadataExtractor.PrivilegedUnlockedAttribute attribute) {
|
||||
if (!(authentication instanceof HangarAuthenticationToken)) {
|
||||
return ACCESS_DENIED;
|
||||
}
|
||||
if (!((HangarAuthenticationToken) authentication).getPrincipal().isPrivileged()) {
|
||||
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "error.privileged");
|
||||
}
|
||||
return ACCESS_GRANTED;
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ public class UnlockedMetadataExtractor implements AnnotationMetadataExtractor<Un
|
||||
return Set.of(UnlockedAttribute.INSTANCE);
|
||||
}
|
||||
|
||||
static class UnlockedAttribute implements ConfigAttribute {
|
||||
static final class UnlockedAttribute implements ConfigAttribute {
|
||||
|
||||
static final UnlockedAttribute INSTANCE = new UnlockedAttribute();
|
||||
|
||||
|
@ -22,8 +22,8 @@ public class VisibilityRequiredMetadataExtractor implements AnnotationMetadataEx
|
||||
record VisibilityRequiredAttribute(VisibilityRequired.Type type, Expression expression) implements ConfigAttribute {
|
||||
|
||||
@Override
|
||||
public String getAttribute() {
|
||||
return null;
|
||||
}
|
||||
public String getAttribute() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1115,6 +1115,9 @@
|
||||
"401": "You must be logged in for this",
|
||||
"403": "You do not have permission to do that",
|
||||
"404": "404 Not found",
|
||||
"unknown": "An error occurred"
|
||||
"unknown": "An error occurred",
|
||||
"aal1": "You have to verify your email to do this",
|
||||
"aal2": "You have to setup 2fa to do this",
|
||||
"privileged": "You need to be in a privileged session to do this"
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ async function loginBackupCode() {
|
||||
}
|
||||
|
||||
async function finish(response: LoginResponse) {
|
||||
if (response.aal && response.user?.accessToken) {
|
||||
if (response.aal !== undefined && response.user?.accessToken) {
|
||||
authStore.aal = response.aal;
|
||||
authStore.user = response.user;
|
||||
authStore.authenticated = true;
|
||||
|
Loading…
Reference in New Issue
Block a user