Skip to content

Commit 8bce040

Browse files
Copilotphrocker
andcommitted
Add JiraProxyController with core JIRA REST API endpoints
Co-authored-by: phrocker <[email protected]>
1 parent 90cc9fa commit 8bce040

File tree

2 files changed

+572
-0
lines changed

2 files changed

+572
-0
lines changed
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
package io.sentrius.sso.controllers.api;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
import java.util.concurrent.ExecutionException;
6+
import com.fasterxml.jackson.core.JsonProcessingException;
7+
import io.opentelemetry.api.GlobalOpenTelemetry;
8+
import io.opentelemetry.api.trace.Span;
9+
import io.opentelemetry.api.trace.Tracer;
10+
import io.opentelemetry.context.Scope;
11+
import io.sentrius.sso.config.ApplicationConfig;
12+
import io.sentrius.sso.core.annotations.LimitAccess;
13+
import io.sentrius.sso.core.config.SystemOptions;
14+
import io.sentrius.sso.core.controllers.BaseController;
15+
import io.sentrius.sso.core.dto.TicketDTO;
16+
import io.sentrius.sso.core.integrations.ticketing.JiraService;
17+
import io.sentrius.sso.core.model.security.IntegrationSecurityToken;
18+
import io.sentrius.sso.core.model.security.enums.ApplicationAccessEnum;
19+
import io.sentrius.sso.core.services.ErrorOutputService;
20+
import io.sentrius.sso.core.services.UserService;
21+
import io.sentrius.sso.core.services.security.IntegrationSecurityTokenService;
22+
import io.sentrius.sso.core.services.security.KeycloakService;
23+
import io.sentrius.sso.integrations.exceptions.HttpException;
24+
import jakarta.servlet.http.HttpServletRequest;
25+
import jakarta.servlet.http.HttpServletResponse;
26+
import lombok.extern.slf4j.Slf4j;
27+
import org.apache.http.HttpStatus;
28+
import org.springframework.boot.web.client.RestTemplateBuilder;
29+
import org.springframework.http.ResponseEntity;
30+
import org.springframework.web.bind.annotation.*;
31+
import org.springframework.web.client.RestTemplate;
32+
33+
@RestController
34+
@RequestMapping("/api/v1/jira")
35+
@Slf4j
36+
public class JiraProxyController extends BaseController {
37+
38+
final KeycloakService keycloakService;
39+
final IntegrationSecurityTokenService integrationSecurityTokenService;
40+
final RestTemplateBuilder restTemplateBuilder;
41+
final ApplicationConfig applicationConfig;
42+
43+
Tracer tracer = GlobalOpenTelemetry.getTracer("io.sentrius.sso");
44+
45+
protected JiraProxyController(
46+
UserService userService,
47+
SystemOptions systemOptions,
48+
ErrorOutputService errorOutputService,
49+
KeycloakService keycloakService,
50+
IntegrationSecurityTokenService integrationSecurityTokenService,
51+
RestTemplateBuilder restTemplateBuilder,
52+
ApplicationConfig applicationConfig
53+
) {
54+
super(userService, systemOptions, errorOutputService);
55+
this.keycloakService = keycloakService;
56+
this.integrationSecurityTokenService = integrationSecurityTokenService;
57+
this.restTemplateBuilder = restTemplateBuilder;
58+
this.applicationConfig = applicationConfig;
59+
}
60+
61+
@GetMapping("/rest/api/3/search")
62+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
63+
public ResponseEntity<?> search(
64+
@RequestHeader("Authorization") String token,
65+
@RequestParam(value = "jql", required = false) String jql,
66+
@RequestParam(value = "query", required = false) String query,
67+
HttpServletRequest request,
68+
HttpServletResponse response
69+
) throws JsonProcessingException, HttpException {
70+
71+
Span span = tracer.spanBuilder("jira-proxy-search").startSpan();
72+
try (Scope scope = span.makeCurrent()) {
73+
String compactJwt = token.startsWith("Bearer ") ? token.substring(7) : token;
74+
75+
if (!keycloakService.validateJwt(compactJwt)) {
76+
log.warn("Invalid Keycloak token");
77+
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).body("Invalid Keycloak token");
78+
}
79+
80+
var operatingUser = getOperatingUser(request, response);
81+
if (null == operatingUser) {
82+
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).body("User not authenticated");
83+
}
84+
85+
// Get the first available JIRA integration for the user
86+
// In a production environment, you might want to allow specifying which integration to use
87+
List<IntegrationSecurityToken> jiraIntegrations = integrationSecurityTokenService
88+
.findByConnectionType("jira");
89+
90+
if (jiraIntegrations.isEmpty()) {
91+
return ResponseEntity.status(HttpStatus.SC_NOT_FOUND).body("No JIRA integration configured");
92+
}
93+
94+
IntegrationSecurityToken jiraIntegration = jiraIntegrations.get(0);
95+
JiraService jiraService = new JiraService(new RestTemplate(), jiraIntegration);
96+
97+
// Use the query parameter if jql is not provided
98+
String searchQuery = jql != null ? jql : query;
99+
if (searchQuery == null) {
100+
return ResponseEntity.badRequest().body("Either 'jql' or 'query' parameter is required");
101+
}
102+
103+
List<TicketDTO> tickets = jiraService.searchForIncidents(searchQuery);
104+
105+
span.setAttribute("search.query", searchQuery);
106+
span.setAttribute("search.results.count", tickets.size());
107+
108+
return ResponseEntity.ok(tickets);
109+
110+
} catch (ExecutionException | InterruptedException e) {
111+
log.error("Error executing JIRA search", e);
112+
throw new RuntimeException(e);
113+
} finally {
114+
span.end();
115+
}
116+
}
117+
118+
@GetMapping("/rest/api/3/issue/{issueKey}")
119+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
120+
public ResponseEntity<?> getIssue(
121+
@RequestHeader("Authorization") String token,
122+
@PathVariable String issueKey,
123+
HttpServletRequest request,
124+
HttpServletResponse response
125+
) throws JsonProcessingException, HttpException {
126+
127+
Span span = tracer.spanBuilder("jira-proxy-get-issue").startSpan();
128+
try (Scope scope = span.makeCurrent()) {
129+
String compactJwt = token.startsWith("Bearer ") ? token.substring(7) : token;
130+
131+
if (!keycloakService.validateJwt(compactJwt)) {
132+
log.warn("Invalid Keycloak token");
133+
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).body("Invalid Keycloak token");
134+
}
135+
136+
var operatingUser = getOperatingUser(request, response);
137+
if (null == operatingUser) {
138+
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).body("User not authenticated");
139+
}
140+
141+
List<IntegrationSecurityToken> jiraIntegrations = integrationSecurityTokenService
142+
.findByConnectionType("jira");
143+
144+
if (jiraIntegrations.isEmpty()) {
145+
return ResponseEntity.status(HttpStatus.SC_NOT_FOUND).body("No JIRA integration configured");
146+
}
147+
148+
IntegrationSecurityToken jiraIntegration = jiraIntegrations.get(0);
149+
JiraService jiraService = new JiraService(new RestTemplate(), jiraIntegration);
150+
151+
boolean isActive = jiraService.isTicketActive(issueKey);
152+
153+
span.setAttribute("issue.key", issueKey);
154+
span.setAttribute("issue.active", isActive);
155+
156+
return ResponseEntity.ok(new IssueStatusResponse(issueKey, isActive ? "Active" : "Inactive"));
157+
158+
} finally {
159+
span.end();
160+
}
161+
}
162+
163+
@PostMapping("/rest/api/3/issue/{issueKey}/comment")
164+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
165+
public ResponseEntity<?> addComment(
166+
@RequestHeader("Authorization") String token,
167+
@PathVariable String issueKey,
168+
@RequestBody CommentRequest commentRequest,
169+
HttpServletRequest request,
170+
HttpServletResponse response
171+
) throws JsonProcessingException, HttpException {
172+
173+
Span span = tracer.spanBuilder("jira-proxy-add-comment").startSpan();
174+
try (Scope scope = span.makeCurrent()) {
175+
String compactJwt = token.startsWith("Bearer ") ? token.substring(7) : token;
176+
177+
if (!keycloakService.validateJwt(compactJwt)) {
178+
log.warn("Invalid Keycloak token");
179+
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).body("Invalid Keycloak token");
180+
}
181+
182+
var operatingUser = getOperatingUser(request, response);
183+
if (null == operatingUser) {
184+
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).body("User not authenticated");
185+
}
186+
187+
List<IntegrationSecurityToken> jiraIntegrations = integrationSecurityTokenService
188+
.findByConnectionType("jira");
189+
190+
if (jiraIntegrations.isEmpty()) {
191+
return ResponseEntity.status(HttpStatus.SC_NOT_FOUND).body("No JIRA integration configured");
192+
}
193+
194+
IntegrationSecurityToken jiraIntegration = jiraIntegrations.get(0);
195+
JiraService jiraService = new JiraService(new RestTemplate(), jiraIntegration);
196+
197+
// Extract comment text from the request
198+
String commentText = extractCommentText(commentRequest);
199+
if (commentText == null || commentText.trim().isEmpty()) {
200+
return ResponseEntity.badRequest().body("Comment text is required");
201+
}
202+
203+
boolean success = jiraService.updateTicket(issueKey, commentText);
204+
205+
span.setAttribute("issue.key", issueKey);
206+
span.setAttribute("comment.success", success);
207+
208+
if (success) {
209+
return ResponseEntity.ok(new CommentResponse("Comment added successfully"));
210+
} else {
211+
return ResponseEntity.status(HttpStatus.SC_INTERNAL_SERVER_ERROR)
212+
.body("Failed to add comment to issue");
213+
}
214+
215+
} finally {
216+
span.end();
217+
}
218+
}
219+
220+
@PutMapping("/rest/api/3/issue/{issueKey}/assignee")
221+
@LimitAccess(applicationAccess = {ApplicationAccessEnum.CAN_LOG_IN})
222+
public ResponseEntity<?> assignIssue(
223+
@RequestHeader("Authorization") String token,
224+
@PathVariable String issueKey,
225+
@RequestBody AssigneeRequest assigneeRequest,
226+
HttpServletRequest request,
227+
HttpServletResponse response
228+
) throws JsonProcessingException, HttpException {
229+
230+
Span span = tracer.spanBuilder("jira-proxy-assign-issue").startSpan();
231+
try (Scope scope = span.makeCurrent()) {
232+
String compactJwt = token.startsWith("Bearer ") ? token.substring(7) : token;
233+
234+
if (!keycloakService.validateJwt(compactJwt)) {
235+
log.warn("Invalid Keycloak token");
236+
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).body("Invalid Keycloak token");
237+
}
238+
239+
var operatingUser = getOperatingUser(request, response);
240+
if (null == operatingUser) {
241+
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).body("User not authenticated");
242+
}
243+
244+
List<IntegrationSecurityToken> jiraIntegrations = integrationSecurityTokenService
245+
.findByConnectionType("jira");
246+
247+
if (jiraIntegrations.isEmpty()) {
248+
return ResponseEntity.status(HttpStatus.SC_NOT_FOUND).body("No JIRA integration configured");
249+
}
250+
251+
IntegrationSecurityToken jiraIntegration = jiraIntegrations.get(0);
252+
JiraService jiraService = new JiraService(new RestTemplate(), jiraIntegration);
253+
254+
Optional<String> assigneeId = Optional.ofNullable(assigneeRequest.getAccountId());
255+
boolean success = jiraService.assignTicket(issueKey, assigneeId);
256+
257+
span.setAttribute("issue.key", issueKey);
258+
span.setAttribute("assignee.id", assigneeId.orElse("unassigned"));
259+
span.setAttribute("assignment.success", success);
260+
261+
if (success) {
262+
return ResponseEntity.noContent().build();
263+
} else {
264+
return ResponseEntity.status(HttpStatus.SC_INTERNAL_SERVER_ERROR)
265+
.body("Failed to assign issue");
266+
}
267+
268+
} finally {
269+
span.end();
270+
}
271+
}
272+
273+
private String extractCommentText(CommentRequest commentRequest) {
274+
// Handle both simple text and JIRA's complex body structure
275+
if (commentRequest.getBody() != null) {
276+
// Try to extract text from JIRA's structured body format
277+
Object body = commentRequest.getBody();
278+
if (body instanceof String) {
279+
return (String) body;
280+
}
281+
// For complex body structures, try to extract text
282+
// This would need more sophisticated parsing for real JIRA body format
283+
return body.toString();
284+
}
285+
return commentRequest.getText();
286+
}
287+
288+
// DTOs for request/response
289+
public static class CommentRequest {
290+
private Object body;
291+
private String text;
292+
293+
public Object getBody() { return body; }
294+
public void setBody(Object body) { this.body = body; }
295+
public String getText() { return text; }
296+
public void setText(String text) { this.text = text; }
297+
}
298+
299+
public static class AssigneeRequest {
300+
private String accountId;
301+
302+
public String getAccountId() { return accountId; }
303+
public void setAccountId(String accountId) { this.accountId = accountId; }
304+
}
305+
306+
public static class IssueStatusResponse {
307+
private final String key;
308+
private final String status;
309+
310+
public IssueStatusResponse(String key, String status) {
311+
this.key = key;
312+
this.status = status;
313+
}
314+
315+
public String getKey() { return key; }
316+
public String getStatus() { return status; }
317+
}
318+
319+
public static class CommentResponse {
320+
private final String message;
321+
322+
public CommentResponse(String message) {
323+
this.message = message;
324+
}
325+
326+
public String getMessage() { return message; }
327+
}
328+
}

0 commit comments

Comments
 (0)