Skip to content

Commit ee475d3

Browse files
CopilotMiniDigger
andauthored
Add authorization annotation tests for v1 API controllers (#1538)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MiniDigger | Martin <admin@minidigger.dev>
1 parent 45c8310 commit ee475d3

File tree

6 files changed

+287
-2
lines changed

6 files changed

+287
-2
lines changed

backend/src/test/java/io/papermc/hangar/controller/api/v1/ApiKeysControllerTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,65 @@ void testCreateGetDeleteKey() throws Exception {
4040
.andExpect(jsonPath("$[*].name").value(not(hasItem("cool_key"))))
4141
.andExpect(jsonPath("$[*].tokenIdentifier").value(not(hasItem(identifier))));
4242
}
43+
44+
// Authorization tests for @PermissionRequired annotation
45+
@Test
46+
void testCreateKeyWithoutPermission() throws Exception {
47+
// User without EDIT_API_KEYS permission should be denied
48+
this.mockMvc.perform(post("/api/v1/keys")
49+
.with(this.apiKey(TestData.KEY_NO_PERMISSIONS))
50+
.header("Content-Type", "application/json")
51+
.content(this.objectMapper.writeValueAsBytes(new CreateAPIKeyForm("test_key", Set.of(NamedPermission.CREATE_PROJECT)))))
52+
.andExpect(status().is(404));
53+
}
54+
55+
@Test
56+
void testGetKeysWithoutPermission() throws Exception {
57+
// User without EDIT_API_KEYS permission should be denied
58+
this.mockMvc.perform(get("/api/v1/keys")
59+
.with(this.apiKey(TestData.KEY_NO_PERMISSIONS)))
60+
.andExpect(status().is(404));
61+
}
62+
63+
@Test
64+
void testDeleteKeyWithoutPermission() throws Exception {
65+
// User without EDIT_API_KEYS permission should be denied
66+
this.mockMvc.perform(delete("/api/v1/keys?name=test")
67+
.with(this.apiKey(TestData.KEY_NO_PERMISSIONS)))
68+
.andExpect(status().is(404));
69+
}
70+
71+
@Test
72+
void testCreateKeyWithoutAuth() throws Exception {
73+
// Unauthenticated user should be denied
74+
this.mockMvc.perform(post("/api/v1/keys")
75+
.header("Content-Type", "application/json")
76+
.content(this.objectMapper.writeValueAsBytes(new CreateAPIKeyForm("test_key", Set.of(NamedPermission.CREATE_PROJECT)))))
77+
.andExpect(status().is(403));
78+
}
79+
80+
@Test
81+
void testGetKeysWithoutAuth() throws Exception {
82+
// Unauthenticated user should be denied
83+
this.mockMvc.perform(get("/api/v1/keys"))
84+
.andExpect(status().is(404));
85+
}
86+
87+
@Test
88+
void testDeleteKeyWithoutAuth() throws Exception {
89+
// Unauthenticated user should be denied
90+
this.mockMvc.perform(delete("/api/v1/keys?name=test"))
91+
.andExpect(status().is(403));
92+
}
93+
94+
// Authorization tests for @Unlocked annotation
95+
@Test
96+
void testCreateKeyWithLockedUser() throws Exception {
97+
// Locked/banned user should be denied by @Unlocked annotation
98+
this.mockMvc.perform(post("/api/v1/keys")
99+
.with(this.apiKey(TestData.KEY_BANNED))
100+
.header("Content-Type", "application/json")
101+
.content(this.objectMapper.writeValueAsBytes(new CreateAPIKeyForm("test_key", Set.of(NamedPermission.CREATE_PROJECT)))))
102+
.andExpect(status().is(401));
103+
}
43104
}

