improve SPeL support in various annotations

This commit is contained in:
Jake Potrebic 2022-11-06 17:24:13 -08:00
parent b4f4f426af
commit 03352e47cb
No known key found for this signature in database
GPG Key ID: ECE0B3C133C016C5
26 changed files with 146 additions and 116 deletions

View File

@ -161,7 +161,7 @@ public class LoginController extends HangarComponent {
@RateLimit(overdraft = 5, refillTokens = 2, refillSeconds = 10)
@ResponseStatus(HttpStatus.ACCEPTED)
public void sync(@NotNull @RequestBody SsoSyncData body, @RequestHeader("X-Kratos-Hook-Api-Key") String apiKey) {
if (!apiKey.equals("hookapikey-changeme")) {
if (!apiKey.equals("hookapikey-changeme")) { // TODO change
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}

View File

@ -17,6 +17,10 @@ import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired.Type;
import io.papermc.hangar.service.internal.projects.ChannelService;
import io.papermc.hangar.service.internal.projects.ProjectService;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -30,11 +34,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.validation.Valid;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
// @el(projectId: long)
@Controller
@RateLimit(path = "channel")
@RequestMapping(value = "/api/internal/channels", produces = MediaType.APPLICATION_JSON_VALUE)

View File

@ -1,7 +1,13 @@
package io.papermc.hangar.controller.internal;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.internal.job.PostDiscourseReplyJob;
import io.papermc.hangar.security.annotations.LoggedIn;
import io.papermc.hangar.security.annotations.ratelimit.RateLimit;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
import io.papermc.hangar.service.internal.JobService;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
@ -10,16 +16,9 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Map;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.model.internal.job.PostDiscourseReplyJob;
import io.papermc.hangar.security.annotations.LoggedIn;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
import io.papermc.hangar.service.internal.JobService;
import static io.papermc.hangar.security.annotations.visibility.VisibilityRequired.Type;
// @el(projectId: long)
@LoggedIn
@Controller
@RateLimit(path = "discourse")
@ -36,7 +35,7 @@ public class DiscourseController extends HangarComponent {
@ResponseBody
@RateLimit(overdraft = 5, refillTokens = 1, refillSeconds = 30)
@VisibilityRequired(type = Type.PROJECT, args = "{#projectId}")
public String createPost(@PathVariable("projectId") long projectId, @RequestBody Map<String, String> content) {
public String createPost(@PathVariable long projectId, @RequestBody Map<String, String> content) {
if (!config.discourse.isEnabled()) {
throw new HangarApiException("Discourse is NOT enabled!");
}

View File

@ -90,6 +90,7 @@ public class HangarUserController extends HangarComponent {
return ResponseEntity.ok(usersApiService.getUser(hangarAuthenticationToken.getName(), HangarUser.class));
}
// @el(userName: String)
@Unlocked
@CurrentUser("#userName")
@ResponseStatus(HttpStatus.OK)

View File

@ -47,6 +47,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MultipartFile;
// @el(orgName: String)
@Controller
@RateLimit(path = "organization")
@RequestMapping("/api/internal/organizations")
@ -83,20 +84,20 @@ public class OrganizationController extends HangarComponent {
}
}
@GetMapping(value = "/org/{name}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<HangarOrganization> getOrganization(@PathVariable String name) {
return ResponseEntity.ok(organizationService.getHangarOrganization(name));
@GetMapping(value = "/org/{orgName}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<HangarOrganization> getOrganization(@PathVariable String orgName) {
return ResponseEntity.ok(organizationService.getHangarOrganization(orgName));
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@RateLimit(overdraft = 7, refillTokens = 2, refillSeconds = 10)
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.MANAGE_SUBJECT_MEMBERS, args = "{#name}")
@PostMapping(path = "/org/{name}/members/add", consumes = MediaType.APPLICATION_JSON_VALUE)
public void addOrganizationMember(@PathVariable String name, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.MANAGE_SUBJECT_MEMBERS, args = "{#orgName}")
@PostMapping(path = "/org/{orgName}/members/add", consumes = MediaType.APPLICATION_JSON_VALUE)
public void addOrganizationMember(@PathVariable String orgName, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
OrganizationTable organizationTable = organizationService.getOrganizationTable(orgName);
if (organizationTable == null) {
throw new HangarApiException("Org " + name + " doesn't exist");
throw new HangarApiException("Org " + orgName + " doesn't exist");
}
inviteService.sendInvite(member, organizationTable);
}
@ -104,48 +105,48 @@ public class OrganizationController extends HangarComponent {
@Unlocked
@ResponseStatus(HttpStatus.OK)
@RateLimit(overdraft = 5, refillTokens = 2, refillSeconds = 10)
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.MANAGE_SUBJECT_MEMBERS, args = "{#name}")
@PostMapping(path = "/org/{name}/members/edit", consumes = MediaType.APPLICATION_JSON_VALUE)
public void editOrganizationMember(@PathVariable String name, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.MANAGE_SUBJECT_MEMBERS, args = "{#orgName}")
@PostMapping(path = "/org/{orgName}/members/edit", consumes = MediaType.APPLICATION_JSON_VALUE)
public void editOrganizationMember(@PathVariable String orgName, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
OrganizationTable organizationTable = organizationService.getOrganizationTable(orgName);
memberService.editMember(member, organizationTable);
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@RateLimit(overdraft = 7, refillTokens = 2, refillSeconds = 10)
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.MANAGE_SUBJECT_MEMBERS, args = "{#name}")
@PostMapping(path = "/org/{name}/members/remove", consumes = MediaType.APPLICATION_JSON_VALUE)
public void removeOrganizationMember(@PathVariable String name, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.MANAGE_SUBJECT_MEMBERS, args = "{#orgName}")
@PostMapping(path = "/org/{orgName}/members/remove", consumes = MediaType.APPLICATION_JSON_VALUE)
public void removeOrganizationMember(@PathVariable String orgName, @Valid @RequestBody EditMembersForm.Member<OrganizationRole> member) {
OrganizationTable organizationTable = organizationService.getOrganizationTable(orgName);
memberService.removeMember(member, organizationTable);
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.IS_SUBJECT_MEMBER, args = "{#name}")
@PostMapping(path = "/org/{name}/members/leave", consumes = MediaType.APPLICATION_JSON_VALUE)
public void leaveOrganization(@PathVariable String name) {
OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.IS_SUBJECT_MEMBER, args = "{#orgName}")
@PostMapping(path = "/org/{orgName}/members/leave", consumes = MediaType.APPLICATION_JSON_VALUE)
public void leaveOrganization(@PathVariable String orgName) {
OrganizationTable organizationTable = organizationService.getOrganizationTable(orgName);
memberService.leave(organizationTable);
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.IS_SUBJECT_OWNER, args = "{#name}")
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.IS_SUBJECT_OWNER, args = "{#orgName}")
@RateLimit(overdraft = 5, refillTokens = 1, refillSeconds = 60)
@PostMapping(path = "/org/{name}/transfer", consumes = MediaType.APPLICATION_JSON_VALUE)
public void transferOrganization(@PathVariable String name, @Valid @RequestBody StringContent nameContent) {
final OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
@PostMapping(path = "/org/{orgName}/transfer", consumes = MediaType.APPLICATION_JSON_VALUE)
public void transferOrganization(@PathVariable String orgName, @Valid @RequestBody StringContent nameContent) {
final OrganizationTable organizationTable = organizationService.getOrganizationTable(orgName);
inviteService.sendTransferRequest(nameContent.getContent(), organizationTable);
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.IS_SUBJECT_OWNER, args = "{#name}")
@PostMapping(path = "/org/{name}/canceltransfer", consumes = MediaType.APPLICATION_JSON_VALUE)
public void cancelOrganizationTransfer(@PathVariable String name) {
final OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.IS_SUBJECT_OWNER, args = "{#orgName}")
@PostMapping(path = "/org/{orgName}/canceltransfer", consumes = MediaType.APPLICATION_JSON_VALUE)
public void cancelOrganizationTransfer(@PathVariable String orgName) {
final OrganizationTable organizationTable = organizationService.getOrganizationTable(orgName);
inviteService.cancelTransferRequest(organizationTable);
}
@ -160,21 +161,21 @@ public class OrganizationController extends HangarComponent {
@Unlocked
@ResponseStatus(HttpStatus.OK)
@RateLimit(overdraft = 3, refillTokens = 1, refillSeconds = 60)
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.DELETE_ORGANIZATION, args = "{#name}")
@PostMapping(path = "/org/{name}/delete", consumes = MediaType.APPLICATION_JSON_VALUE)
public void delete(@PathVariable String name, @RequestBody @Valid StringContent content) {
final OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.DELETE_ORGANIZATION, args = "{#orgName}")
@PostMapping(path = "/org/{orgName}/delete", consumes = MediaType.APPLICATION_JSON_VALUE)
public void delete(@PathVariable String orgName, @RequestBody @Valid StringContent content) {
final OrganizationTable organizationTable = organizationService.getOrganizationTable(orgName);
organizationFactory.deleteOrganization(organizationTable, content.getContent());
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@RateLimit(overdraft = 7, refillTokens = 1, refillSeconds = 20)
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.EDIT_SUBJECT_SETTINGS, args = "{#name}")
@PostMapping(path = "/org/{name}/settings/tagline", consumes = MediaType.APPLICATION_JSON_VALUE)
public void saveTagline(@PathVariable String name, @Valid @RequestBody StringContent content) {
UserTable userTable = userService.getUserTable(name);
OrganizationTable organizationTable = organizationService.getOrganizationTable(name);
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.EDIT_SUBJECT_SETTINGS, args = "{#orgName}")
@PostMapping(path = "/org/{orgName}/settings/tagline", consumes = MediaType.APPLICATION_JSON_VALUE)
public void saveTagline(@PathVariable String orgName, @Valid @RequestBody StringContent content) {
UserTable userTable = userService.getUserTable(orgName);
OrganizationTable organizationTable = organizationService.getOrganizationTable(orgName);
if (userTable == null || organizationTable == null) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
@ -190,10 +191,10 @@ public class OrganizationController extends HangarComponent {
@Unlocked
@ResponseBody
@RateLimit(overdraft = 5, refillTokens = 1, refillSeconds = 60)
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.EDIT_SUBJECT_SETTINGS, args = "{#name}")
@PostMapping(value = "/org/{name}/settings/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void changeAvatar(@PathVariable String name, @RequestParam MultipartFile avatar) throws IOException {
authenticationService.changeAvatar(name, avatar);
@PermissionRequired(type = PermissionType.ORGANIZATION, perms = NamedPermission.EDIT_SUBJECT_SETTINGS, args = "{#orgName}")
@PostMapping(value = "/org/{orgName}/settings/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void changeAvatar(@PathVariable String orgName, @RequestParam MultipartFile avatar) throws IOException {
authenticationService.changeAvatar(orgName, avatar);
}
@Anyone

View File

@ -47,6 +47,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MultipartFile;
// @el(projectId: long, author: String, slug: String, versionString: String, platform: io.papermc.hangar.model.common.Platform)
@Controller
@Secured("ROLE_USER")
@RateLimit(path = "version")
@ -174,13 +175,13 @@ public class VersionController extends HangarComponent {
@ResponseBody
@RateLimit(overdraft = 5, refillTokens = 1, refillSeconds = 20)
@VisibilityRequired(type = Type.VERSION, args = "{#author, #slug, #versionString, #platform}")
@VisibilityRequired(type = Type.VERSION, args = "{#author, #slug, #versionString, #platform}") // TODO is platform needed in the visibility check? it's not used in the VisibilityRequiredVoter
@GetMapping(path = "/version/{author}/{slug}/versions/{versionString}/{platform}/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public Resource download(@PathVariable String author, @PathVariable String slug, @PathVariable String versionString, @PathVariable Platform platform, @RequestParam(required = false) String token) {
return downloadService.getVersionFile(author, slug, versionString, platform, true, token);
}
@VisibilityRequired(type = Type.VERSION, args = "{#author, #slug, #versionString, #platform}")
@VisibilityRequired(type = Type.VERSION, args = "{#author, #slug, #versionString, #platform}") // TODO is platform needed in the visibility check? it's not used in the VisibilityRequiredVoter
@GetMapping(path = "/version/{author}/{slug}/versions/{versionString}/{platform}/downloadCheck")
public ResponseEntity<String> downloadCheck(@PathVariable String author, @PathVariable String slug, @PathVariable String versionString, @PathVariable Platform platform) {
boolean requiresConfirmation = downloadService.requiresConfirmation(author, slug, versionString, platform);

View File

@ -10,16 +10,13 @@ import io.papermc.hangar.security.annotations.permission.PermissionRequired;
import io.papermc.hangar.security.annotations.ratelimit.RateLimit;
import io.papermc.hangar.service.internal.projects.ProjectAdminService;
import io.papermc.hangar.service.internal.versions.ReviewService;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.List;
@Controller
@RateLimit(path = "adminapproval")

View File

@ -12,6 +12,8 @@ import io.papermc.hangar.security.annotations.ratelimit.RateLimit;
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
import io.papermc.hangar.service.internal.projects.ProjectAdminService;
import io.papermc.hangar.service.internal.projects.ProjectNoteService;
import java.util.List;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -24,9 +26,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.validation.Valid;
import java.util.List;
// @el(projectId: long)
@LoggedIn
@Controller
@RateLimit(path = "projectadmin")

View File

@ -30,6 +30,9 @@ import io.papermc.hangar.service.internal.projects.ProjectService;
import io.papermc.hangar.service.internal.uploads.ImageService;
import io.papermc.hangar.service.internal.users.UserService;
import io.papermc.hangar.service.internal.users.invites.ProjectInviteService;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -44,10 +47,8 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.view.RedirectView;
import javax.validation.Valid;
import java.util.List;
import java.util.stream.Collectors;
// @el(author: String, slug: String, projectId: long, project: io.papermc.hangar.model.db.projects.ProjectTable)
@Controller
@RateLimit(path = "project")
@RequestMapping(path = "/api/internal/projects", produces = MediaType.APPLICATION_JSON_VALUE)
@ -96,7 +97,7 @@ public class ProjectController extends HangarComponent {
@PostMapping(value = "/create", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> createProject(@RequestBody @Valid NewProjectForm newProject) {
ProjectTable projectTable = projectFactory.createProject(newProject);
// need to do this here, outside of the transactional
// need to do this here, outside the transactional
projectService.refreshHomeProjects();
return ResponseEntity.ok(projectTable.getUrl());
}

View File

@ -18,6 +18,8 @@ import io.papermc.hangar.service.ValidationService;
import io.papermc.hangar.service.internal.MarkdownService;
import io.papermc.hangar.service.internal.projects.ProjectPageService;
import io.papermc.hangar.util.BBCodeConverter;
import java.util.Collection;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -32,9 +34,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.validation.Valid;
import java.util.Collection;
// @el(author: String, slug: String, projectId: long)
@Anyone
@Controller
@RateLimit(path = "projectpage")

View File

@ -1,21 +1,21 @@
package io.papermc.hangar.controller.validations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import org.intellij.lang.annotations.Language;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
@Documented
@Constraint(validatedBy = Validate.ValidateConstraintValidator.class)
@ -23,6 +23,7 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Validate.List.class)
public @interface Validate {
@Language("SpEL")
String SpEL() default "";
String message() default "Invalid field";
Class<?>[] groups() default {};

View File

@ -12,8 +12,12 @@ public class ProjectLicense {
private static final HangarConfig config = StaticContextAccessor.getBean(HangarConfig.class);
private final String name;
// @el(root: String)
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.urlRegex)", message = "validation.invalidUrl")
private final String url;
// @el(root: String)
@Validate(SpEL = "@validate.required(#root)")
private final String type;

View File

@ -2,33 +2,45 @@ package io.papermc.hangar.model.api.project;
import com.fasterxml.jackson.annotation.JsonCreator;
import io.papermc.hangar.controller.validations.Validate;
import java.util.Collection;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.jdbi.v3.core.mapper.Nested;
import org.jetbrains.annotations.Nullable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.Collection;
public class ProjectSettings {
// @el(root: String)
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.urlRegex)", message = "validation.invalidUrl")
private final String homepage;
// @el(root: String)
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.urlRegex)", message = "validation.invalidUrl")
private final String issues;
// @el(root: String)
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.urlRegex)", message = "validation.invalidUrl")
private final String source;
// @el(root: String)
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.urlRegex)", message = "validation.invalidUrl")
private final String support;
// @el(root: String)
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.urlRegex)", message = "validation.invalidUrl")
private final String wiki;
@Valid
private final ProjectLicense license;
@Valid
private final ProjectDonationSettings donation;
// @el(root: Collection<String>)
@NotNull
@Validate(SpEL = "@validate.max(#root, @hangarConfig.projects.maxKeywords)", message = "project.new.error.tooManyKeywords")
private final Collection<String> keywords;
private final boolean forumSync;
// @el(root: String)
@Validate(SpEL = "@validate.max(#root, @hangarConfig.projects.maxSponsorsLen)", message = "project.new.error.tooLongSponsors")
private final String sponsors;

View File

@ -2,10 +2,10 @@ package io.papermc.hangar.model.internal.api.requests;
import io.papermc.hangar.controller.validations.Validate;
import io.papermc.hangar.model.common.roles.OrganizationRole;
import java.util.List;
public class CreateOrganizationForm extends EditMembersForm<OrganizationRole> {
// @el(root: String)
@Validate(SpEL = "@validate.required(#root)", message = "organization.new.error.invalidName")
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.org.nameRegex)", message = "organization.new.error.invalidName")
@Validate(SpEL = "@validate.max(#root, @hangarConfig.org.maxNameLen)", message = "organization.new.error.invalidName")

View File

@ -5,11 +5,10 @@ import io.papermc.hangar.controller.validations.Validate;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.common.roles.Role;
import io.papermc.hangar.model.db.roles.ExtendedRoleTable;
import org.springframework.http.HttpStatus;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.util.List;
import org.springframework.http.HttpStatus;
public class EditMembersForm<R extends Role<? extends ExtendedRoleTable<R, ?>>> {
@ -35,7 +34,9 @@ public class EditMembersForm<R extends Role<? extends ExtendedRoleTable<R, ?>>>
@NotBlank
private final String name;
@Validate(SpEL = "#root.isAssignable")
// @el(root: Role)
@Validate(SpEL = "#root.assignable")
private final R role;
@SuppressWarnings("unchecked")

View File

@ -4,13 +4,13 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import io.papermc.hangar.controller.validations.Validate;
import io.papermc.hangar.model.common.ChannelFlag;
import io.papermc.hangar.model.common.Color;
import java.util.Set;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Set;
public class ChannelForm {
// @el(root: String)
@NotBlank
@Validate(SpEL = "@validations.regex(#root, @hangarConfig.channels.nameRegex)", message = "channel.modal.error.invalidName")
@Validate(SpEL = "@validations.max(#root, @hangarConfig.channels.maxNameLen)", message = "channel.modal.error.tooLongName")

View File

@ -3,16 +3,19 @@ package io.papermc.hangar.model.internal.api.requests.projects;
import io.papermc.hangar.controller.validations.Validate;
import io.papermc.hangar.model.api.project.ProjectSettings;
import io.papermc.hangar.model.common.projects.Category;
import javax.validation.constraints.NotNull;
public class NewProjectForm extends ProjectSettingsForm {
private final long ownerId;
// @el(root: String)
@NotNull(message = "project.new.error.invalidName")
@Validate(SpEL = "@validate.max(#root, @hangarConfig.projects.maxNameLen)", message = "project.new.error.tooLongName")
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.projects.nameRegex)", message = "project.new.error.invalidName")
private final String name;
// @el(root: String)
@Validate(SpEL = "@validate.max(#root, @hangarConfig.pages.maxLen)", message = "page.new.error.maxLength")
private final String pageContent;

View File

@ -2,11 +2,11 @@ package io.papermc.hangar.model.internal.api.requests.projects;
import com.fasterxml.jackson.annotation.JsonCreator;
import io.papermc.hangar.controller.validations.Validate;
import javax.validation.constraints.NotBlank;
public class NewProjectPage {
// @el(root: String)
@NotBlank
@Validate(SpEL = "@validate.min(#root, @hangarConfig.pages.minNameLen)", message = "page.new.error.name.minLength")
@Validate(SpEL = "@validate.max(#root, @hangarConfig.pages.maxNameLen)", message = "page.new.error.name.maxLength")

View File

@ -4,7 +4,6 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import io.papermc.hangar.controller.validations.Validate;
import io.papermc.hangar.model.api.project.ProjectSettings;
import io.papermc.hangar.model.common.projects.Category;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@ -14,6 +13,8 @@ public class ProjectSettingsForm {
private final ProjectSettings settings;
@NotNull(message = "project.new.error.noCategory")
private final Category category;
// @el(root: String)
@NotNull(message = "project.new.error.noDescription")
@Validate(SpEL = "@validate.max(#root, @hangarConfig.projects.maxDescLen)", message = "project.new.error.tooLongDesc")
private final String description;

View File

@ -2,11 +2,11 @@ package io.papermc.hangar.model.internal.versions;
import io.papermc.hangar.controller.validations.Validate;
import io.papermc.hangar.model.common.Platform;
import java.util.List;
import javax.validation.constraints.Size;
import org.jetbrains.annotations.Nullable;
import javax.validation.constraints.Size;
import java.util.List;
// @el(root: String) // can't figure out how to apply this to just the one record component
public record MultipartFileOrUrl(
@Size(min = 1, max = 3, message = "version.new.error.invalidNumOfPlatforms") List<Platform> platforms,
@Nullable @Validate(SpEL = "@validate.regex(#root, @hangarConfig.urlRegex)", message = "validation.invalidUrl") String externalUrl) {

View File

@ -7,31 +7,35 @@ import io.papermc.hangar.model.api.project.version.PluginDependency;
import io.papermc.hangar.model.common.ChannelFlag;
import io.papermc.hangar.model.common.Color;
import io.papermc.hangar.model.common.Platform;
import org.jetbrains.annotations.Nullable;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.jetbrains.annotations.Nullable;
public class PendingVersion {
// @el(root: String)
@NotBlank(message = "version.new.error.invalidVersionString")
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.projects.versionNameRegex)", message = "version.new.error.invalidVersionString")
private final String versionString;
private final Map<Platform, Set<@Valid PluginDependency>> pluginDependencies;
@Size(min = 1, max = 3, message = "version.new.error.invalidNumOfPlatforms")
private final Map<Platform, @Size(min = 1, message = "version.edit.error.noPlatformVersions") SortedSet<@NotBlank(message = "version.new.error.invalidPlatformVersion") String>> platformDependencies;
// @el(root: String)
@NotBlank(message = "version.new.error.noDescription")
@Validate(SpEL = "@validate.max(#root, @hangarConfig.pages.maxLen)", message = "page.new.error.maxLength")
private final String description;
@Size(min = 1, max = 3, message = "version.new.error.invalidNumOfPlatforms")
private final List<@Valid PendingVersionFile> files;
// @el(root: String)
@NotBlank(message = "version.new.error.channel.noName")
@Validate(SpEL = "@validate.regex(#root, @hangarConfig.channels.nameRegex)", message = "channel.modal.error.invalidName")
private final String channelName;

View File

@ -3,11 +3,11 @@ package io.papermc.hangar.model.internal.versions;
import io.papermc.hangar.controller.validations.Validate;
import io.papermc.hangar.model.api.project.version.FileInfo;
import io.papermc.hangar.model.common.Platform;
import java.util.List;
import javax.validation.constraints.Size;
import org.jetbrains.annotations.Nullable;
import javax.validation.constraints.Size;
import java.util.List;
// @el(root: String) // can't figure out how to apply this to just the field
public record PendingVersionFile(
@Size(min = 1, max = 3, message = "version.new.error.invalidNumOfPlatforms") List<Platform> platforms,
@Nullable FileInfo fileInfo,

View File

@ -1,12 +1,12 @@
package io.papermc.hangar.security.annotations.currentuser;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.intellij.lang.annotations.Language;
import org.springframework.core.annotation.AliasFor;
/**
* Requires that the user referenced in the method be the current user
@ -21,6 +21,7 @@ public @interface CurrentUser {
* interchangeable with {@link #userArgument()}
* @return the SpEL string for the argument in a method
*/
@Language("SpEL")
@AliasFor("userArgument")
String value() default "";
@ -28,6 +29,7 @@ public @interface CurrentUser {
* interchangeable with {@link #value()}
* @return the SpEL string for the argument in a method
*/
@Language("SpEL")
@AliasFor("value")
String userArgument() default "";
}

View File

@ -3,14 +3,14 @@ package io.papermc.hangar.security.annotations.permission;
import io.papermc.hangar.model.common.NamedPermission;
import io.papermc.hangar.model.common.PermissionType;
import io.papermc.hangar.security.annotations.permission.PermissionRequired.List;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.intellij.lang.annotations.Language;
import org.springframework.core.annotation.AliasFor;
/**
* Mark a method or class to require permissions
@ -45,6 +45,8 @@ public @interface PermissionRequired {
* <br>
* The length of the SpEL array <b>must</b> match the type;
*/
// @el(projectId: String)
@Language("SpEL")
String args() default "{}";
@Target({ElementType.METHOD, ElementType.TYPE})

View File

@ -6,6 +6,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Set;
import org.intellij.lang.annotations.Language;
/**
* Visibility check for a project or version
@ -24,6 +25,7 @@ public @interface VisibilityRequired {
* Method arguments to resolve the project or version
* @return method arguments as an SpEL array
*/
@Language("SpEL")
String args();
enum Type {

View File

@ -1,19 +1,17 @@
package io.papermc.hangar.security.annotations.visibility;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.common.Platform;
import io.papermc.hangar.security.annotations.HangarDecisionVoter;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequiredMetadataExtractor.VisibilityRequiredAttribute;
import io.papermc.hangar.service.internal.projects.ProjectService;
import io.papermc.hangar.service.internal.versions.VersionService;
import java.util.Arrays;
import org.aopalliance.intercept.MethodInvocation;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
public class VisibilityRequiredVoter extends HangarDecisionVoter<VisibilityRequiredAttribute> {
@ -47,7 +45,7 @@ public class VisibilityRequiredVoter extends HangarDecisionVoter<VisibilityRequi
if (arguments.length == 1 && versionService.getProjectVersionTable((long) arguments[0]) != null) {
return ACCESS_GRANTED;
} else {
if (versionService.getProjectVersionTable((String) arguments[0], (String) arguments[1], (String) arguments[2]) != null) {
if (versionService.getProjectVersionTable((String) arguments[0], (String) arguments[1], (String) arguments[2]) != null) { // TODO is platform needed here?
return ACCESS_GRANTED;
} else {
return ACCESS_DENIED;