Skip to content

Commit f41bafb

Browse files
authored
feat(dashboard): add OpenAPI spec generation and Swagger UI (#161)
* feat(dashboard): add OpenAPI spec generation and Swagger UI Adds /api/openapi.json and /api/openapi.yaml endpoints generated from Spring MVC route metadata at startup, plus a Swagger UI browser at /swagger-ui backed by a custom HTML page that bypasses swagger-initializer.js. Uses swagger-core-jakarta (no JAX-RS dep) + RequestMappingHandlerMapping to build the OAS3 spec without springdoc or Spring Boot. WebJar paths are versioned and injected via Gradle token expansion at build time. All controllers annotated with @tag and @operation for a clean spec grouped by System, Auth, Users, Repos, Push, Providers, Profile, Admin. closes #121 * feat(dashboard): add GET /api root endpoint returning version and API docs link Adds a public /api endpoint (no auth) that returns the app version and the path to the OpenAPI spec. Useful as a quick sanity-check that the API is reachable. SecurityConfig updated to permit /api alongside the other public endpoints. * fix(ci): replace depcheck with CycloneDX SBOM + Grype for Gradle CVE scanning OWASP dependency-check is failing due to a breaking NVD API change (nanosecond-precision timestamps not handled by the Jackson deserializer; dependency-check/DependencyCheck#8425). No fix is available yet. Replaces the depcheck job with: - org.cyclonedx.bom 3.2.4 plugin generating a CycloneDX SBOM at build time - anchore/scan-action scanning the SBOM with Grype (same scanner used for npm and container image scans) Also adds failOnError=false to the dependencyCheck config so local runs with the OWASP plugin don't abort when NVD is unavailable. Renames CI job display names to be tool-agnostic: - 'CVE / Dependency Check (Gradle)' -> 'CVE / Gradle' - 'CVE / Grype (npm)' -> 'CVE / npm' - 'Grype / Container Scan' -> 'Container Scan' Branch protection and release gate rulesets updated to match new names.
1 parent 12c70ca commit f41bafb

22 files changed

Lines changed: 382 additions & 48 deletions

.github/workflows/cve.yml

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111

1212
jobs:
1313
grype-npm:
14-
name: CVE / Grype (npm)
14+
name: CVE / npm
1515
runs-on: ubuntu-latest
1616
permissions:
1717
contents: read
@@ -36,9 +36,12 @@ jobs:
3636
with:
3737
sarif_file: ${{ steps.scan.outputs.sarif }}
3838

39-
depcheck:
40-
name: CVE / Dependency Check (Gradle)
39+
grype-gradle:
40+
name: CVE / Gradle
4141
runs-on: ubuntu-latest
42+
permissions:
43+
contents: read
44+
security-events: write
4245
steps:
4346
- name: Checkout
4447
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6
@@ -48,32 +51,29 @@ jobs:
4851
java-version: 21
4952
cache: gradle
5053

51-
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # ratchet:actions/setup-node@v6
54+
- name: Generate SBOM
55+
run: ./gradlew cyclonedxBom -PskipFrontend
56+
57+
- name: Scan SBOM with Grype
58+
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # ratchet:anchore/scan-action@v7
59+
id: scan
5260
with:
53-
node-version: '24.14.1'
54-
cache: npm
55-
cache-dependency-path: git-proxy-java-dashboard/frontend/package-lock.json
61+
sbom: build/reports/cyclonedx/bom.json
62+
fail-build: true
63+
severity-cutoff: high
64+
only-fixed: true
65+
config: .grype.yaml
5666

57-
- name: Build project with Gradle
58-
run: ./gradlew clean testClasses
59-
- name: Cache CVE data
60-
id: cache-cve
61-
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # ratchet:actions/cache@v5
67+
- name: Upload SARIF report
68+
if: ${{ always() }}
69+
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # ratchet:github/codeql-action/upload-sarif@v4
6270
with:
63-
path: ~/.gradle/dependency-check-data/
64-
key: depcheck-db-${{ github.run_id }}
65-
restore-keys: depcheck-db-
66-
- name: Depcheck
67-
run: ./gradlew dependencyCheckAggregate --info
68-
timeout-minutes: 180
69-
env:
70-
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
71-
OSS_INDEX_USERNAME: ${{ secrets.OSS_INDEX_USERNAME }}
72-
OSS_INDEX_TOKEN: ${{ secrets.OSS_INDEX_TOKEN }}
71+
sarif_file: ${{ steps.scan.outputs.sarif }}
7372

74-
- name: Upload Test results
73+
- name: Upload SBOM
7574
if: ${{ always() }}
7675
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # ratchet:actions/upload-artifact@v7
7776
with:
78-
name: Depcheck report
79-
path: ${{ github.workspace }}/build/reports/dependency-check*
77+
name: sbom-gradle
78+
path: build/reports/cyclonedx/bom.json
79+
retention-days: 30

.github/workflows/grype.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ on:
1515

1616
jobs:
1717
grype:
18-
name: Grype / Container Scan
18+
name: Container Scan
1919
runs-on: ubuntu-latest
2020
steps:
2121
- name: Checkout

build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
id 'com.diffplug.spotless' version '8.4.0' apply false
33
id 'org.owasp.dependencycheck' version '12.2.0'
4+
id 'org.cyclonedx.bom' version '3.2.4'
45
id 'com.github.node-gradle.node' version '7.1.0' apply false
56
}
67