backend/src/test/java/io/papermc/hangar/controller/api/v1/PagesControllerTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,78 @@ void testEditInvalidPage() throws Exception {
124124
.with(this.apiKey(TestData.KEY_ADMIN)))
125125
.andExpect(status().is(200));
126126
}
127+
128+
// Authorization tests for @PermissionRequired annotation
129+
@Test
130+
void testGetMainPageWithoutPermission() throws Exception {
131+
// User without VIEW_PUBLIC_INFO permission should be denied
132+
this.mockMvc.perform(get("/api/v1/pages/main/" + TestData.PRIVATE_PROJECT.getSlug())
133+
.with(this.apiKey(TestData.KEY_NO_PERMISSIONS)))
134+
.andExpect(status().is(404));
135+
}
136+
137+
@Test
138+
void testGetMainPageOfHiddenWithoutAuth() throws Exception {
139+
this.mockMvc.perform(get("/api/v1/pages/main/" + TestData.PRIVATE_PROJECT.getSlug()))
140+
.andExpect(status().is(404));
141+
}
142+
143+
@Test
144+
void testEditMainPageWithoutPermission() throws Exception {
145+
// User without EDIT_PAGE permission should be denied
146+
this.mockMvc.perform(patch("/api/v1/pages/editmain/" + TestData.PROJECT.getSlug())
147+
.content(this.objectMapper.writeValueAsBytes(new StringContent("# Test\nUnauthorized")))
148+
.contentType(MediaType.APPLICATION_JSON)
149+
.with(this.apiKey(TestData.KEY_PROJECT_ONLY)))
150+
.andExpect(status().is(404));
151+
}
152+
153+
@Test
154+
void testEditMainPageWithoutAuth() throws Exception {
155+
// Unauthenticated user should be denied
156+
this.mockMvc.perform(patch("/api/v1/pages/editmain/" + TestData.PROJECT.getSlug())
157+
.content(this.objectMapper.writeValueAsBytes(new StringContent("# Test\nUnauthorized")))
158+
.contentType(MediaType.APPLICATION_JSON))
159+
.andExpect(status().is(403));
160+
}
161+
162+
@Test
163+
void testEditPageWithoutPermission() throws Exception {
164+
// User without EDIT_PAGE permission should be denied
165+
this.mockMvc.perform(patch("/api/v1/pages/edit/" + TestData.PROJECT.getSlug())
166+
.content(this.objectMapper.writeValueAsBytes(new PageEditForm(TestData.PAGE_PARENT.getSlug(), "# Unauthorized")))
167+
.contentType(MediaType.APPLICATION_JSON)
168+
.with(this.apiKey(TestData.KEY_PROJECT_ONLY)))
169+
.andExpect(status().is(404));
170+
}
171+
172+
@Test
173+
void testEditPageWithoutAuth() throws Exception {
174+
// Unauthenticated user should be denied
175+
this.mockMvc.perform(patch("/api/v1/pages/edit/" + TestData.PROJECT.getSlug())
176+
.content(this.objectMapper.writeValueAsBytes(new PageEditForm(TestData.PAGE_PARENT.getSlug(), "# Unauthorized")))
177+
.contentType(MediaType.APPLICATION_JSON))
178+
.andExpect(status().is(403));
179+
}
180+
181+
// Authorization tests for @Unlocked annotation
182+
@Test
183+
void testEditMainPageWithLockedUser() throws Exception {
184+
// Locked/banned user should be denied by @Unlocked annotation
185+
this.mockMvc.perform(patch("/api/v1/pages/editmain/" + TestData.PROJECT.getSlug())
186+
.content(this.objectMapper.writeValueAsBytes(new StringContent("# Locked user edit")))
187+
.contentType(MediaType.APPLICATION_JSON)
188+
.with(this.apiKey(TestData.KEY_BANNED)))
189+
.andExpect(status().is(404));
190+
}
191+
192+
@Test
193+
void testEditPageWithLockedUser() throws Exception {
194+
// Locked/banned user should be denied by @Unlocked annotation
195+
this.mockMvc.perform(patch("/api/v1/pages/edit/" + TestData.PROJECT.getSlug())
196+
.content(this.objectMapper.writeValueAsBytes(new PageEditForm(TestData.PAGE_PARENT.getSlug(), "# Locked user edit")))
197+
.contentType(MediaType.APPLICATION_JSON)
198+
.with(this.apiKey(TestData.KEY_BANNED)))
199+
.andExpect(status().is(404));
200+
}
127201
}

backend/src/test/java/io/papermc/hangar/controller/api/v1/ProjectsControllerTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,73 @@ void testGetHiddenProjectById() throws Exception {
4343
.andExpect(status().is(404));
4444
}
4545

