mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-03-13 15:39:18 +08:00
api key authentication
This commit is contained in:
parent
8009325e2f
commit
7b490320fa
@ -2,6 +2,7 @@ package io.papermc.hangar.controller.api.v1;
|
||||
|
||||
import io.papermc.hangar.controller.api.v1.interfaces.IApiKeysController;
|
||||
import io.papermc.hangar.model.internal.api.requests.CreateAPIKeyForm;
|
||||
import io.papermc.hangar.security.annotations.permission.PermissionRequired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
@ -13,6 +14,7 @@ public class ApiKeysController implements IApiKeysController {
|
||||
@Override
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@PermissionRequired()
|
||||
public String createKey(CreateAPIKeyForm apiKeyForm) {
|
||||
// TODO implement
|
||||
System.out.println(apiKeyForm);
|
||||
|
@ -3,24 +3,25 @@ package io.papermc.hangar.controller.api.v1;
|
||||
import io.papermc.hangar.controller.HangarController;
|
||||
import io.papermc.hangar.controller.api.v1.interfaces.IAuthenticationController;
|
||||
import io.papermc.hangar.model.api.auth.ApiSession;
|
||||
import io.papermc.hangar.model.api.requests.SessionProperties;
|
||||
import io.papermc.hangar.service.AuthenticationService;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import io.papermc.hangar.security.annotations.Anyone;
|
||||
import io.papermc.hangar.service.api.APIAuthenticationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class AuthenticationController extends HangarController implements IAuthenticationController {
|
||||
|
||||
private final AuthenticationService authenticationService;
|
||||
private final APIAuthenticationService apiAuthenticationService;
|
||||
|
||||
public AuthenticationController(AuthenticationService authenticationService) {
|
||||
this.authenticationService = authenticationService;
|
||||
@Autowired
|
||||
public AuthenticationController(APIAuthenticationService apiAuthenticationService) {
|
||||
this.apiAuthenticationService = apiAuthenticationService;
|
||||
}
|
||||
|
||||
// TODO JWT
|
||||
@Anyone
|
||||
@Override
|
||||
public ResponseEntity<ApiSession> authenticate(SessionProperties body) {
|
||||
throw new NotImplementedException("Setup JWT here");
|
||||
public ResponseEntity<ApiSession> authenticate(String apiKey) {
|
||||
return ResponseEntity.ok(apiAuthenticationService.createJWTForApiKey(apiKey));
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,5 @@
|
||||
package io.papermc.hangar.controller.api.v1;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
import io.papermc.hangar.controller.HangarController;
|
||||
import io.papermc.hangar.controller.api.v1.interfaces.IProjectsController;
|
||||
import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableFilters;
|
||||
@ -22,7 +14,16 @@ import io.papermc.hangar.model.api.project.Project;
|
||||
import io.papermc.hangar.model.api.project.ProjectMember;
|
||||
import io.papermc.hangar.model.api.project.ProjectSortingStrategy;
|
||||
import io.papermc.hangar.model.api.requests.RequestPagination;
|
||||
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
|
||||
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired.Type;
|
||||
import io.papermc.hangar.service.api.ProjectsApiService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
public class ProjectsController extends HangarController implements IProjectsController {
|
||||
@ -35,11 +36,13 @@ public class ProjectsController extends HangarController implements IProjectsCon
|
||||
}
|
||||
|
||||
@Override
|
||||
@VisibilityRequired(type = Type.PROJECT, args = "{#author, #slug}")
|
||||
public ResponseEntity<Project> getProject(String author, String slug) {
|
||||
return ResponseEntity.ok(projectsApiService.getProject(author, slug));
|
||||
}
|
||||
|
||||
@Override
|
||||
@VisibilityRequired(type = Type.PROJECT, args = "{#author, #slug}")
|
||||
public ResponseEntity<PaginatedResult<ProjectMember>> getProjectMembers(String author, String slug, @NotNull RequestPagination pagination) {
|
||||
return ResponseEntity.ok(projectsApiService.getProjectMembers(author, slug, pagination));
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package io.papermc.hangar.controller.api.v1.interfaces;
|
||||
|
||||
import io.papermc.hangar.model.api.auth.ApiSession;
|
||||
import io.papermc.hangar.model.api.requests.SessionProperties;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
@ -11,26 +10,26 @@ import io.swagger.annotations.Authorization;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Api(tags = "Sessions (Authentication)", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@RequestMapping(path = "/api/v1", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST)
|
||||
@Api(tags = "Sessions (Authentication)", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@RequestMapping(path = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST)
|
||||
public interface IAuthenticationController {
|
||||
|
||||
@ApiOperation(
|
||||
value = "Creates an API session",
|
||||
value = "Creates an API JWT",
|
||||
nickname = "authenticate",
|
||||
notes = "Creates a new API session. Pass an API key to create an authenticated session. To create a public session, don't pass an Authorization header. When passing an API key, you should use the scheme `HangarApi`, and parameter `apikey`. An example would be `Authorization: HangarApi apikey=\"foobar\"`. The returned session should be specified in all following request as the parameter `session`. An example would be `Authorization: HangarApi session=\"noisses\"`",
|
||||
notes = "Creates a new API JWT. Pass an API key to create an authenticated session. When passing an API key, you should use the scheme `HangarAuth`, and parameter `apikey`. An example would be `Authorization: HangarAuth <apikey>`. The returned JWT should be specified in all following request as the parameter `token`.",
|
||||
authorizations = @Authorization(value = "Key"),
|
||||
tags = "Sessions (Authentication)"
|
||||
)
|
||||
@ApiResponses({
|
||||
@ApiResponse(code = 200, message = "Ok"),
|
||||
@ApiResponse(code = 400, message = "Sent if the requested expiration can't be used."),
|
||||
@ApiResponse(code = 400, message = "Bad Request"),
|
||||
@ApiResponse(code = 401, message = "Api key missing or invalid")
|
||||
})
|
||||
@PostMapping("/authenticate")
|
||||
ResponseEntity<ApiSession> authenticate(@ApiParam("Session properties") @RequestBody(required = false) SessionProperties body);
|
||||
ResponseEntity<ApiSession> authenticate(@ApiParam("JWT") @RequestParam String apiKey);
|
||||
}
|
||||
|
@ -153,11 +153,11 @@ public class ProjectController extends HangarController {
|
||||
}
|
||||
|
||||
@Anyone
|
||||
@VisibilityRequired(type = Type.PROJECT, args = "{#author, #name}")
|
||||
@GetMapping(value = "/project/{author}/{name}/icon", produces = {MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE})
|
||||
public Object getProjectIcon(@PathVariable String author, @PathVariable String name) {
|
||||
@VisibilityRequired(type = Type.PROJECT, args = "{#author, #slug}")
|
||||
@GetMapping(value = "/project/{author}/{slug}/icon", produces = {MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE})
|
||||
public Object getProjectIcon(@PathVariable String author, @PathVariable String slug) {
|
||||
try {
|
||||
return imageService.getProjectIcon(author, name);
|
||||
return imageService.getProjectIcon(author, slug);
|
||||
} catch (InternalHangarException e) {
|
||||
return new RedirectView(imageService.getUserIcon(author));
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RegisterColumnMapper(PermissionMapper.class)
|
||||
@RegisterConstructorMapper(ApiKeyTable.class)
|
||||
public interface ApiKeyDAO {
|
||||
|
||||
@ -21,11 +22,12 @@ public interface ApiKeyDAO {
|
||||
@SqlUpdate("DELETE FROM api_keys WHERE name = :keyName AND owner_id = :userId")
|
||||
int delete(String keyName, long userId);
|
||||
|
||||
@RegisterColumnMapper(PermissionMapper.class)
|
||||
@SqlQuery("SELECT *, raw_key_permissions::bigint permissions FROM api_keys WHERE owner_id = :userId AND lower(name) = lower(:name)")
|
||||
ApiKeyTable getByUserAndName(long userId, String name);
|
||||
|
||||
// @SqlQuery("SELECT *, raw_key_permissions::BIGINT permissions FROM api_keys k WHERE k.token_identifier = :identifier AND k.token = crypt(:token, k.token)")
|
||||
// ApiKeyTable findApiKey(String identifier, String token);
|
||||
// 1318e930-ef32-4034-88bd-967285a9d28b.f22f2f94-e3a5-496c-8ff7-5230ed16c8a6
|
||||
@SqlQuery("SELECT *, raw_key_permissions::bigint permissions FROM api_keys WHERE token_identifier = :identifier AND token = crypt(:token, token)")
|
||||
ApiKeyTable findApiKey(String identifier, String token);
|
||||
|
||||
@SqlQuery("SELECT *, raw_key_permissions::bigint permissions FROM api_keys WHERE owner_id = :userId AND token_identifier = :identifier")
|
||||
ApiKeyTable findApiKey(long userId, String identifier);
|
||||
}
|
||||
|
@ -118,8 +118,8 @@ public interface ProjectsApiDAO {
|
||||
" JOIN roles r ON upr.role_type = r.name " +
|
||||
" WHERE p.slug = :slug AND p.owner_name = :author " +
|
||||
" GROUP BY u.name ORDER BY max(r.permission::BIGINT) DESC " +
|
||||
" LIMIT :limit OFFSET :offset")
|
||||
List<ProjectMember> getProjectMembers(String author, String slug, long limit, long offset);
|
||||
" <offsetLimit>")
|
||||
List<ProjectMember> getProjectMembers(String author, String slug, @BindPagination RequestPagination pagination);
|
||||
|
||||
@SqlQuery("SELECT count(*) " +
|
||||
" FROM projects p " +
|
||||
|
@ -1,62 +1,28 @@
|
||||
package io.papermc.hangar.model.api.auth;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public class ApiSession {
|
||||
|
||||
private final String session;
|
||||
private final OffsetDateTime expires;
|
||||
private final SessionType type;
|
||||
private final String token;
|
||||
private final long expiresIn;
|
||||
|
||||
public ApiSession(String session, OffsetDateTime expires, SessionType type) {
|
||||
this.session = session;
|
||||
this.expires = expires;
|
||||
this.type = type;
|
||||
public ApiSession(String token, long expiresIn) {
|
||||
this.token = token;
|
||||
this.expiresIn = expiresIn;
|
||||
}
|
||||
|
||||
public String getSession() {
|
||||
return session;
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public OffsetDateTime getExpires() {
|
||||
return expires;
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public SessionType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public enum SessionType {
|
||||
KEY("key"),
|
||||
USER("user"),
|
||||
PUBLIC("public"),
|
||||
DEV("dev");
|
||||
|
||||
public static final SessionType[] VALUES = values();
|
||||
|
||||
private final String value;
|
||||
|
||||
SessionType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static SessionType fromValue(String value) {
|
||||
for (SessionType sessionType : SessionType.VALUES) {
|
||||
if (sessionType.value.equals(value)) {
|
||||
return sessionType;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ApiSession{" +
|
||||
"token='" + token + '\'' +
|
||||
", expiresIn=" + expiresIn +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +77,10 @@ public class Permission implements Comparable<Permission>, Argument {
|
||||
return new Permission(value | other.value);
|
||||
}
|
||||
|
||||
public Permission intersect(Permission other) {
|
||||
return new Permission(value & other.value);
|
||||
}
|
||||
|
||||
public Permission remove(Permission other) {
|
||||
return new Permission(value & ~other.value);
|
||||
}
|
||||
|
@ -1,24 +1,46 @@
|
||||
package io.papermc.hangar.security.annotations;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.context.expression.MethodBasedEvaluationContext;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.security.access.AccessDecisionVoter;
|
||||
import org.springframework.security.access.ConfigAttribute;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Must override one of
|
||||
* <ul>
|
||||
* <li>{@link #vote(Authentication, MethodInvocation, ConfigAttribute)}</li>
|
||||
* <li>{@link #vote(Authentication, MethodInvocation, Set)}</li>
|
||||
* </ul>
|
||||
* @param <A>
|
||||
*/
|
||||
public abstract class HangarDecisionVoter<A extends ConfigAttribute> implements AccessDecisionVoter<MethodInvocation> {
|
||||
|
||||
private final Class<A> attributeClass;
|
||||
protected final ParameterNameDiscoverer parameterNameDiscoverer;
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer;
|
||||
private boolean allowMultipleAttributes = false;
|
||||
private MethodBasedEvaluationContext evaluationContext = null;
|
||||
|
||||
protected HangarDecisionVoter(Class<A> attributeClass) {
|
||||
this.attributeClass = attributeClass;
|
||||
this.parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
}
|
||||
|
||||
public boolean isAllowMultipleAttributes() {
|
||||
return allowMultipleAttributes;
|
||||
}
|
||||
|
||||
public void setAllowMultipleAttributes(boolean allowMultipleAttributes) {
|
||||
this.allowMultipleAttributes = allowMultipleAttributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(ConfigAttribute attribute) {
|
||||
return attributeClass.isAssignableFrom(attribute.getClass());
|
||||
@ -29,18 +51,43 @@ public abstract class HangarDecisionVoter<A extends ConfigAttribute> implements
|
||||
return MethodInvocation.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected final A findAttribute(Collection<ConfigAttribute> attributes) {
|
||||
for (ConfigAttribute attribute : attributes) {
|
||||
if (attributeClass.isAssignableFrom(attribute.getClass())) {
|
||||
return (A) attribute;
|
||||
}
|
||||
@Override
|
||||
public final int vote(Authentication authentication, MethodInvocation methodInvocation, Collection<ConfigAttribute> configAttributes) {
|
||||
Set<A> attributes = findAttributes(configAttributes);
|
||||
if (attributes.isEmpty()) {
|
||||
return ACCESS_ABSTAIN;
|
||||
}
|
||||
if (!allowMultipleAttributes && attributes.size() > 1) {
|
||||
throw new IllegalArgumentException("Multiple " + attributeClass + " found where only 1 is allowed (or use setAllowMultipleAttributes(true))");
|
||||
} else if (allowMultipleAttributes) {
|
||||
return vote(authentication, methodInvocation, attributes);
|
||||
} else {
|
||||
return vote(authentication, methodInvocation, attributes.stream().findFirst().get());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected final Collection<A> findAttributes(Collection<ConfigAttribute> attributes) {
|
||||
public int vote(Authentication authentication, MethodInvocation methodInvocation, @NotNull A attribute) {
|
||||
return ACCESS_ABSTAIN;
|
||||
}
|
||||
|
||||
public int vote(Authentication authentication, MethodInvocation methodInvocation, Set<A> attributes) {
|
||||
return ACCESS_ABSTAIN;
|
||||
}
|
||||
|
||||
protected final Set<A> findAttributes(Collection<ConfigAttribute> attributes) {
|
||||
return attributes.stream().filter(a -> attributeClass.isAssignableFrom(a.getClass())).map(attributeClass::cast).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected final MethodBasedEvaluationContext getMethodEvaluationContext(MethodInvocation invocation) {
|
||||
if (this.evaluationContext == null) {
|
||||
this.evaluationContext = new MethodBasedEvaluationContext(
|
||||
invocation.getMethod().getDeclaringClass(),
|
||||
invocation.getMethod(),
|
||||
invocation.getArguments(),
|
||||
parameterNameDiscoverer
|
||||
);
|
||||
}
|
||||
return this.evaluationContext;
|
||||
}
|
||||
}
|
||||
|
@ -7,14 +7,11 @@ import io.papermc.hangar.security.annotations.HangarDecisionVoter;
|
||||
import io.papermc.hangar.security.annotations.currentuser.CurrentUserMetadataExtractor.CurrentUserAttribute;
|
||||
import io.papermc.hangar.security.authentication.HangarAuthenticationToken;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.context.expression.MethodBasedEvaluationContext;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.ConfigAttribute;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@Component
|
||||
public class CurrentUserVoter extends HangarDecisionVoter<CurrentUserAttribute> {
|
||||
|
||||
@ -23,25 +20,16 @@ public class CurrentUserVoter extends HangarDecisionVoter<CurrentUserAttribute>
|
||||
}
|
||||
|
||||
@Override
|
||||
public int vote(Authentication authentication, MethodInvocation methodInvocation, Collection<ConfigAttribute> attributes) {
|
||||
CurrentUserAttribute attribute = findAttribute(attributes);
|
||||
if (attribute == null) {
|
||||
return ACCESS_ABSTAIN;
|
||||
}
|
||||
public int vote(Authentication authentication, MethodInvocation methodInvocation, @NotNull CurrentUserAttribute attribute) {
|
||||
if (!(authentication instanceof HangarAuthenticationToken)) {
|
||||
throw new HangarApiException(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
HangarAuthenticationToken hangarAuthenticationToken = (HangarAuthenticationToken) authentication;
|
||||
if (hangarAuthenticationToken.getPrincipal().getGlobalPermissions().has(Permission.EditAllUserSettings)) {
|
||||
if (hangarAuthenticationToken.getPrincipal().isAllowedGlobal(Permission.EditAllUserSettings)) {
|
||||
return ACCESS_GRANTED;
|
||||
}
|
||||
String userName;
|
||||
Object user = attribute.getExpression().getValue(new MethodBasedEvaluationContext(
|
||||
methodInvocation.getMethod().getDeclaringClass(),
|
||||
methodInvocation.getMethod(),
|
||||
methodInvocation.getArguments(),
|
||||
parameterNameDiscoverer
|
||||
));
|
||||
Object user = attribute.getExpression().getValue(getMethodEvaluationContext(methodInvocation));
|
||||
if (user instanceof UserTable) {
|
||||
userName = ((UserTable) user).getName();
|
||||
} else if (user instanceof String) {
|
||||
|
@ -1,19 +1,20 @@
|
||||
package io.papermc.hangar.security.annotations.permission;
|
||||
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.common.NamedPermission;
|
||||
import io.papermc.hangar.model.common.Permission;
|
||||
import io.papermc.hangar.security.annotations.HangarDecisionVoter;
|
||||
import io.papermc.hangar.security.annotations.permission.PermissionRequiredMetadataExtractor.PermissionRequiredAttribute;
|
||||
import io.papermc.hangar.security.authentication.HangarAuthenticationToken;
|
||||
import io.papermc.hangar.service.PermissionService;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.expression.MethodBasedEvaluationContext;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.ConfigAttribute;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class PermissionRequiredVoter extends HangarDecisionVoter<PermissionRequiredAttribute> {
|
||||
@ -24,55 +25,46 @@ public class PermissionRequiredVoter extends HangarDecisionVoter<PermissionRequi
|
||||
public PermissionRequiredVoter(PermissionService permissionService) {
|
||||
super(PermissionRequiredAttribute.class);
|
||||
this.permissionService = permissionService;
|
||||
this.setAllowMultipleAttributes(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int vote(Authentication authentication, MethodInvocation methodInvocation, Collection<ConfigAttribute> attributes) {
|
||||
Collection<PermissionRequiredAttribute> permAttributes = findAttributes(attributes);
|
||||
if (permAttributes.isEmpty()) {
|
||||
return ACCESS_ABSTAIN;
|
||||
}
|
||||
public int vote(Authentication authentication, MethodInvocation methodInvocation, Set<PermissionRequiredAttribute> attributes) {
|
||||
if (!(authentication instanceof HangarAuthenticationToken)) {
|
||||
throw new HangarApiException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
HangarAuthenticationToken hangarAuthenticationToken = (HangarAuthenticationToken) authentication;
|
||||
int result = ACCESS_DENIED;
|
||||
for (PermissionRequiredAttribute attribute : permAttributes) {
|
||||
Object[] arguments = attribute.getExpression().getValue(new MethodBasedEvaluationContext(
|
||||
methodInvocation.getMethod().getDeclaringClass(),
|
||||
methodInvocation.getMethod(),
|
||||
methodInvocation.getArguments(),
|
||||
parameterNameDiscoverer
|
||||
), Object[].class);
|
||||
for (PermissionRequiredAttribute attribute : attributes) {
|
||||
Object[] arguments = attribute.getExpression().getValue(getMethodEvaluationContext(methodInvocation), Object[].class);
|
||||
if (arguments == null || !attribute.getPermissionType().getArgCounts().contains(arguments.length)) {
|
||||
throw new IllegalStateException("Bad annotation configuration");
|
||||
}
|
||||
Permission requiredPerm = Arrays.stream(attribute.getPermissions()).map(NamedPermission::getPermission).reduce(Permission::add).orElse(Permission.None);
|
||||
Permission currentPerm;
|
||||
switch (attribute.getPermissionType()) {
|
||||
case PROJECT:
|
||||
if (arguments.length == 1 && permissionService.getProjectPermissions(hangarAuthenticationToken.getUserId(), (long) arguments[0]).hasAll(attribute.getPermissions())) {
|
||||
result = ACCESS_GRANTED;
|
||||
} else if (arguments.length == 2 && permissionService.getProjectPermissions(hangarAuthenticationToken.getUserId(), (String) arguments[0], (String) arguments[1]).hasAll(attribute.getPermissions())) {
|
||||
result = ACCESS_GRANTED;
|
||||
if (arguments.length == 1) {
|
||||
currentPerm = permissionService.getProjectPermissions(hangarAuthenticationToken.getUserId(), (long) arguments[0]);
|
||||
} else if (arguments.length == 2) {
|
||||
currentPerm = permissionService.getProjectPermissions(hangarAuthenticationToken.getUserId(), (String) arguments[0], (String) arguments[1]);
|
||||
} else {
|
||||
result = ACCESS_DENIED;
|
||||
currentPerm = Permission.None;
|
||||
}
|
||||
break;
|
||||
case ORGANIZATION:
|
||||
if (arguments.length == 1 && permissionService.getOrganizationPermissions(hangarAuthenticationToken.getUserId(), (String) arguments[0]).hasAll(attribute.getPermissions())) {
|
||||
result = ACCESS_GRANTED;
|
||||
if (arguments.length == 1) {
|
||||
currentPerm = permissionService.getOrganizationPermissions(hangarAuthenticationToken.getUserId(), (String) arguments[0]);
|
||||
} else {
|
||||
result = ACCESS_DENIED;
|
||||
currentPerm = Permission.None;
|
||||
}
|
||||
break;
|
||||
case GLOBAL:
|
||||
if (permissionService.getGlobalPermissions(hangarAuthenticationToken.getUserId()).hasAll(attribute.getPermissions())) {
|
||||
result = ACCESS_GRANTED;
|
||||
} else {
|
||||
result = ACCESS_DENIED;
|
||||
}
|
||||
currentPerm = permissionService.getGlobalPermissions(hangarAuthenticationToken.getUserId());
|
||||
break;
|
||||
default:
|
||||
currentPerm = Permission.None;
|
||||
}
|
||||
if (result == ACCESS_GRANTED) {
|
||||
if (hangarAuthenticationToken.getPrincipal().isAllowed(requiredPerm, currentPerm)) {
|
||||
return ACCESS_GRANTED;
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +1,24 @@
|
||||
package io.papermc.hangar.security.annotations.unlocked;
|
||||
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.security.annotations.HangarDecisionVoter;
|
||||
import io.papermc.hangar.security.annotations.unlocked.UnlockedMetadataExtractor.UnlockedAttribute;
|
||||
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.access.AccessDecisionVoter;
|
||||
import org.springframework.security.access.ConfigAttribute;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@Component
|
||||
public class UnlockedVoter implements AccessDecisionVoter<MethodInvocation> {
|
||||
public class UnlockedVoter extends HangarDecisionVoter<UnlockedAttribute> {
|
||||
|
||||
@Override
|
||||
public boolean supports(ConfigAttribute attribute) {
|
||||
return attribute instanceof UnlockedAttribute;
|
||||
public UnlockedVoter() {
|
||||
super(UnlockedAttribute.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
return MethodInvocation.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int vote(Authentication authentication, MethodInvocation object, Collection<ConfigAttribute> attributes) {
|
||||
UnlockedAttribute attribute = findUnlockedAttribute(attributes);
|
||||
if (attribute == null) {
|
||||
return ACCESS_ABSTAIN;
|
||||
}
|
||||
public int vote(Authentication authentication, MethodInvocation object, @NotNull UnlockedAttribute attribute) {
|
||||
if (!(authentication instanceof HangarAuthenticationToken)) {
|
||||
return ACCESS_DENIED;
|
||||
}
|
||||
@ -39,13 +27,4 @@ public class UnlockedVoter implements AccessDecisionVoter<MethodInvocation> {
|
||||
}
|
||||
return ACCESS_GRANTED;
|
||||
}
|
||||
|
||||
private UnlockedAttribute findUnlockedAttribute(Collection<ConfigAttribute> attributes) {
|
||||
for (ConfigAttribute attribute : attributes) {
|
||||
if (attribute instanceof UnlockedAttribute) {
|
||||
return (UnlockedAttribute) attribute;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -7,15 +7,12 @@ import io.papermc.hangar.security.annotations.visibility.VisibilityRequiredMetad
|
||||
import io.papermc.hangar.service.internal.projects.ProjectService;
|
||||
import io.papermc.hangar.service.internal.versions.VersionService;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.expression.MethodBasedEvaluationContext;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.ConfigAttribute;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@Component
|
||||
public class VisibilityRequiredVoter extends HangarDecisionVoter<VisibilityRequiredAttribute> {
|
||||
|
||||
@ -30,17 +27,8 @@ public class VisibilityRequiredVoter extends HangarDecisionVoter<VisibilityRequi
|
||||
}
|
||||
|
||||
@Override
|
||||
public int vote(Authentication authentication, MethodInvocation method, Collection<ConfigAttribute> attributes) {
|
||||
VisibilityRequiredAttribute attribute = findAttribute(attributes);
|
||||
if (attribute == null) {
|
||||
return ACCESS_ABSTAIN;
|
||||
}
|
||||
Object[] arguments = attribute.getExpression().getValue(new MethodBasedEvaluationContext(
|
||||
method.getMethod().getDeclaringClass(),
|
||||
method.getMethod(),
|
||||
method.getArguments(),
|
||||
parameterNameDiscoverer
|
||||
), Object[].class);
|
||||
public int vote(Authentication authentication, MethodInvocation method, @NotNull VisibilityRequiredAttribute attribute) {
|
||||
Object[] arguments = attribute.getExpression().getValue(getMethodEvaluationContext(method), Object[].class);
|
||||
if (arguments == null || !attribute.getType().getArgCount().contains(arguments.length)) {
|
||||
throw new IllegalStateException("Bad annotation configuration");
|
||||
}
|
||||
|
@ -1,20 +1,44 @@
|
||||
package io.papermc.hangar.security.authentication;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import org.apache.pdfbox.util.Charsets;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* For both INTERNAL and API requests
|
||||
*/
|
||||
@Component
|
||||
public class HangarAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
@Autowired
|
||||
public HangarAuthenticationEntryPoint(ObjectMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
|
||||
// All should-be-authenticated requests are NOT browser requests, so redirecting from here to login won't work.
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Not found");
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
|
||||
HttpStatus status;
|
||||
if (failed instanceof CredentialsExpiredException) {
|
||||
status = HttpStatus.FORBIDDEN;
|
||||
} else if (failed instanceof BadCredentialsException) {
|
||||
status = HttpStatus.UNAUTHORIZED;
|
||||
} else {
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setStatus(status.value());
|
||||
response.setCharacterEncoding(Charsets.UTF_8.name());
|
||||
response.getWriter().write(mapper.writeValueAsString(new HangarApiException(status, failed.getMessage())));
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,10 @@ package io.papermc.hangar.security.authentication;
|
||||
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.auth0.jwt.exceptions.TokenExpiredException;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.security.configs.SecurityConfig;
|
||||
import io.papermc.hangar.service.TokenService;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
@ -15,7 +13,9 @@ import org.springframework.security.authentication.InternalAuthenticationService
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
@ -34,9 +34,10 @@ public class HangarAuthenticationFilter extends AbstractAuthenticationProcessing
|
||||
|
||||
private final TokenService tokenService;
|
||||
|
||||
public HangarAuthenticationFilter(final RequestMatcher requiresAuth, final TokenService tokenService, final AuthenticationManager authenticationManager) {
|
||||
public HangarAuthenticationFilter(final RequestMatcher requiresAuth, final TokenService tokenService, final AuthenticationManager authenticationManager, final AuthenticationEntryPoint authenticationEntryPoint) {
|
||||
super(requiresAuth);
|
||||
this.setAuthenticationManager(authenticationManager);
|
||||
this.setAuthenticationFailureHandler(new AuthenticationEntryPointFailureHandler(authenticationEntryPoint));
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
@ -65,7 +66,7 @@ public class HangarAuthenticationFilter extends AbstractAuthenticationProcessing
|
||||
// request should ALWAYS have a `HangarAuthJWTToken` attribute here
|
||||
String jwt = (String) request.getAttribute(AUTH_TOKEN_ATTR);
|
||||
try {
|
||||
HangarAuthenticationToken token = new HangarAuthenticationToken(tokenService.verify(jwt));
|
||||
HangarAuthenticationToken token = HangarAuthenticationToken.createUnverifiedToken(tokenService.verify(jwt));
|
||||
return getAuthenticationManager().authenticate(token);
|
||||
} catch (TokenExpiredException tokenExpiredException) {
|
||||
throw new CredentialsExpiredException("JWT was expired", tokenExpiredException);
|
||||
@ -84,24 +85,4 @@ public class HangarAuthenticationFilter extends AbstractAuthenticationProcessing
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
|
||||
SecurityContextHolder.clearContext();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Failed to process authentication request", failed);
|
||||
this.logger.trace("Cleared SecurityContextHolder");
|
||||
this.logger.trace("Handling authentication failure");
|
||||
}
|
||||
|
||||
HttpStatus status;
|
||||
if (failed instanceof CredentialsExpiredException) {
|
||||
status = HttpStatus.FORBIDDEN;
|
||||
} else if (failed instanceof BadCredentialsException) {
|
||||
status = HttpStatus.UNAUTHORIZED;
|
||||
} else {
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
throw new HangarApiException(status, failed.getMessage());
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ public class HangarAuthenticationProvider implements AuthenticationProvider {
|
||||
HangarAuthenticationToken token = (HangarAuthenticationToken) authentication;
|
||||
|
||||
HangarPrincipal hangarPrincipal = tokenService.parseHangarPrincipal(token.getCredentials());
|
||||
return new HangarAuthenticationToken(hangarPrincipal, token.getCredentials());
|
||||
return HangarAuthenticationToken.createVerifiedToken(hangarPrincipal, token.getCredentials());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -15,21 +15,29 @@ public class HangarAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private final DecodedJWT token;
|
||||
private final HangarPrincipal user;
|
||||
|
||||
// Used by HangarAuthenticationProvider once user is verified
|
||||
public HangarAuthenticationToken(HangarPrincipal user, DecodedJWT token) {
|
||||
private HangarAuthenticationToken(HangarPrincipal user, DecodedJWT token) {
|
||||
super(AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
this.token = token;
|
||||
this.user = user;
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
// Initial token creation before verifying the user exists in the table
|
||||
public HangarAuthenticationToken(DecodedJWT token) {
|
||||
private HangarAuthenticationToken(DecodedJWT token) {
|
||||
super(null);
|
||||
this.token = token;
|
||||
this.user = null;
|
||||
}
|
||||
|
||||
// Initial token creation before verifying the user exists in the table
|
||||
public static HangarAuthenticationToken createUnverifiedToken(DecodedJWT token) {
|
||||
return new HangarAuthenticationToken(token);
|
||||
}
|
||||
|
||||
// Used by HangarAuthenticationProvider once user is verified
|
||||
public static HangarAuthenticationToken createVerifiedToken(HangarPrincipal user, DecodedJWT token) {
|
||||
return new HangarAuthenticationToken(user, token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecodedJWT getCredentials() {
|
||||
return token;
|
||||
|
@ -32,12 +32,21 @@ public class HangarPrincipal implements ProjectOwner {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
public final boolean isLocked() {
|
||||
return locked;
|
||||
}
|
||||
|
||||
public Permission getGlobalPermissions() {
|
||||
return globalPermissions;
|
||||
public Permission getPossiblePermissions(){ return Permission.All; }
|
||||
|
||||
public final Permission getGlobalPermissions() {
|
||||
return globalPermissions.intersect(getPossiblePermissions());
|
||||
}
|
||||
|
||||
public final boolean isAllowedGlobal(Permission requiredPermission) {
|
||||
return isAllowed(requiredPermission, globalPermissions);
|
||||
}
|
||||
public final boolean isAllowed(Permission requiredPermission, Permission currentPermission) {
|
||||
return getPossiblePermissions().has(requiredPermission.intersect(currentPermission));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,31 @@
|
||||
package io.papermc.hangar.security.authentication.api;
|
||||
|
||||
import io.papermc.hangar.model.common.Permission;
|
||||
import io.papermc.hangar.model.db.auth.ApiKeyTable;
|
||||
import io.papermc.hangar.security.authentication.HangarPrincipal;
|
||||
|
||||
public class HangarApiPrincipal extends HangarPrincipal {
|
||||
|
||||
private final ApiKeyTable apiKeyTable;
|
||||
|
||||
public HangarApiPrincipal(long id, String name, boolean locked, Permission globalPermissions, ApiKeyTable apiKeyTable) {
|
||||
super(id, name, locked, globalPermissions);
|
||||
this.apiKeyTable = apiKeyTable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Permission getPossiblePermissions() {
|
||||
return apiKeyTable.getPermissions();
|
||||
}
|
||||
|
||||
public ApiKeyTable getApiKeyTable() {
|
||||
return apiKeyTable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HangarApiPrincipal{" +
|
||||
"apiKeyTable=" + apiKeyTable +
|
||||
"} " + super.toString();
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package io.papermc.hangar.security.configs;
|
||||
|
||||
import io.papermc.hangar.security.authentication.HangarAuthenticationEntryPoint;
|
||||
import io.papermc.hangar.security.authentication.HangarAuthenticationFilter;
|
||||
import io.papermc.hangar.security.authentication.HangarAuthenticationProvider;
|
||||
import io.papermc.hangar.service.TokenService;
|
||||
@ -12,6 +11,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
@ -30,10 +30,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
private static final RequestMatcher INTERNAL_API_MATCHER = new AntPathRequestMatcher("/api/internal/**");
|
||||
|
||||
private final TokenService tokenService;
|
||||
private final AuthenticationEntryPoint authenticationEntryPoint;
|
||||
|
||||
@Autowired
|
||||
public SecurityConfig(TokenService tokenService, HangarAuthenticationProvider hangarAuthenticationProvider) {
|
||||
public SecurityConfig(TokenService tokenService, HangarAuthenticationProvider hangarAuthenticationProvider, AuthenticationEntryPoint authenticationEntryPoint) {
|
||||
this.tokenService = tokenService;
|
||||
this.authenticationEntryPoint = authenticationEntryPoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -41,8 +43,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
http
|
||||
// Disable default configurations
|
||||
.logout().disable()
|
||||
// .httpBasic().disable()
|
||||
// .formLogin().disable()
|
||||
.httpBasic().disable()
|
||||
.formLogin().disable()
|
||||
|
||||
// Disable session creation
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
|
||||
@ -51,9 +53,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
.csrf().disable()
|
||||
|
||||
// Custom auth filters
|
||||
.addFilterBefore(new HangarAuthenticationFilter(API_MATCHER, tokenService, authenticationManager()), AnonymousAuthenticationFilter.class)
|
||||
.addFilterBefore(new HangarAuthenticationFilter(API_MATCHER, tokenService, authenticationManager(), authenticationEntryPoint), AnonymousAuthenticationFilter.class)
|
||||
|
||||
.exceptionHandling().authenticationEntryPoint(new HangarAuthenticationEntryPoint()).and()
|
||||
// .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
|
||||
|
||||
// Permit all (use method security for controller access)
|
||||
.authorizeRequests().anyRequest().permitAll();
|
||||
|
@ -5,15 +5,19 @@ import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import io.papermc.hangar.db.dao.HangarDao;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.ApiKeyDAO;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.UserRefreshTokenDAO;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.api.auth.RefreshResponse;
|
||||
import io.papermc.hangar.model.common.Permission;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
import io.papermc.hangar.model.db.auth.ApiKeyTable;
|
||||
import io.papermc.hangar.model.db.auth.UserRefreshToken;
|
||||
import io.papermc.hangar.security.authentication.HangarPrincipal;
|
||||
import io.papermc.hangar.security.authentication.api.HangarApiPrincipal;
|
||||
import io.papermc.hangar.security.configs.SecurityConfig;
|
||||
import io.papermc.hangar.service.internal.users.UserService;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@ -29,6 +33,7 @@ import java.util.UUID;
|
||||
@Service
|
||||
public class TokenService extends HangarService {
|
||||
|
||||
private final ApiKeyDAO apiKeyDAO;
|
||||
private final UserRefreshTokenDAO userRefreshTokenDAO;
|
||||
private final UserService userService;
|
||||
private final PermissionService permissionService;
|
||||
@ -37,7 +42,8 @@ public class TokenService extends HangarService {
|
||||
private Algorithm algo;
|
||||
|
||||
@Autowired
|
||||
public TokenService(HangarDao<UserRefreshTokenDAO> userRefreshTokenDAO, UserService userService, PermissionService permissionService) {
|
||||
public TokenService(HangarDao<ApiKeyDAO> apiKeyDAO, HangarDao<UserRefreshTokenDAO> userRefreshTokenDAO, UserService userService, PermissionService permissionService) {
|
||||
this.apiKeyDAO = apiKeyDAO.get();
|
||||
this.userRefreshTokenDAO = userRefreshTokenDAO.get();
|
||||
this.userService = userService;
|
||||
this.permissionService = permissionService;
|
||||
@ -55,7 +61,7 @@ public class TokenService extends HangarService {
|
||||
|
||||
private String _newToken(UserTable userTable, UserRefreshToken userRefreshToken) {
|
||||
Permission globalPermissions = permissionService.getGlobalPermissions(userTable.getId());
|
||||
return expiring(userTable, globalPermissions);
|
||||
return expiring(userTable, globalPermissions, null);
|
||||
}
|
||||
|
||||
public RefreshResponse refreshToken(String refreshToken) {
|
||||
@ -81,7 +87,7 @@ public class TokenService extends HangarService {
|
||||
userRefreshTokenDAO.delete(UUID.fromString(refreshToken));
|
||||
}
|
||||
|
||||
public String expiring(UserTable userTable, Permission globalPermission) {
|
||||
public String expiring(UserTable userTable, Permission globalPermission, @Nullable String apiKeyIdentifier) {
|
||||
return JWT.create()
|
||||
.withIssuer(config.security.getTokenIssuer())
|
||||
.withExpiresAt(new Date(Instant.now().plus(config.security.getTokenExpiry()).toEpochMilli()))
|
||||
@ -89,6 +95,7 @@ public class TokenService extends HangarService {
|
||||
.withClaim("id", userTable.getId())
|
||||
.withClaim("permissions", globalPermission.toBinString())
|
||||
.withClaim("locked", userTable.isLocked())
|
||||
.withClaim("apiKeyIdentifier", apiKeyIdentifier)
|
||||
.sign(getAlgo());
|
||||
}
|
||||
|
||||
@ -97,10 +104,19 @@ public class TokenService extends HangarService {
|
||||
Long userId = decodedJWT.getClaim("id").asLong();
|
||||
boolean locked = decodedJWT.getClaim("locked").asBoolean();
|
||||
Permission globalPermission = Permission.fromBinString(decodedJWT.getClaim("permissions").asString());
|
||||
String apiKeyIdentifier = decodedJWT.getClaim("apiKeyIdentifier").asString();
|
||||
if (subject == null || userId == null || globalPermission == null) {
|
||||
throw new BadCredentialsException("Malformed jwt");
|
||||
}
|
||||
return new HangarPrincipal(userId, subject, locked, globalPermission);
|
||||
if (apiKeyIdentifier != null) {
|
||||
ApiKeyTable apiKeyTable = apiKeyDAO.findApiKey(userId, apiKeyIdentifier);
|
||||
if (apiKeyTable == null) {
|
||||
throw new BadCredentialsException("Invalid api key identifier");
|
||||
}
|
||||
return new HangarApiPrincipal(userId, subject, locked, globalPermission, apiKeyTable);
|
||||
} else {
|
||||
return new HangarPrincipal(userId, subject, locked, globalPermission);
|
||||
}
|
||||
}
|
||||
|
||||
private JWTVerifier getVerifier() {
|
||||
|
@ -0,0 +1,52 @@
|
||||
package io.papermc.hangar.service.api;
|
||||
|
||||
import io.papermc.hangar.db.dao.HangarDao;
|
||||
import io.papermc.hangar.db.dao.internal.table.UserDAO;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.ApiKeyDAO;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.api.auth.ApiSession;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
import io.papermc.hangar.model.db.auth.ApiKeyTable;
|
||||
import io.papermc.hangar.service.HangarService;
|
||||
import io.papermc.hangar.service.PermissionService;
|
||||
import io.papermc.hangar.service.TokenService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
public class APIAuthenticationService extends HangarService {
|
||||
|
||||
private static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
||||
private static final Pattern API_KEY_PATTERN = Pattern.compile("(" + UUID_REGEX + ").(" + UUID_REGEX + ")");
|
||||
|
||||
private final UserDAO userDAO;
|
||||
private final ApiKeyDAO apiKeyDAO;
|
||||
private final TokenService tokenService;
|
||||
private final PermissionService permissionService;
|
||||
|
||||
@Autowired
|
||||
public APIAuthenticationService(HangarDao<UserDAO> userDAO, HangarDao<ApiKeyDAO> apiKeyDAO, TokenService tokenService, PermissionService permissionService) {
|
||||
this.userDAO = userDAO.get();
|
||||
this.apiKeyDAO = apiKeyDAO.get();
|
||||
this.tokenService = tokenService;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
public ApiSession createJWTForApiKey(String apiKey) {
|
||||
if (!API_KEY_PATTERN.matcher(apiKey).matches()) {
|
||||
throw new HangarApiException("Badly formatted API Key");
|
||||
}
|
||||
String identifier = apiKey.split("\\.")[0];
|
||||
String token = apiKey.split("\\.")[1];
|
||||
ApiKeyTable apiKeyTable = apiKeyDAO.findApiKey(identifier, token);
|
||||
if (apiKeyTable == null) {
|
||||
throw new HangarApiException("No valid API Key found");
|
||||
}
|
||||
UserTable userTable = userDAO.getUserTable(apiKeyTable.getOwnerId());
|
||||
String jwt = tokenService.expiring(userTable, permissionService.getGlobalPermissions(userTable.getId()), identifier);
|
||||
return new ApiSession(jwt, config.security.getRefreshTokenExpiry().toSeconds());
|
||||
}
|
||||
// 006ad884-3df9-43e8-af01-91590f92cfd7.fa31831d-097f-4d11-9031-b57b41c59fa1
|
||||
}
|
@ -35,7 +35,7 @@ public class ProjectsApiService extends HangarService {
|
||||
}
|
||||
|
||||
public PaginatedResult<ProjectMember> getProjectMembers(String author, String slug, RequestPagination requestPagination) {
|
||||
List<ProjectMember> projectMembers = projectsApiDAO.getProjectMembers(author, slug, requestPagination.getLimit(), requestPagination.getOffset());
|
||||
List<ProjectMember> projectMembers = projectsApiDAO.getProjectMembers(author, slug, requestPagination);
|
||||
return new PaginatedResult<>(new Pagination(projectsApiDAO.getProjectMembersCount(author, slug), requestPagination), projectMembers);
|
||||
}
|
||||
|
||||
|
@ -28,8 +28,6 @@ public class AuthenticationService extends HangarService {
|
||||
private final HttpServletRequest request;
|
||||
|
||||
private static final Pattern API_ROUTE_PATTERN = Pattern.compile("^/api/(?!old).+");
|
||||
private static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
||||
private static final Pattern API_KEY_PATTERN = Pattern.compile("(" + UUID_REGEX + ").(" + UUID_REGEX + ")");
|
||||
|
||||
@Autowired
|
||||
public AuthenticationService(HangarDao<SessionsDao> sessionsDao, HangarDao<ApiKeyDao> apiKeyDao, HttpServletRequest request, Supplier<Optional<UsersTable>> currentUser) {
|
||||
|
@ -0,0 +1,3 @@
|
||||
ALTER TABLE api_keys ADD CONSTRAINT api_keys_owner_id_token_identifier UNIQUE (owner_id, token_identifier);
|
||||
|
||||
DROP TABLE api_sessions;
|
Loading…
x
Reference in New Issue
Block a user