@@ -61,6 +62,10 @@ ext {
6162
// Gitleaks (bundled binary)
6263
gitleaksVersion = '8.21.2'
6364

65+
// OpenAPI spec generation
66+
swaggerCoreVersion = '2.2.48'
67+
swaggerUiVersion = '5.21.0'
68+
6469
}
6570

6671
allprojects {
@@ -80,10 +85,15 @@ allprojects {
8085
analyzers.ossIndex.url = 'https://api.guide.sonatype.com/'
8186
failBuildOnCVSS = 5
8287
suppressionFile = rootProject.file('gradle-suppressions.xml').toString()
88+
failOnError = false
8389
}
8490
}
8591

8692

93+
cyclonedxBom {
94+
projectType = 'application'
95+
}
96+
8797
tasks.register('installGitHooks') {
8898
group = 'build setup'
8999
description = 'Configures git to use the .githooks/ directory (run once after cloning).'

git-proxy-java-dashboard/build.gradle

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ tasks.named('processResources') {
9898
if (!project.hasProperty('skipFrontend')) {
9999
dependsOn tasks.named('copyFrontend')
100100
}
101+
// Inject the project version into version.properties so OpenApiController can read it at runtime.
102+
filesMatching('version.properties') {
103+
expand(appVersion: project.version)
104+
}
105+
// Inject the Swagger UI WebJar version into swagger-ui.html so the asset paths are correct.
106+
filesMatching('**/swagger-ui.html') {
107+
expand(swaggerUiVersion: swaggerUiVersion)
108+
}
101109
}
102110

103111
tasks.named('run') {
@@ -161,6 +169,12 @@ dependencies {
161169
// MongoDB driver — compile-scoped because MongoSessionRepository uses the sync client directly.
162170
implementation "org.mongodb:mongodb-driver-sync:${mongoVersion}"
163171

172+
// OpenAPI spec generation — core model + YAML/JSON serialisation only (no JAX-RS required).
173+
// The spec is built programmatically from Spring's RequestMappingHandlerMapping.
174+
implementation "io.swagger.core.v3:swagger-core-jakarta:${swaggerCoreVersion}"
175+
// Swagger UI static assets served from /webjars/** via the WebJar resource handler.
176+
runtimeOnly "org.webjars:swagger-ui:${swaggerUiVersion}"
177+
164178
// Gestalt config (needed to catch GestaltException from GitProxyConfigLoader)
165179
implementation "com.github.gestalt-config:gestalt-core:${gestaltVersion}"
166180

git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/GitProxyWithDashboardApplication.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ public void lifeCycleStopping(LifeCycle event) {
120120
server.start();
121121

122122
log.info("JGit Proxy with Dashboard started on port {}", connector.getPort());
123-
log.info(" Dashboard: http://localhost:{}/dashboard/", connector.getPort());
124-
log.info(" API: http://localhost:{}/api/push", connector.getPort());
125-
log.info(" Health: http://localhost:{}/api/health", connector.getPort());
123+
log.info(" Dashboard: http://localhost:{}/dashboard/", connector.getPort());
124+
log.info(" API: http://localhost:{}/api", connector.getPort());
125+
log.info(" Health: http://localhost:{}/api/health", connector.getPort());
126+
log.info(" Swagger UI: http://localhost:{}/swagger-ui", connector.getPort());
126127

127128
server.join();
128129
}

git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
147147
: new String[] {"/api/**", "/login", "/logout"};
148148

149149
http.securityMatcher(protectedPaths)
150-
.authorizeHttpRequests(auth -> auth.requestMatchers("/api/runtime-config", "/api/health")
150+
.authorizeHttpRequests(auth -> auth.requestMatchers(
151+
"/api",
152+
"/api/runtime-config",
153+
"/api/health",
154+
"/api/openapi.yaml",
155+
"/api/openapi.json",
156+
"/webjars/**")
151157
.permitAll()
152158
.requestMatchers("/api/users", "/api/users/**")
153159
.hasRole("ADMIN")

git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SpringWebConfig.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public void configureMessageConverters(HttpMessageConverters.ServerBuilder build
2626

2727
@Override
2828
public void addResourceHandlers(ResourceHandlerRegistry registry) {
29+
// WebJar assets (Swagger UI) served at /webjars/** with versioned paths.
30+
registry.addResourceHandler("/webjars/**")
31+
.addResourceLocations("classpath:/META-INF/resources/webjars/")
32+
.resourceChain(true);
2933
registry.addResourceHandler("/**")
3034
.addResourceLocations("classpath:/static/")
3135
.resourceChain(true);

git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/controller/AuthController.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.finos.gitproxy.dashboard.controller;
22

3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.tags.Tag;
35
import java.util.ArrayList;
46
import java.util.List;
57
import java.util.Map;
@@ -14,6 +16,7 @@
1416
import org.springframework.web.bind.annotation.RequestMapping;
1517
import org.springframework.web.bind.annotation.RestController;
1618

19+
@Tag(name = "Auth", description = "Current user profile and session information")
1720
@RestController
1821
@RequestMapping("/api")
1922
public class AuthController {
@@ -28,6 +31,7 @@ public class AuthController {
2831
* Returns the currently authenticated user's full profile: username, emails (with verified flag), SCM identities,
2932
* authorities, and repo permissions.
3033
*/
34+
@Operation(operationId = "getCurrentUser", summary = "Get the authenticated user's profile")
3135
@GetMapping("/me")
3236
public Map<String, Object> me() {
3337
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/controller/ConfigReloadController.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.finos.gitproxy.dashboard.controller;
22

3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.tags.Tag;
35
import java.util.Map;
46
import org.finos.gitproxy.jetty.reload.LiveConfigLoader;
57
import org.finos.gitproxy.jetty.reload.LiveConfigLoader.Section;
@@ -30,13 +32,15 @@
3032
*
3133
* <p>This endpoint requires {@code ROLE_ADMIN}.
3234
*/
35+
@Tag(name = "Admin", description = "Administrative operations — requires ROLE_ADMIN")
3336
@RestController
3437
@RequestMapping("/api/config")
3538
public class ConfigReloadController {
3639

3740
@Autowired
3841
private LiveConfigLoader liveConfigLoader;
3942

43+
@Operation(operationId = "reloadConfig", summary = "Trigger a live config reload")
4044
@PostMapping("/reload")
4145
public ResponseEntity<Map<String, String>> reload(
4246
@RequestParam(name = "section", defaultValue = "all") String sectionParam) {

git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/controller/ConnectivityController.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.finos.gitproxy.dashboard.controller;
22

3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.tags.Tag;
35
import jakarta.annotation.Resource;
46
import java.net.InetSocketAddress;
57
import java.net.Socket;
@@ -56,6 +58,7 @@
5658
*
5759
* <p>Requires {@code ROLE_ADMIN}.
5860
*/
61+
@Tag(name = "Admin", description = "Administrative operations — requires ROLE_ADMIN")
5962
@Slf4j
6063
@RestController
6164
public class ConnectivityController {
@@ -85,6 +88,7 @@ public class ConnectivityController {
8588
* @param repoPath optional — repo path (e.g. {@code /owner/repo.git}) appended to the provider base URI for the git
8689
* probe step; requires {@code provider}
8790
*/
91+
@Operation(operationId = "checkConnectivity", summary = "Test outbound connectivity to configured providers")
8892
@GetMapping("/api/admin/connectivity")
8993
public Map<String, Object> check(
9094
@RequestParam(name = "provider", required = false) String providerName,

0 commit comments

Comments
 (0)