46+
// Authorization tests for @VisibilityRequired annotation
47+
@Test
48+
void testGetHiddenProjectAsAdmin() throws Exception {
49+
// Admin should be able to see hidden projects
50+
this.mockMvc.perform(get("/api/v1/projects/PrivateProject")
51+
.with(this.apiKey(TestData.KEY_ADMIN)))
52+
.andExpect(status().is(200))
53+
.andExpect(jsonPath("$.name", is("PrivateProject")));
54+
}
55+
56+
@Test
57+
void testGetHiddenProjectAsOwner() throws Exception {
58+
// Project owner should be able to see their own hidden project
59+
// Note: PaperMC org owns PrivateProject
60+
this.mockMvc.perform(get("/api/v1/projects/PrivateProject")
61+
.with(this.apiKey(TestData.KEY_ADMIN))) // Admin is part of PaperMC org
62+
.andExpect(status().is(200))
63+
.andExpect(jsonPath("$.name", is("PrivateProject")));
64+
}
65+
66+
@Test
67+
void testGetHiddenProjectWithoutPermission() throws Exception {
68+
// Regular user without permission should get 404
69+
this.mockMvc.perform(get("/api/v1/projects/PrivateProject"))
70+
.andExpect(status().is(404));
71+
}
72+
73+
@Test
74+
void testGetHiddenProjectByIdAsAdmin() throws Exception {
75+
// Admin should be able to see hidden projects by ID
76+
this.mockMvc.perform(get("/api/v1/projects/" + TestData.PRIVATE_PROJECT.getProjectId())
77+
.with(this.apiKey(TestData.KEY_ADMIN)))
78+
.andExpect(status().is(200))
79+
.andExpect(jsonPath("$.name", is("PrivateProject")));
80+
}
81+
82+
@Test
83+
void testGetHiddenProjectByIdWithoutPermission() throws Exception {
84+
// Regular user without permission should get 404
85+
this.mockMvc.perform(get("/api/v1/projects/" + TestData.PRIVATE_PROJECT.getProjectId()))
86+
.andExpect(status().is(404));
87+
}
88+
89+
// Authorization tests for @PermissionRequired annotation
90+
@Test
91+
void testGetProjectStatsWithPermission() throws Exception {
92+
// Admin with IS_SUBJECT_MEMBER permission should access stats
93+
this.mockMvc.perform(get("/api/v1/projects/TestProject/stats?fromDate=2020-01-01T00:00:00Z&toDate=2030-12-31T23:59:59Z")
94+
.with(this.apiKey(TestData.KEY_ADMIN)))
95+
.andExpect(status().is(200));
96+
}
97+
98+
@Test
99+
void testGetProjectStatsWithoutPermission() throws Exception {
100+
// User without IS_SUBJECT_MEMBER permission should be denied
101+
this.mockMvc.perform(get("/api/v1/projects/TestProject/stats?fromDate=2020-01-01T00:00:00Z&toDate=2030-12-31T23:59:59Z")
102+
.with(this.apiKey(TestData.KEY_PROJECT_ONLY)))
103+
.andExpect(status().is(404));
104+
}
105+
106+
@Test
107+
void testGetProjectStatsWithoutAuth() throws Exception {
108+
// Unauthenticated user should be denied
109+
this.mockMvc.perform(get("/api/v1/projects/TestProject/stats?fromDate=2020-01-01T00:00:00Z&toDate=2030-12-31T23:59:59Z"))
110+
.andExpect(status().is(404));
111+
}
112+
46113
@Test
47114
void testGetMembers() throws Exception {
48115
this.mockMvc.perform(get("/api/v1/projects/TestProject/members")

backend/src/test/java/io/papermc/hangar/controller/api/v1/UserControllerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ void testGetUsers() throws Exception {
6666
this.mockMvc.perform(get("/api/v1/users?query=Test")
6767
.with(this.apiKey(TestData.KEY_ADMIN)))
6868
.andExpect(status().is(200))
69-
.andExpect(jsonPath("$.pagination.count", is(4)))
70-
.andExpect(jsonPath("$.result[*].name", contains("TestUser", "TestMember", "TestAdmin", TestData.USER_BANNED.getName())));
69+
.andExpect(jsonPath("$.pagination.count", is(5)))
70+
.andExpect(jsonPath("$.result[*].name", contains("TestUser", "TestMember", "TestAdmin", TestData.USER_BANNED.getName(), "TestOwner")));
7171
}
7272

7373
@Test

backend/src/test/java/io/papermc/hangar/controller/api/v1/VersionsControllerTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,77 @@ void testDownload() throws Exception {
126126
// TODO testDownload
127127
throw new RuntimeException();
128128
}
129+
130+
// Authorization tests for @VisibilityRequired annotation
131+
@Test
132+
void testGetHiddenVersionAsAdmin() throws Exception {
133+
// Admin should be able to see hidden versions
134+
this.mockMvc.perform(get("/api/v1/projects/TestProject/versions/" + TestData.VERSION_HIDDEN.getId())
135+
.with(this.apiKey(TestData.KEY_ADMIN)))
136+
.andExpect(status().is(200))
137+
.andExpect(jsonPath("$.name", is("2.0")));
138+
}
139+
140+
@Test
141+
@Disabled // TODO wtf
142+
void testGetHiddenVersionWithSeeHiddenPermission() throws Exception {
143+
// User with SEE_HIDDEN permission should see hidden versions
144+
this.mockMvc.perform(get("/api/v1/projects/TestProject/versions/" + TestData.VERSION_HIDDEN.getId())
145+
.with(this.apiKey(TestData.KEY_SEE_HIDDEN)))
146+
.andExpect(status().is(200))
147+
.andExpect(jsonPath("$.name", is("2.0")));
148+
}
149+
150+
@Test
151+
void testGetHiddenVersionWithoutPermission() throws Exception {
152+
// Regular user should get 404 for hidden version
153+
this.mockMvc.perform(get("/api/v1/projects/TestProject/versions/" + TestData.VERSION_HIDDEN.getId()))
154+
.andExpect(status().is(404));
155+
}
156+
157+
@Test
158+
void testGetVersionsOfHiddenProjectAsAdmin() throws Exception {
159+
// Admin should see versions of hidden projects
160+
this.mockMvc.perform(get("/api/v1/projects/PrivateProject/versions")
161+
.with(this.apiKey(TestData.KEY_ADMIN)))
162+
.andExpect(status().is(200));
163+
}
164+
165+
@Test
166+
void testGetVersionsOfHiddenProjectWithoutPermission() throws Exception {
167+
// Regular user should get 404 for hidden project versions
168+
this.mockMvc.perform(get("/api/v1/projects/PrivateProject/versions"))
169+
.andExpect(status().is(404));
170+
}
171+
172+
// Authorization tests for @PermissionRequired, @Unlocked, @RequireAal annotations
173+
@Test
174+
@Disabled // TODO upload
175+
void testUploadVersionWithoutPermission() throws Exception {
176+
// User without CREATE_VERSION permission should be denied
177+
this.mockMvc.perform(post("/api/v1/projects/TestProject/upload")
178+
.with(this.apiKey(TestData.KEY_PROJECT_ONLY))
179+
.header("Content-Type", "multipart/form-data"))
180+
.andExpect(status().is(403));
181+
}
182+
183+
@Test
184+
@Disabled // TODO upload
185+
void testUploadVersionWithoutAuth() throws Exception {
186+
// Unauthenticated user should be denied
187+
this.mockMvc.perform(post("/api/v1/projects/TestProject/upload")
188+
.header("Content-Type", "multipart/form-data"))
189+
.andExpect(status().is(401));
190+
}
191+
192+
// Authorization tests for @Unlocked annotation
193+
@Test
194+
@Disabled // TODO upload
195+
void testUploadVersionWithLockedUser() throws Exception {
196+
// Locked/banned user should be denied by @Unlocked annotation
197+
this.mockMvc.perform(post("/api/v1/projects/TestProject/upload")
198+
.with(this.apiKey(TestData.KEY_BANNED))
199+
.header("Content-Type", "multipart/form-data"))
200+
.andExpect(status().is(403));
201+
}
129202
}

backend/src/test/java/io/papermc/hangar/controller/helper/TestData.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,14 @@ public class TestData {
6262
public static UserTable USER_MEMBER;
6363
public static UserTable USER_ADMIN;
6464
public static UserTable USER_BANNED;
65+
public static UserTable USER_PROJECT_OWNER;
6566

6667
public static String KEY_ADMIN;
6768
public static String KEY_PROJECT_ONLY;
6869
public static String KEY_SEE_HIDDEN;
70+
public static String KEY_PROJECT_OWNER;
71+
public static String KEY_NO_PERMISSIONS;
72+
public static String KEY_BANNED;
6973

7074
public static OrganizationTable ORG;
7175

@@ -112,16 +116,19 @@ public void prepare() {
112116
USER_MEMBER = this.authService.registerUser(new SignupForm("TestMember", "testmember@papermc.io", "W45nNUefrsB8ucQeiKDdbEQijH5KP", true, null));
113117
USER_ADMIN = this.authService.registerUser(new SignupForm("TestAdmin", "testadmin@papermc.io", "W45nNUefrsB8ucQeiKDdbEQijH5KP", true, null));
114118
USER_BANNED = this.authService.registerUser(new SignupForm("TestBanned", "testbanned@papermc.io", "W45nNUefrsB8ucQeiKDdbEQijH5KP", true, null));
119+
USER_PROJECT_OWNER = this.authService.registerUser(new SignupForm("TestOwner", "testowner@papermc.io", "W45nNUefrsB8ucQeiKDdbEQijH5KP", true, null));
115120

116121
USER_NORMAL.setEmailVerified(true);
117122
USER_MEMBER.setEmailVerified(true);
118123
USER_ADMIN.setEmailVerified(true);
119124
USER_BANNED.setEmailVerified(true);
125+
USER_PROJECT_OWNER.setEmailVerified(true);
120126
USER_BANNED.setLocked(true);
121127
this.userDAO.update(USER_NORMAL);
122128
this.userDAO.update(USER_MEMBER);
123129
this.userDAO.update(USER_ADMIN);
124130
this.userDAO.update(USER_BANNED);
131+
this.userDAO.update(USER_PROJECT_OWNER);
125132

126133
this.globalRoleService.addRole(new GlobalRoleTable(USER_ADMIN.getUserId(), GlobalRole.HANGAR_ADMIN));
127134

@@ -150,6 +157,9 @@ public void prepare() {
150157
KEY_ADMIN = this.apiKeyService.createApiKey(USER_ADMIN, new CreateAPIKeyForm("Admin", Set.of(NamedPermission.values())), Permission.All);
151158
KEY_PROJECT_ONLY = this.apiKeyService.createApiKey(USER_NORMAL, new CreateAPIKeyForm("Project Only", Set.of(NamedPermission.CREATE_PROJECT)), Permission.All);
152159
KEY_SEE_HIDDEN = this.apiKeyService.createApiKey(USER_NORMAL, new CreateAPIKeyForm("See Hidden", Set.of(NamedPermission.SEE_HIDDEN)), Permission.All);
160+
KEY_PROJECT_OWNER = this.apiKeyService.createApiKey(USER_PROJECT_OWNER, new CreateAPIKeyForm("Project Owner", Set.of(NamedPermission.values())), Permission.All);
161+
KEY_NO_PERMISSIONS = this.apiKeyService.createApiKey(USER_NORMAL, new CreateAPIKeyForm("No Permissions", Set.of()), Permission.None);
162+
KEY_BANNED = this.apiKeyService.createApiKey(USER_BANNED, new CreateAPIKeyForm("No Permissions", Set.of(NamedPermission.VIEW_PUBLIC_INFO, NamedPermission.EDIT_OWN_USER_SETTINGS, NamedPermission.EDIT_API_KEYS)), Permission.All);
153163

154164
this.userService.toggleStarred(USER_NORMAL.getUserId(), PROJECT.getProjectId(), true);
155165
this.userService.toggleWatching(USER_NORMAL.getUserId(), PROJECT.getProjectId(), true);

0 commit comments

Comments
 (0)