gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

๐Ÿ› (collaboration): Fix dev login scopes and comment-delete visibility

7a9b04dc93a0ae2ea67be61267cc883861b6f0ee ยท Michael Hainz ยท 2026-07-20T08:33:09Z

Changes

9 files changed, +188 -10

MODIFY docs/maintainers/comments.md +4 -1
diff --git a/docs/maintainers/comments.md b/docs/maintainers/comments.md
index 942ddbe..46687b4 100644
--- a/docs/maintainers/comments.md
+++ b/docs/maintainers/comments.md
@@ -33,7 +33,10 @@
33 33
34 34 - `IssueResource`: `POST {number}/comments` and
35 35 `POST {number}/comments/{commentId}/delete`; the detail view passes the comment
36 - list plus `loggedIn` / `currentUserId` to `issue.html`.
36 + list plus `loggedIn` / `currentUserId` / `canModerate` to `issue.html`. The
37 + delete control renders when the viewer is the comment author or `canModerate`
38 + (`AccessPolicy.canWrite` โ€” owner, collaborator or org member), matching what the
39 + service enforces; `owner` (admin) alone would hide it from collaborators.
37 40 - `MergeRequestResource`: `POST {number}/discussion` for general comments;
38 41 deletion reuses the existing `{number}/comments/{commentId}/delete`. `detail`
39 42 splits `commentService.list(mr)` into line comments (`filePath != null`) and
MODIFY src/main/java/de/workaround/web/IssueResource.java +4 -2
diff --git a/src/main/java/de/workaround/web/IssueResource.java b/src/main/java/de/workaround/web/IssueResource.java
index 027f05f..2077473 100644
--- a/src/main/java/de/workaround/web/IssueResource.java
+++ b/src/main/java/de/workaround/web/IssueResource.java
@@ -47,7 +47,7 @@
47 47
48 48 static native TemplateInstance issue(Repository repo, RepoNav nav, boolean owner, Issue issue,
49 49 String descriptionHtml, List<Issue.Status> statuses, List<User> assignees, boolean loggedIn,
50 - UUID currentUserId, List<IssueComment> comments);
50 + UUID currentUserId, boolean canModerate, List<IssueComment> comments);
51 51 }
52 52
53 53 @Inject
@@ -125,9 +125,11 @@
125 125 boolean isOwner = accessPolicy.canAdmin(user, repo);
126 126 boolean loggedIn = user != null;
127 127 UUID currentUserId = user == null ? null : user.id;
128 + // comment moderation (delete any comment) follows write access: owner, collaborator or org member
129 + boolean canModerate = accessPolicy.canWrite(user, repo);
128 130 String descriptionHtml = issue.description == null ? null : Markdown.render(issue.description);
129 131 return Templates.issue(repo, repoNav.build(repo, uriInfo), isOwner, issue, descriptionHtml,
130 - List.of(Issue.Status.values()), assignableUsers(repo), loggedIn, currentUserId,
132 + List.of(Issue.Status.values()), assignableUsers(repo), loggedIn, currentUserId, canModerate,
131 133 issueCommentService.list(issue));
132 134 }
133 135
MODIFY src/main/java/de/workaround/web/MergeRequestResource.java +6 -4
diff --git a/src/main/java/de/workaround/web/MergeRequestResource.java b/src/main/java/de/workaround/web/MergeRequestResource.java
index e000fa3..d175e92 100644
--- a/src/main/java/de/workaround/web/MergeRequestResource.java
+++ b/src/main/java/de/workaround/web/MergeRequestResource.java
@@ -49,8 +49,8 @@
49 49 String defaultBranch);
50 50
51 51 static native TemplateInstance mergeRequest(Repository repo, RepoNav nav, boolean owner, boolean loggedIn,
52 - UUID currentUserId, MergeRequest mr, List<FileDiffView> files, int additions, int deletions,
53 - List<User> assignees, List<MergeRequestComment> discussion);
52 + UUID currentUserId, boolean canModerate, MergeRequest mr, List<FileDiffView> files, int additions,
53 + int deletions, List<User> assignees, List<MergeRequestComment> discussion);
54 54 }
55 55
56 56 /**
@@ -149,6 +149,8 @@
149 149 User user = currentUser.get();
150 150 boolean loggedIn = user != null;
151 151 UUID currentUserId = user == null ? null : user.id;
152 + // comment moderation (delete any comment) follows write access: owner, collaborator or org member
153 + boolean canModerate = accessPolicy.canWrite(user, repo);
152 154
153 155 List<FileDiffView> files = new ArrayList<>();
154 156 int additions = 0;
@@ -179,8 +181,8 @@
179 181 fileIndex++;
180 182 }
181 183 }
182 - return Templates.mergeRequest(repo, repoNav.build(repo, uriInfo), isOwner(repo), loggedIn, currentUserId, mr,
183 - files, additions, deletions, assignableUsers(repo), discussion);
184 + return Templates.mergeRequest(repo, repoNav.build(repo, uriInfo), isOwner(repo), loggedIn, currentUserId,
185 + canModerate, mr, files, additions, deletions, assignableUsers(repo), discussion);
184 186 }
185 187
186 188 /**
ADD src/main/resources/quarkus-realm.json +56 -0
diff --git a/src/main/resources/quarkus-realm.json b/src/main/resources/quarkus-realm.json
new file mode 100644
index 0000000..4f745bf
--- /dev/null
+++ b/src/main/resources/quarkus-realm.json
@@ -0,0 +1,56 @@
1 +{
2 + "realm": "quarkus",
3 + "enabled": true,
4 + "sslRequired": "none",
5 + "registrationAllowed": false,
6 + "loginWithEmailAllowed": true,
7 + "duplicateEmailsAllowed": false,
8 + "roles": {
9 + "realm": [
10 + { "name": "user", "description": "User privileges" },
11 + { "name": "admin", "description": "Administrator privileges" },
12 + { "name": "confidential", "description": "Confidential privileges" }
13 + ]
14 + },
15 + "users": [
16 + {
17 + "username": "alice",
18 + "enabled": true,
19 + "email": "alice@example.com",
20 + "emailVerified": true,
21 + "firstName": "Alice",
22 + "lastName": "Demo",
23 + "credentials": [
24 + { "type": "password", "value": "alice" }
25 + ],
26 + "realmRoles": [ "user", "admin" ]
27 + },
28 + {
29 + "username": "bob",
30 + "enabled": true,
31 + "email": "bob@example.com",
32 + "emailVerified": true,
33 + "firstName": "Bob",
34 + "lastName": "Demo",
35 + "credentials": [
36 + { "type": "password", "value": "bob" }
37 + ],
38 + "realmRoles": [ "user" ]
39 + }
40 + ],
41 + "clients": [
42 + {
43 + "clientId": "quarkus-app",
44 + "enabled": true,
45 + "publicClient": false,
46 + "secret": "secret",
47 + "clientAuthenticatorType": "client-secret",
48 + "standardFlowEnabled": true,
49 + "directAccessGrantsEnabled": true,
50 + "redirectUris": [ "*" ],
51 + "webOrigins": [ "*" ],
52 + "defaultClientScopes": [ "profile", "email", "roles", "web-origins", "acr", "basic" ],
53 + "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ]
54 + }
55 + ]
56 +}
MODIFY src/main/resources/templates/IssueResource/issue.html +1 -1
diff --git a/src/main/resources/templates/IssueResource/issue.html b/src/main/resources/templates/IssueResource/issue.html
index 3eaf22e..cc3cf67 100644
--- a/src/main/resources/templates/IssueResource/issue.html
+++ b/src/main/resources/templates/IssueResource/issue.html
@@ -44,7 +44,7 @@
44 44 </div>
45 45 <div class="comment-body">{c.body}</div>
46 46 </div>
47 - {#if owner || c.author.id == currentUserId}
47 + {#if canModerate || c.author.id == currentUserId}
48 48 <form class="comment-del-form" method="post"
49 49 action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/comments/{c.id}/delete">
50 50 <button type="submit" class="comment-del" title="Delete comment" aria-label="Delete comment">
MODIFY src/main/resources/templates/MergeRequestResource/mergeRequest.html +2 -2
diff --git a/src/main/resources/templates/MergeRequestResource/mergeRequest.html b/src/main/resources/templates/MergeRequestResource/mergeRequest.html
index f6b9716..f3fe823 100644
--- a/src/main/resources/templates/MergeRequestResource/mergeRequest.html
+++ b/src/main/resources/templates/MergeRequestResource/mergeRequest.html
@@ -121,7 +121,7 @@
121 121 </div>
122 122 <div class="comment-body">{c.body}</div>
123 123 </div>
124 - {#if owner || c.author.id == currentUserId}
124 + {#if canModerate || c.author.id == currentUserId}
125 125 <form class="comment-del-form" method="post"
126 126 action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/comments/{c.id}/delete">
127 127 <button type="submit" class="comment-del" title="Delete comment" aria-label="Delete comment">
@@ -196,7 +196,7 @@
196 196 </div>
197 197 <div class="comment-body">{c.body}</div>
198 198 </div>
199 - {#if owner || c.author.id == currentUserId}
199 + {#if canModerate || c.author.id == currentUserId}
200 200 <form class="comment-del-form" method="post"
201 201 action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests/{mr.number}/comments/{c.id}/delete">
202 202 <button type="submit" class="comment-del" title="Delete comment" aria-label="Delete comment">
ADD src/test/java/de/workaround/dev/DevRealmFileTest.java +47 -0
diff --git a/src/test/java/de/workaround/dev/DevRealmFileTest.java b/src/test/java/de/workaround/dev/DevRealmFileTest.java
new file mode 100644
index 0000000..5dd2aee
--- /dev/null
+++ b/src/test/java/de/workaround/dev/DevRealmFileTest.java
@@ -0,0 +1,47 @@
1 +package de.workaround.dev;
2 +
3 +import java.io.InputStream;
4 +import java.util.List;
5 +
6 +import org.junit.jupiter.api.Test;
7 +
8 +import com.fasterxml.jackson.databind.JsonNode;
9 +import com.fasterxml.jackson.databind.ObjectMapper;
10 +
11 +import static org.junit.jupiter.api.Assertions.assertNotNull;
12 +import static org.junit.jupiter.api.Assertions.assertTrue;
13 +
14 +/**
15 + * Guards the dev/test Keycloak realm shipped for {@code quarkus.keycloak.devservices.realm-path}. Without this
16 + * file the auto-provisioned realm's {@code quarkus-app} client lacks the {@code profile}/{@code email} client
17 + * scopes the app requests via {@code quarkus.oidc.authentication.scopes}, so every browser login fails with
18 + * {@code invalid_scope}. The file must therefore exist and grant both scopes to the client by default.
19 + */
20 +class DevRealmFileTest
21 +{
22 + @Test
23 + void quarkusAppClientGrantsProfileAndEmailScopesByDefault() throws Exception
24 + {
25 + try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("quarkus-realm.json"))
26 + {
27 + assertNotNull(in, "quarkus-realm.json must be on the classpath (referenced by devservices.realm-path)");
28 + JsonNode realm = new ObjectMapper().readTree(in);
29 +
30 + JsonNode client = null;
31 + for (JsonNode c : realm.path("clients"))
32 + {
33 + if ("quarkus-app".equals(c.path("clientId").asText()))
34 + {
35 + client = c;
36 + break;
37 + }
38 + }
39 + assertNotNull(client, "realm must define the quarkus-app client");
40 +
41 + List<String> defaultScopes = new ObjectMapper()
42 + .convertValue(client.path("defaultClientScopes"), List.class);
43 + assertTrue(defaultScopes.contains("profile"), "quarkus-app must grant the 'profile' scope by default");
44 + assertTrue(defaultScopes.contains("email"), "quarkus-app must grant the 'email' scope by default");
45 + }
46 + }
47 +}
MODIFY src/test/java/de/workaround/web/IssueCommentUiTest.java +29 -0
diff --git a/src/test/java/de/workaround/web/IssueCommentUiTest.java b/src/test/java/de/workaround/web/IssueCommentUiTest.java
index d8662bd..85e0943 100644
--- a/src/test/java/de/workaround/web/IssueCommentUiTest.java
+++ b/src/test/java/de/workaround/web/IssueCommentUiTest.java
@@ -4,6 +4,7 @@
4 4
5 5 import org.junit.jupiter.api.Test;
6 6
7 +import de.workaround.git.CollaboratorService;
7 8 import de.workaround.git.GitRepositoryService;
8 9 import de.workaround.git.IssueCommentService;
9 10 import de.workaround.git.IssueService;
@@ -35,6 +36,28 @@
35 36 @Inject
36 37 User.Repo userRepo;
37 38
39 + @Inject
40 + CollaboratorService collaboratorService;
41 +
42 + @Test
43 + @TestSecurity(user = "ic-collab")
44 + void aCollaboratorSeesTheDeleteControlOnAnotherUsersComment()
45 + {
46 + User collaborator = persistUser("ic-collab");
47 + User owner = persistUser("ic-owner-collab-" + UUID.randomUUID().toString().substring(0, 8));
48 + Repository repo = service.create(owner, "board", Repository.Visibility.PUBLIC, null);
49 + addCollaborator(owner, repo, collaborator);
50 + Issue issue = issueService.create(owner, repo, "Team issue", null);
51 + IssueComment comment = comments.add(owner, issue, "owner wrote this");
52 + String detail = "/repos/" + owner.username + "/board/issues/" + issue.number;
53 +
54 + // a collaborator (write access, not the owner and not the author) may delete comments,
55 + // so the delete control must be rendered for them
56 + given().when().get(detail)
57 + .then().statusCode(200)
58 + .body(containsString(detail + "/comments/" + comment.id + "/delete"));
59 + }
60 +
38 61 @Test
39 62 @TestSecurity(user = "ic-owner")
40 63 void ownerCanCommentOnAnIssueAndSeeItInline()
@@ -126,6 +149,12 @@
126 149 }
127 150
128 151 @Transactional
152 + void addCollaborator(User owner, Repository repo, User member)
153 + {
154 + collaboratorService.add(owner, repo, member.username);
155 + }
156 +
157 + @Transactional
129 158 User persistUser(String name)
130 159 {
131 160 User existing = userRepo.findByOidcSubOptional(name).orElse(null);
MODIFY src/test/java/de/workaround/web/MergeRequestDiscussionUiTest.java +39 -0
diff --git a/src/test/java/de/workaround/web/MergeRequestDiscussionUiTest.java b/src/test/java/de/workaround/web/MergeRequestDiscussionUiTest.java
index 5f73a49..1048b81 100644
--- a/src/test/java/de/workaround/web/MergeRequestDiscussionUiTest.java
+++ b/src/test/java/de/workaround/web/MergeRequestDiscussionUiTest.java
@@ -10,10 +10,13 @@
10 10 import org.eclipse.jgit.transport.RefSpec;
11 11 import org.junit.jupiter.api.Test;
12 12
13 +import de.workaround.git.CollaboratorService;
13 14 import de.workaround.git.GitRepositoryService;
14 15 import de.workaround.git.GitTestSeeder;
16 +import de.workaround.git.MergeRequestCommentService;
15 17 import de.workaround.git.MergeRequestService;
16 18 import de.workaround.model.MergeRequest;
19 +import de.workaround.model.MergeRequestComment;
17 20 import de.workaround.model.Repository;
18 21 import de.workaround.model.User;
19 22 import io.quarkus.test.junit.QuarkusTest;
@@ -36,6 +39,30 @@
36 39 @Inject
37 40 User.Repo userRepo;
38 41
42 + @Inject
43 + CollaboratorService collaboratorService;
44 +
45 + @Inject
46 + MergeRequestCommentService commentService;
47 +
48 + @Test
49 + @TestSecurity(user = "mrd-collab")
50 + void aCollaboratorSeesTheDeleteControlOnAnotherUsersDiscussionComment()
51 + {
52 + User collaborator = persistUser("mrd-collab");
53 + User owner = persistUser("mrd-owner-collab-" + UUID.randomUUID().toString().substring(0, 8));
54 + MergeRequest mr = seededMr(owner, "board");
55 + addCollaborator(owner, mr.repository, collaborator);
56 + MergeRequestComment comment = addDiscussion(owner, mr, "owner note on the MR");
57 + String detail = "/repos/" + owner.username + "/board/merge-requests/" + mr.number;
58 +
59 + // a collaborator (write access, not the owner and not the author) may delete comments,
60 + // so the delete control must be rendered for them
61 + given().when().get(detail)
62 + .then().statusCode(200)
63 + .body(containsString(detail + "/comments/" + comment.id + "/delete"));
64 + }
65 +
39 66 @Test
40 67 @TestSecurity(user = "mrd-owner")
41 68 void ownerCanPostAGeneralDiscussionCommentAndSeeIt()
@@ -99,6 +126,18 @@
99 126 }
100 127
101 128 @Transactional
129 + void addCollaborator(User owner, Repository repo, User member)
130 + {
131 + collaboratorService.add(owner, repo, member.username);
132 + }
133 +
134 + @Transactional
135 + MergeRequestComment addDiscussion(User author, MergeRequest mr, String body)
136 + {
137 + return commentService.addGeneral(author, mr, body);
138 + }
139 +
140 + @Transactional
102 141 User persistUser(String name)
103 142 {
104 143 User existing = userRepo.findByOidcSubOptional(name).orElse(null);

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog