diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index b6c4b3d45..23577d479 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -131,6 +131,15 @@ jobs: # jq validates every field before the row reaches matrix generation. while IFS= read -r manifest; do path="${manifest%/bazel-java-ci.json}" + for forbidden in \ + MODULE.bazel MODULE.bazel.lock maven_install.json \ + .bazelrc .bazelversion .bazel_downloader_config \ + WORKSPACE WORKSPACE.bazel bazel-enablement bazel-migration; do + if [ -e "${path}/${forbidden}" ]; then + echo "::error file=${path}/${forbidden}::Java components use root Bazel state; remove imported standalone file" + exit 1 + fi + done id="$(jq -er '.id | select(test("^[a-z0-9][a-z0-9-]*$"))' "$manifest")" tests_skip="$(jq -er '.tests_skip | if type == "boolean" then tostring else error("tests_skip must be boolean") end' "$manifest")" component_kind="$(jq -er '.component_kind | select(. == "java-framework" or . == "java-service")' "$manifest")" diff --git a/BAZEL.md b/BAZEL.md index ccd108a58..74daee1d1 100644 --- a/BAZEL.md +++ b/BAZEL.md @@ -14,6 +14,8 @@ Bazel currently builds, tests, and packages: - `src/libraries/java/nv-boot-parent` (Java framework libraries and tests) - `src/control-plane-services/cloud-tasks` (Java libraries, tests, and Spring Boot application) +- `src/control-plane-services/notary` (Java libraries, tests, and Spring Boot + application) Other upstream-owned subtrees remain excluded until they are onboarded one at a time. `nv-boot-parent` and onboarded Java service directories are folded diff --git a/BUILD.bazel b/BUILD.bazel index 1cd9bf97b..85c8cd72e 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -20,19 +20,23 @@ exports_files( visibility = ["//src/libraries/go/lib:__pkg__"], ) -# Root module identity + third-party lock + NOTICE index, consumed by the -# Java subtrees' NOTICE generation/verification tooling now that their Maven -# closure is owned by the single root module. +package_group( + name = "java_notice_consumers", + packages = [ + "//src/control-plane-services/...", + "//src/libraries/java/...", + ], +) + +# Root module identity, third-party lock, and NOTICE index consumed by Java +# NOTICE generation and verification under the stable Java component paths. exports_files( [ "MODULE.bazel", "NOTICE", "maven_install.json", ], - visibility = [ - "//src/control-plane-services/cloud-tasks:__subpackages__", - "//src/libraries/java/nv-boot-parent:__subpackages__", - ], + visibility = [":java_notice_consumers"], ) # Gazelle manages only the native Go subtrees listed in go.work.bazel. diff --git a/MODULE.bazel b/MODULE.bazel index ba2b262ee..f652880fb 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -264,6 +264,8 @@ maven.install( "com.github.ben-manes.caffeine:guava", "com.github.java-json-tools:json-patch:1.13", "commons-codec:commons-codec", + # nv-boot-parent overrides the Spring Boot BOM's lower version. + "commons-logging:commons-logging:1.4.0", "io.cloudevents:cloudevents-core:4.1.1", "io.cloudevents:cloudevents-json-jackson:4.1.1", "io.micrometer:micrometer-tracing-bridge-otel", @@ -272,6 +274,7 @@ maven.install( "io.opentelemetry:opentelemetry-sdk-testing", "jakarta.servlet:jakarta.servlet-api", "org.apache.cassandra:java-driver-metrics-micrometer", + "org.apache.commons:commons-collections4:4.5.0", "org.apache.commons:commons-lang3:3.20.0", "org.bouncycastle:bcprov-jdk18on:1.84", "org.jacoco:org.jacoco.agent:jar:runtime:0.8.14", @@ -284,6 +287,8 @@ maven.install( "org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.3", "org.springframework.boot:spring-boot-micrometer-tracing-opentelemetry", "org.springframework.boot:spring-boot-restclient", + # Spring Boot's Maven repackage goal injects this into executable jars. + "org.springframework.boot:spring-boot-jarmode-tools", "org.springframework.boot:spring-boot-starter", "org.springframework.boot:spring-boot-starter-actuator", "org.springframework.boot:spring-boot-starter-actuator-test", @@ -361,6 +366,10 @@ maven.install( "org.springframework.cloud:spring-cloud-dependencies:2025.1.2", "org.testcontainers:testcontainers-bom:2.0.5", ], + # Spring Framework 7 embeds the AOP Alliance API in spring-aop. Maven's + # executable jar therefore omits the legacy standalone jar; excluding it + # here avoids duplicate org.aopalliance classes in Bazel applications. + excluded_artifacts = ["aopalliance:aopalliance"], fail_on_missing_checksum = True, fetch_sources = True, known_contributing_modules = [ diff --git a/NOTICE b/NOTICE index d48fa2ce4..d574b1d7d 100644 --- a/NOTICE +++ b/NOTICE @@ -307,6 +307,7 @@ The following third-party licenses are included in this repository: src/compute-plane-services/nvca/vendor/sigs.k8s.io/yaml/LICENSE src/compute-plane-services/nvsnap/NOTICE src/control-plane-services/cloud-tasks/NOTICE + src/control-plane-services/notary/NOTICE src/invocation-plane-services/llm-api-gateway/NOTICE src/invocation-plane-services/ratelimiter/NOTICE src/libraries/go/lib/vendor/cloud.google.com/go/compute/metadata/LICENSE diff --git a/maven_install.json b/maven_install.json index ebfae1f7c..a0e960fb1 100755 --- a/maven_install.json +++ b/maven_install.json @@ -1,6 +1,7 @@ { "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL", "__INPUT_ARTIFACTS_HASH": { + "aopalliance:aopalliance": 85128824, "at.yawk.lz4:lz4-java": 1725015399, "com.bucket4j:bucket4j_jdk17-core": -1409600715, "com.github.ben-manes.caffeine:caffeine": 1100418982, @@ -9,6 +10,7 @@ "com.google.protobuf:protobuf-java": 1906581597, "com.google.protobuf:protobuf-java-util": -1626074784, "commons-codec:commons-codec": -1269462382, + "commons-logging:commons-logging": -916690979, "io.cloudevents:cloudevents-core": -2103567774, "io.cloudevents:cloudevents-json-jackson": -1197487309, "io.grpc:grpc-api": 811923177, @@ -28,6 +30,7 @@ "net.javacrumbs.shedlock:shedlock-provider-cassandra": -1200872, "net.javacrumbs.shedlock:shedlock-spring": -326290089, "org.apache.cassandra:java-driver-metrics-micrometer": -465267397, + "org.apache.commons:commons-collections4": 1132363499, "org.apache.commons:commons-lang3": -278168457, "org.assertj:assertj-core": 1863574844, "org.awaitility:awaitility": -1630939750, @@ -52,6 +55,7 @@ "org.springdoc:springdoc-openapi-starter-webmvc-api": 655186513, "org.springframework.boot:spring-boot-actuator": 763411999, "org.springframework.boot:spring-boot-dependencies": 70164638, + "org.springframework.boot:spring-boot-jarmode-tools": 1339984806, "org.springframework.boot:spring-boot-loader": 1745496505, "org.springframework.boot:spring-boot-micrometer-metrics": -782059807, "org.springframework.boot:spring-boot-micrometer-tracing": -2099172128, @@ -97,8 +101,6 @@ "tools.jackson.module:jackson-module-blackbird": -423541381 }, "__RESOLVED_ARTIFACTS_HASH": { - "aopalliance:aopalliance": -1763688673, - "aopalliance:aopalliance:jar:sources": 758113234, "args4j:args4j": -572028113, "args4j:args4j:jar:sources": 74047526, "at.yawk.lz4:lz4-java": -1985362494, @@ -207,8 +209,8 @@ "commons-codec:commons-codec:jar:sources": 990790236, "commons-io:commons-io": -1021273518, "commons-io:commons-io:jar:sources": 2066085027, - "commons-logging:commons-logging": 1061992981, - "commons-logging:commons-logging:jar:sources": 1867783947, + "commons-logging:commons-logging": 1066868550, + "commons-logging:commons-logging:jar:sources": 433125849, "io.cloudevents:cloudevents-api": -617548735, "io.cloudevents:cloudevents-api:jar:sources": 924762007, "io.cloudevents:cloudevents-core": -688560325, @@ -263,8 +265,8 @@ "io.micrometer:micrometer-observation:jar:sources": 1279306785, "io.micrometer:micrometer-registry-prometheus": -104964751, "io.micrometer:micrometer-registry-prometheus:jar:sources": 769452823, - "io.micrometer:micrometer-tracing": -109714346, - "io.micrometer:micrometer-tracing-bridge-otel": 975863312, + "io.micrometer:micrometer-tracing": -1023327334, + "io.micrometer:micrometer-tracing-bridge-otel": 1199970811, "io.micrometer:micrometer-tracing-bridge-otel:jar:sources": 1300289348, "io.micrometer:micrometer-tracing:jar:sources": 266031561, "io.nats:jnats": 1273546329, @@ -393,9 +395,9 @@ "net.bytebuddy:byte-buddy-agent": -1380713096, "net.bytebuddy:byte-buddy-agent:jar:sources": 564051985, "net.bytebuddy:byte-buddy:jar:sources": -1360611642, - "net.devh:grpc-common-spring-boot": 868090547, + "net.devh:grpc-common-spring-boot": -312272718, "net.devh:grpc-common-spring-boot:jar:sources": 1655513026, - "net.devh:grpc-server-spring-boot-starter": 1078456931, + "net.devh:grpc-server-spring-boot-starter": 267656663, "net.devh:grpc-server-spring-boot-starter:jar:sources": 1954333860, "net.java.dev.jna:jna": -1951542637, "net.java.dev.jna:jna:jar:sources": -545183654, @@ -403,7 +405,7 @@ "net.javacrumbs.shedlock:shedlock-core:jar:sources": 69227555, "net.javacrumbs.shedlock:shedlock-provider-cassandra": -460637299, "net.javacrumbs.shedlock:shedlock-provider-cassandra:jar:sources": 1860167, - "net.javacrumbs.shedlock:shedlock-spring": 2008414309, + "net.javacrumbs.shedlock:shedlock-spring": 337705847, "net.javacrumbs.shedlock:shedlock-spring:jar:sources": 875349683, "net.minidev:accessors-smart": -325667575, "net.minidev:accessors-smart:jar:sources": -124254155, @@ -531,207 +533,209 @@ "org.slf4j:jul-to-slf4j:jar:sources": -662175280, "org.slf4j:slf4j-api": -1249720338, "org.slf4j:slf4j-api:jar:sources": -297247278, - "org.springdoc:springdoc-openapi-starter-common": -50810541, + "org.springdoc:springdoc-openapi-starter-common": 130340199, "org.springdoc:springdoc-openapi-starter-common:jar:sources": 776689800, - "org.springdoc:springdoc-openapi-starter-webflux-api": -2128144311, + "org.springdoc:springdoc-openapi-starter-webflux-api": 2033495665, "org.springdoc:springdoc-openapi-starter-webflux-api:jar:sources": -1775632648, - "org.springdoc:springdoc-openapi-starter-webmvc-api": 479427887, + "org.springdoc:springdoc-openapi-starter-webmvc-api": -1330546544, "org.springdoc:springdoc-openapi-starter-webmvc-api:jar:sources": -1201450538, - "org.springframework.boot:spring-boot": -1519545366, - "org.springframework.boot:spring-boot-actuator": 1868004981, - "org.springframework.boot:spring-boot-actuator-autoconfigure": 437017470, + "org.springframework.boot:spring-boot": -217042451, + "org.springframework.boot:spring-boot-actuator": 391557646, + "org.springframework.boot:spring-boot-actuator-autoconfigure": 575390172, "org.springframework.boot:spring-boot-actuator-autoconfigure:jar:sources": 1090885133, "org.springframework.boot:spring-boot-actuator:jar:sources": 1545596535, - "org.springframework.boot:spring-boot-autoconfigure": -491644458, + "org.springframework.boot:spring-boot-autoconfigure": -931486067, "org.springframework.boot:spring-boot-autoconfigure:jar:sources": 562322138, - "org.springframework.boot:spring-boot-cassandra": 1880514148, + "org.springframework.boot:spring-boot-cassandra": -162228161, "org.springframework.boot:spring-boot-cassandra:jar:sources": -285158244, - "org.springframework.boot:spring-boot-data-cassandra": -1136414714, - "org.springframework.boot:spring-boot-data-cassandra-test": 1687171774, + "org.springframework.boot:spring-boot-data-cassandra": -300793208, + "org.springframework.boot:spring-boot-data-cassandra-test": 182994733, "org.springframework.boot:spring-boot-data-cassandra-test:jar:sources": -601584645, "org.springframework.boot:spring-boot-data-cassandra:jar:sources": 73983427, - "org.springframework.boot:spring-boot-data-commons": 1096447250, + "org.springframework.boot:spring-boot-data-commons": 2146784261, "org.springframework.boot:spring-boot-data-commons:jar:sources": 867332098, - "org.springframework.boot:spring-boot-health": 988376788, + "org.springframework.boot:spring-boot-health": 363182543, "org.springframework.boot:spring-boot-health:jar:sources": 1156692002, - "org.springframework.boot:spring-boot-http-client": -294534102, + "org.springframework.boot:spring-boot-http-client": 1376712547, "org.springframework.boot:spring-boot-http-client:jar:sources": 881974750, - "org.springframework.boot:spring-boot-http-codec": 434302862, + "org.springframework.boot:spring-boot-http-codec": -178860782, "org.springframework.boot:spring-boot-http-codec:jar:sources": 585983495, - "org.springframework.boot:spring-boot-http-converter": -1456188332, + "org.springframework.boot:spring-boot-http-converter": -2069351976, "org.springframework.boot:spring-boot-http-converter:jar:sources": 678862764, - "org.springframework.boot:spring-boot-jackson": -886310726, + "org.springframework.boot:spring-boot-jackson": -1601807469, "org.springframework.boot:spring-boot-jackson:jar:sources": -928970153, + "org.springframework.boot:spring-boot-jarmode-tools": -1589017826, + "org.springframework.boot:spring-boot-jarmode-tools:jar:sources": -1540901734, "org.springframework.boot:spring-boot-loader": 1516185416, "org.springframework.boot:spring-boot-loader:jar:sources": 499181734, - "org.springframework.boot:spring-boot-micrometer-metrics": -971815116, - "org.springframework.boot:spring-boot-micrometer-metrics-test": 1059861465, + "org.springframework.boot:spring-boot-micrometer-metrics": -1491451355, + "org.springframework.boot:spring-boot-micrometer-metrics-test": 1306882889, "org.springframework.boot:spring-boot-micrometer-metrics-test:jar:sources": 539702709, "org.springframework.boot:spring-boot-micrometer-metrics:jar:sources": -1326033951, - "org.springframework.boot:spring-boot-micrometer-observation": -515345138, + "org.springframework.boot:spring-boot-micrometer-observation": 1284506709, "org.springframework.boot:spring-boot-micrometer-observation:jar:sources": -1374159176, - "org.springframework.boot:spring-boot-micrometer-tracing": -1769177987, - "org.springframework.boot:spring-boot-micrometer-tracing-opentelemetry": 686601187, + "org.springframework.boot:spring-boot-micrometer-tracing": -437563721, + "org.springframework.boot:spring-boot-micrometer-tracing-opentelemetry": -1134218580, "org.springframework.boot:spring-boot-micrometer-tracing-opentelemetry:jar:sources": 2058843242, "org.springframework.boot:spring-boot-micrometer-tracing:jar:sources": -531678236, - "org.springframework.boot:spring-boot-netty": -1868279944, + "org.springframework.boot:spring-boot-netty": 1517958315, "org.springframework.boot:spring-boot-netty:jar:sources": 2079852054, - "org.springframework.boot:spring-boot-opentelemetry": -1423093456, + "org.springframework.boot:spring-boot-opentelemetry": -1100080141, "org.springframework.boot:spring-boot-opentelemetry:jar:sources": 1840051882, - "org.springframework.boot:spring-boot-persistence": -1485790991, + "org.springframework.boot:spring-boot-persistence": -109390311, "org.springframework.boot:spring-boot-persistence:jar:sources": -1027448649, - "org.springframework.boot:spring-boot-reactor": 1298372962, - "org.springframework.boot:spring-boot-reactor-netty": 1434335970, + "org.springframework.boot:spring-boot-reactor": 1897203073, + "org.springframework.boot:spring-boot-reactor-netty": -1658018437, "org.springframework.boot:spring-boot-reactor-netty:jar:sources": -732886702, "org.springframework.boot:spring-boot-reactor:jar:sources": -1373689055, - "org.springframework.boot:spring-boot-restclient": -67067992, + "org.springframework.boot:spring-boot-restclient": -804354864, "org.springframework.boot:spring-boot-restclient:jar:sources": -1554622109, - "org.springframework.boot:spring-boot-resttestclient": 1078416021, + "org.springframework.boot:spring-boot-resttestclient": 427136130, "org.springframework.boot:spring-boot-resttestclient:jar:sources": 1128597445, - "org.springframework.boot:spring-boot-security": -528218864, - "org.springframework.boot:spring-boot-security-oauth2-client": 1972725629, + "org.springframework.boot:spring-boot-security": 1577209621, + "org.springframework.boot:spring-boot-security-oauth2-client": 1868854830, "org.springframework.boot:spring-boot-security-oauth2-client:jar:sources": -1211778370, - "org.springframework.boot:spring-boot-security-oauth2-resource-server": -1021125134, + "org.springframework.boot:spring-boot-security-oauth2-resource-server": -21026393, "org.springframework.boot:spring-boot-security-oauth2-resource-server:jar:sources": 1301519418, - "org.springframework.boot:spring-boot-security-test": -1810341741, + "org.springframework.boot:spring-boot-security-test": 1233999834, "org.springframework.boot:spring-boot-security-test:jar:sources": -311791400, "org.springframework.boot:spring-boot-security:jar:sources": -1644906067, - "org.springframework.boot:spring-boot-servlet": -1040246830, + "org.springframework.boot:spring-boot-servlet": -1653410474, "org.springframework.boot:spring-boot-servlet:jar:sources": 1162002660, - "org.springframework.boot:spring-boot-starter": 191814674, - "org.springframework.boot:spring-boot-starter-actuator": 1761935967, - "org.springframework.boot:spring-boot-starter-actuator-test": 910069034, + "org.springframework.boot:spring-boot-starter": 1873824649, + "org.springframework.boot:spring-boot-starter-actuator": -1855864019, + "org.springframework.boot:spring-boot-starter-actuator-test": 1364042115, "org.springframework.boot:spring-boot-starter-actuator-test:jar:sources": 314973026, "org.springframework.boot:spring-boot-starter-actuator:jar:sources": -2079433447, - "org.springframework.boot:spring-boot-starter-aspectj": 740892542, + "org.springframework.boot:spring-boot-starter-aspectj": 2117230935, "org.springframework.boot:spring-boot-starter-aspectj:jar:sources": 1957689077, - "org.springframework.boot:spring-boot-starter-data-cassandra": -1305411192, - "org.springframework.boot:spring-boot-starter-data-cassandra-test": 1049224325, + "org.springframework.boot:spring-boot-starter-data-cassandra": -756828381, + "org.springframework.boot:spring-boot-starter-data-cassandra-test": -1188943434, "org.springframework.boot:spring-boot-starter-data-cassandra-test:jar:sources": 464889083, "org.springframework.boot:spring-boot-starter-data-cassandra:jar:sources": 62860247, - "org.springframework.boot:spring-boot-starter-jackson": 211326145, - "org.springframework.boot:spring-boot-starter-jackson-test": 538761659, + "org.springframework.boot:spring-boot-starter-jackson": -789286705, + "org.springframework.boot:spring-boot-starter-jackson-test": -1259015604, "org.springframework.boot:spring-boot-starter-jackson-test:jar:sources": 1211585483, "org.springframework.boot:spring-boot-starter-jackson:jar:sources": 1689728463, "org.springframework.boot:spring-boot-starter-logging": 51215582, "org.springframework.boot:spring-boot-starter-logging:jar:sources": 1271111015, - "org.springframework.boot:spring-boot-starter-micrometer-metrics": 546085162, - "org.springframework.boot:spring-boot-starter-micrometer-metrics-test": -835747019, + "org.springframework.boot:spring-boot-starter-micrometer-metrics": -103216218, + "org.springframework.boot:spring-boot-starter-micrometer-metrics-test": 1823423190, "org.springframework.boot:spring-boot-starter-micrometer-metrics-test:jar:sources": 1830581000, "org.springframework.boot:spring-boot-starter-micrometer-metrics:jar:sources": -1130818080, - "org.springframework.boot:spring-boot-starter-reactor-netty": 812139334, + "org.springframework.boot:spring-boot-starter-reactor-netty": 1009047865, "org.springframework.boot:spring-boot-starter-reactor-netty:jar:sources": 1164956430, - "org.springframework.boot:spring-boot-starter-security": 1058973423, - "org.springframework.boot:spring-boot-starter-security-oauth2-client": -1355057519, + "org.springframework.boot:spring-boot-starter-security": -1695625735, + "org.springframework.boot:spring-boot-starter-security-oauth2-client": 623595396, "org.springframework.boot:spring-boot-starter-security-oauth2-client:jar:sources": 1707609356, - "org.springframework.boot:spring-boot-starter-security-oauth2-resource-server": -265511875, - "org.springframework.boot:spring-boot-starter-security-oauth2-resource-server-test": -17384836, + "org.springframework.boot:spring-boot-starter-security-oauth2-resource-server": 1168522450, + "org.springframework.boot:spring-boot-starter-security-oauth2-resource-server-test": -1711644160, "org.springframework.boot:spring-boot-starter-security-oauth2-resource-server-test:jar:sources": -746940353, "org.springframework.boot:spring-boot-starter-security-oauth2-resource-server:jar:sources": 2136275088, - "org.springframework.boot:spring-boot-starter-security-test": 1690721902, + "org.springframework.boot:spring-boot-starter-security-test": -1654018173, "org.springframework.boot:spring-boot-starter-security-test:jar:sources": 468207576, "org.springframework.boot:spring-boot-starter-security:jar:sources": -1720617031, - "org.springframework.boot:spring-boot-starter-test": -1087542039, + "org.springframework.boot:spring-boot-starter-test": 1844551537, "org.springframework.boot:spring-boot-starter-test:jar:sources": -480189945, - "org.springframework.boot:spring-boot-starter-tomcat": -521361670, - "org.springframework.boot:spring-boot-starter-tomcat-runtime": -1912812722, + "org.springframework.boot:spring-boot-starter-tomcat": 968892076, + "org.springframework.boot:spring-boot-starter-tomcat-runtime": 1789613861, "org.springframework.boot:spring-boot-starter-tomcat-runtime:jar:sources": 1011838045, "org.springframework.boot:spring-boot-starter-tomcat:jar:sources": 1319064174, - "org.springframework.boot:spring-boot-starter-validation": -321633530, + "org.springframework.boot:spring-boot-starter-validation": -96834434, "org.springframework.boot:spring-boot-starter-validation:jar:sources": -1002865328, - "org.springframework.boot:spring-boot-starter-web": 1122240419, + "org.springframework.boot:spring-boot-starter-web": 1283006341, "org.springframework.boot:spring-boot-starter-web:jar:sources": -768709610, - "org.springframework.boot:spring-boot-starter-webflux": -818164168, - "org.springframework.boot:spring-boot-starter-webflux-test": -415179163, + "org.springframework.boot:spring-boot-starter-webflux": 484244077, + "org.springframework.boot:spring-boot-starter-webflux-test": 248412203, "org.springframework.boot:spring-boot-starter-webflux-test:jar:sources": -1654084055, "org.springframework.boot:spring-boot-starter-webflux:jar:sources": 1008425882, - "org.springframework.boot:spring-boot-starter-webmvc": 170962824, - "org.springframework.boot:spring-boot-starter-webmvc-test": -1343848488, + "org.springframework.boot:spring-boot-starter-webmvc": -1517079847, + "org.springframework.boot:spring-boot-starter-webmvc-test": -33388766, "org.springframework.boot:spring-boot-starter-webmvc-test:jar:sources": -1751111514, "org.springframework.boot:spring-boot-starter-webmvc:jar:sources": -2120956724, "org.springframework.boot:spring-boot-starter:jar:sources": 1138639700, - "org.springframework.boot:spring-boot-test": -927444958, - "org.springframework.boot:spring-boot-test-autoconfigure": -669950838, + "org.springframework.boot:spring-boot-test": -1416360895, + "org.springframework.boot:spring-boot-test-autoconfigure": 2011168683, "org.springframework.boot:spring-boot-test-autoconfigure:jar:sources": 392165449, "org.springframework.boot:spring-boot-test:jar:sources": -1764708648, - "org.springframework.boot:spring-boot-tomcat": -1113349252, + "org.springframework.boot:spring-boot-tomcat": 338031270, "org.springframework.boot:spring-boot-tomcat:jar:sources": -1529683395, - "org.springframework.boot:spring-boot-validation": 1270368638, + "org.springframework.boot:spring-boot-validation": -1013194267, "org.springframework.boot:spring-boot-validation:jar:sources": -1684576067, - "org.springframework.boot:spring-boot-web-server": 285708094, + "org.springframework.boot:spring-boot-web-server": -327455550, "org.springframework.boot:spring-boot-web-server:jar:sources": 990724443, - "org.springframework.boot:spring-boot-webclient": -507647, + "org.springframework.boot:spring-boot-webclient": -1160148098, "org.springframework.boot:spring-boot-webclient:jar:sources": 71538120, - "org.springframework.boot:spring-boot-webflux": -231291542, - "org.springframework.boot:spring-boot-webflux-test": 1600114323, + "org.springframework.boot:spring-boot-webflux": 207589723, + "org.springframework.boot:spring-boot-webflux-test": -1816712577, "org.springframework.boot:spring-boot-webflux-test:jar:sources": 1452173744, "org.springframework.boot:spring-boot-webflux:jar:sources": -1118813321, - "org.springframework.boot:spring-boot-webmvc": -2104103221, - "org.springframework.boot:spring-boot-webmvc-test": 749235095, + "org.springframework.boot:spring-boot-webmvc": -1115468192, + "org.springframework.boot:spring-boot-webmvc-test": 1256774995, "org.springframework.boot:spring-boot-webmvc-test:jar:sources": 444837917, "org.springframework.boot:spring-boot-webmvc:jar:sources": -1895028417, - "org.springframework.boot:spring-boot-webtestclient": -705991472, + "org.springframework.boot:spring-boot-webtestclient": 1146946499, "org.springframework.boot:spring-boot-webtestclient:jar:sources": -542316343, "org.springframework.boot:spring-boot:jar:sources": 580880564, "org.springframework.cloud:spring-cloud-commons": -788453967, "org.springframework.cloud:spring-cloud-commons:jar:sources": 1320616004, "org.springframework.cloud:spring-cloud-context": 1118197933, "org.springframework.cloud:spring-cloud-context:jar:sources": -1413797918, - "org.springframework.cloud:spring-cloud-kubernetes-client-autoconfig": -256822388, + "org.springframework.cloud:spring-cloud-kubernetes-client-autoconfig": 1728280096, "org.springframework.cloud:spring-cloud-kubernetes-client-autoconfig:jar:sources": 758022911, - "org.springframework.cloud:spring-cloud-kubernetes-client-config": 954066893, + "org.springframework.cloud:spring-cloud-kubernetes-client-config": 1376925809, "org.springframework.cloud:spring-cloud-kubernetes-client-config:jar:sources": -251471561, - "org.springframework.cloud:spring-cloud-kubernetes-commons": -500725348, + "org.springframework.cloud:spring-cloud-kubernetes-commons": 376611909, "org.springframework.cloud:spring-cloud-kubernetes-commons:jar:sources": -325491963, - "org.springframework.cloud:spring-cloud-starter": -1812063683, - "org.springframework.cloud:spring-cloud-starter-bootstrap": -1263482746, + "org.springframework.cloud:spring-cloud-starter": 2107316504, + "org.springframework.cloud:spring-cloud-starter-bootstrap": 1445574042, "org.springframework.cloud:spring-cloud-starter-bootstrap:jar:sources": 1965832056, - "org.springframework.cloud:spring-cloud-starter-kubernetes-client-config": -251169345, - "org.springframework.data:spring-data-cassandra": -1548120966, + "org.springframework.cloud:spring-cloud-starter-kubernetes-client-config": 2112043873, + "org.springframework.data:spring-data-cassandra": -953080749, "org.springframework.data:spring-data-cassandra:jar:sources": -1628107854, - "org.springframework.data:spring-data-commons": 1312199320, + "org.springframework.data:spring-data-commons": 60312922, "org.springframework.data:spring-data-commons:jar:sources": 544548950, "org.springframework.retry:spring-retry": -2038056656, "org.springframework.retry:spring-retry:jar:sources": -870799349, - "org.springframework.security:spring-security-config": 2001744589, + "org.springframework.security:spring-security-config": 1023215570, "org.springframework.security:spring-security-config:jar:sources": -756968726, - "org.springframework.security:spring-security-core": -1251894794, + "org.springframework.security:spring-security-core": 462735526, "org.springframework.security:spring-security-core:jar:sources": 1285888871, "org.springframework.security:spring-security-crypto": 424824057, "org.springframework.security:spring-security-crypto:jar:sources": 1862561396, - "org.springframework.security:spring-security-oauth2-client": -1975050683, + "org.springframework.security:spring-security-oauth2-client": -288490529, "org.springframework.security:spring-security-oauth2-client:jar:sources": 1833974239, - "org.springframework.security:spring-security-oauth2-core": -1658804453, + "org.springframework.security:spring-security-oauth2-core": 318009206, "org.springframework.security:spring-security-oauth2-core:jar:sources": -948845785, - "org.springframework.security:spring-security-oauth2-jose": 502613895, + "org.springframework.security:spring-security-oauth2-jose": -404931323, "org.springframework.security:spring-security-oauth2-jose:jar:sources": 17294361, - "org.springframework.security:spring-security-oauth2-resource-server": -1894007153, + "org.springframework.security:spring-security-oauth2-resource-server": -21703339, "org.springframework.security:spring-security-oauth2-resource-server:jar:sources": -1508178760, - "org.springframework.security:spring-security-test": 1672796517, + "org.springframework.security:spring-security-test": 780149405, "org.springframework.security:spring-security-test:jar:sources": -1326480212, - "org.springframework.security:spring-security-web": -1696304083, + "org.springframework.security:spring-security-web": -1366655297, "org.springframework.security:spring-security-web:jar:sources": 1443816202, - "org.springframework:spring-aop": -819786825, + "org.springframework:spring-aop": -424102821, "org.springframework:spring-aop:jar:sources": 1924976574, - "org.springframework:spring-beans": -698130853, + "org.springframework:spring-beans": -742759990, "org.springframework:spring-beans:jar:sources": -2147408778, - "org.springframework:spring-context": -846077202, - "org.springframework:spring-context-support": 427709038, + "org.springframework:spring-context": 1892659904, + "org.springframework:spring-context-support": -605972202, "org.springframework:spring-context-support:jar:sources": -410245914, "org.springframework:spring-context:jar:sources": -1263444176, - "org.springframework:spring-core": -253727183, + "org.springframework:spring-core": 211700524, "org.springframework:spring-core:jar:sources": -173347576, - "org.springframework:spring-expression": 1724609785, + "org.springframework:spring-expression": 1690697580, "org.springframework:spring-expression:jar:sources": 1135225455, - "org.springframework:spring-test": -279979944, + "org.springframework:spring-test": -729271251, "org.springframework:spring-test:jar:sources": -6017836, - "org.springframework:spring-tx": 1899606770, + "org.springframework:spring-tx": -1444759104, "org.springframework:spring-tx:jar:sources": -1007028336, - "org.springframework:spring-web": 2084009704, + "org.springframework:spring-web": -1161715958, "org.springframework:spring-web:jar:sources": 1608360284, - "org.springframework:spring-webflux": 1763806581, + "org.springframework:spring-webflux": 2131701578, "org.springframework:spring-webflux:jar:sources": -1419709374, - "org.springframework:spring-webmvc": 56813816, + "org.springframework:spring-webmvc": -269358301, "org.springframework:spring-webmvc:jar:sources": -837106767, "org.testcontainers:testcontainers": 450183679, "org.testcontainers:testcontainers-cassandra": -1886187917, @@ -791,13 +795,6 @@ "tools.jackson.module:jackson-module-blackbird:jar:sources": -9825245 }, "artifacts": { - "aopalliance:aopalliance": { - "shasums": { - "jar": "0addec670fedcd3f113c5c8091d783280d23f75e3acb841b61a9cdb079376a08", - "sources": "e6ef91d439ada9045f419c77543ebe0416c3cdfc5b063448343417a3e4a72123" - }, - "version": "1.0" - }, "args4j:args4j": { "shasums": { "jar": "11b029a602e787e2bc08eb3b77eda1a4f5e8b263d22e3c5d6220cd5c51f30b18", @@ -1178,10 +1175,10 @@ }, "commons-logging:commons-logging": { "shasums": { - "jar": "f8ead8943401081dea0aa824b5b1ba40a0e4ed297a572a0f02258150a0b62357", - "sources": "6e821e03cfc64e509cc162d428af84697b6c4188eb14c0b137c75b69649976ef" + "jar": "d175dbd751dd782a63bde28c7a039520e971f25e84b79c19b8435edc3603e0dc", + "sources": "28104e5057a11c6e9cda72e973d767b3a0185e65363fede91bc848bc6a309634" }, - "version": "1.3.6" + "version": "1.4.0" }, "io.cloudevents:cloudevents-api": { "shasums": { @@ -2412,6 +2409,13 @@ }, "version": "4.0.7" }, + "org.springframework.boot:spring-boot-jarmode-tools": { + "shasums": { + "jar": "8ceace00d6e6d3193d1fe11347d00e2dc1585901640a37a328ab7fc96a62a435", + "sources": "074acc471ec2c4e496cdd64cfce965086caa5b66cfa174053e73d22537cb0d32" + }, + "version": "4.0.7" + }, "org.springframework.boot:spring-boot-loader": { "shasums": { "jar": "bb1cd5fee23e03eec3fb5fdaff59c56d1db3829afe5f59381904764df76fb1a8", @@ -3509,7 +3513,6 @@ "org.jspecify:jspecify" ], "io.micrometer:micrometer-tracing": [ - "aopalliance:aopalliance", "io.micrometer:context-propagation", "io.micrometer:micrometer-observation", "org.jspecify:jspecify" @@ -4037,6 +4040,10 @@ "org.springframework.boot:spring-boot", "tools.jackson.core:jackson-databind" ], + "org.springframework.boot:spring-boot-jarmode-tools": [ + "org.springframework.boot:spring-boot-loader", + "org.springframework:spring-core" + ], "org.springframework.boot:spring-boot-micrometer-metrics": [ "io.micrometer:micrometer-core", "org.springframework.boot:spring-boot", @@ -4617,10 +4624,6 @@ ] }, "packages": { - "aopalliance:aopalliance": [ - "org.aopalliance.aop", - "org.aopalliance.intercept" - ], "args4j:args4j": [ "org.kohsuke.args4j", "org.kohsuke.args4j.spi" @@ -5280,7 +5283,8 @@ ], "commons-logging:commons-logging": [ "org.apache.commons.logging", - "org.apache.commons.logging.impl" + "org.apache.commons.logging.impl", + "org.apache.commons.logging.jakarta" ], "io.cloudevents:cloudevents-api": [ "io.cloudevents", @@ -7768,6 +7772,9 @@ "org.springframework.boot.jackson", "org.springframework.boot.jackson.autoconfigure" ], + "org.springframework.boot:spring-boot-jarmode-tools": [ + "org.springframework.boot.jarmode.tools" + ], "org.springframework.boot:spring-boot-loader": [ "org.springframework.boot.loader.jar", "org.springframework.boot.loader.jarmode", @@ -9889,8 +9896,6 @@ }, "repositories": { "https://maven-central.storage-download.googleapis.com/maven2/": [ - "aopalliance:aopalliance", - "aopalliance:aopalliance:jar:sources", "args4j:args4j", "args4j:args4j:jar:sources", "at.yawk.lz4:lz4-java", @@ -10354,6 +10359,8 @@ "org.springframework.boot:spring-boot-http-converter:jar:sources", "org.springframework.boot:spring-boot-jackson", "org.springframework.boot:spring-boot-jackson:jar:sources", + "org.springframework.boot:spring-boot-jarmode-tools", + "org.springframework.boot:spring-boot-jarmode-tools:jar:sources", "org.springframework.boot:spring-boot-loader", "org.springframework.boot:spring-boot-loader:jar:sources", "org.springframework.boot:spring-boot-micrometer-metrics", @@ -10583,8 +10590,6 @@ "tools.jackson.module:jackson-module-blackbird:jar:sources" ], "https://repo.maven.apache.org/maven2/": [ - "aopalliance:aopalliance", - "aopalliance:aopalliance:jar:sources", "args4j:args4j", "args4j:args4j:jar:sources", "at.yawk.lz4:lz4-java", @@ -11048,6 +11053,8 @@ "org.springframework.boot:spring-boot-http-converter:jar:sources", "org.springframework.boot:spring-boot-jackson", "org.springframework.boot:spring-boot-jackson:jar:sources", + "org.springframework.boot:spring-boot-jarmode-tools", + "org.springframework.boot:spring-boot-jarmode-tools:jar:sources", "org.springframework.boot:spring-boot-loader", "org.springframework.boot:spring-boot-loader:jar:sources", "org.springframework.boot:spring-boot-micrometer-metrics", diff --git a/src/control-plane-services/cloud-tasks/NOTICE b/src/control-plane-services/cloud-tasks/NOTICE index f667592de..b34a0ad14 100644 --- a/src/control-plane-services/cloud-tasks/NOTICE +++ b/src/control-plane-services/cloud-tasks/NOTICE @@ -1,6 +1,5 @@ -Lists of 258 third-party dependencies. - (Public Domain) AOP alliance (aopalliance:aopalliance:1.0 - http://aopalliance.sourceforge.net) +Lists of 263 third-party dependencies. (Apache License, Version 2.0) LZ4 Java Compression (at.yawk.lz4:lz4-java:1.10.3 - https://github.com/yawkat/lz4-java) (EPL-2.0) (LGPL-2.1-only) Logback Classic Module (ch.qos.logback:logback-classic:1.5.34 - http://logback.qos.ch) (EPL-2.0) (LGPL-2.1-only) Logback Core Module (ch.qos.logback:logback-core:1.5.34 - http://logback.qos.ch) @@ -42,12 +41,13 @@ Lists of 258 third-party dependencies. (Apache License, version 2.0) OAuth 2.0 SDK with OpenID Connect extensions (com.nimbusds:oauth2-oidc-sdk:11.26.1 - https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions) (The Apache Software License, Version 2.0) okhttp-logging-interceptor (com.squareup.okhttp3:logging-interceptor:4.12.0 - https://square.github.io/okhttp/) (The Apache Software License, Version 2.0) okhttp (com.squareup.okhttp3:okhttp:4.12.0 - https://square.github.io/okhttp/) + (The Apache Software License, Version 2.0) okhttp (com.squareup.okhttp3:okhttp-jvm:5.2.1 - https://square.github.io/okhttp/) (The Apache Software License, Version 2.0) okio (com.squareup.okio:okio:3.6.0 - https://github.com/square/okio/) (The Apache Software License, Version 2.0) okio (com.squareup.okio:okio-jvm:3.16.1 - https://github.com/square/okio/) (Apache-2.0) config (com.typesafe:config:1.4.1 - https://github.com/lightbend/config) (Apache-2.0) Apache Commons Codec (commons-codec:commons-codec:1.19.0 - https://commons.apache.org/proper/commons-codec/) (Apache-2.0) Apache Commons IO (commons-io:commons-io:2.20.0 - https://commons.apache.org/proper/commons-io/) - (Apache-2.0) Apache Commons Logging (commons-logging:commons-logging:1.3.6 - https://commons.apache.org/proper/commons-logging/) + (Apache-2.0) Apache Commons Logging (commons-logging:commons-logging:1.4.0 - https://commons.apache.org/proper/commons-logging/) (Apache 2.0) io.grpc:grpc-api (io.grpc:grpc-api:1.63.0 - https://github.com/grpc/grpc-java) (Apache 2.0) io.grpc:grpc-context (io.grpc:grpc-context:1.63.0 - https://github.com/grpc/grpc-java) (Apache 2.0) io.grpc:grpc-core (io.grpc:grpc-core:1.63.0 - https://github.com/grpc/grpc-java) @@ -97,9 +97,14 @@ Lists of 258 third-party dependencies. (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-api:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-common:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-context:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-exporter-common:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-exporter-otlp:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-exporter-otlp-common:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-extension-trace-propagators:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-common:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-logs:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-metrics:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-trace:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) diff --git a/src/control-plane-services/notary/.gitignore b/src/control-plane-services/notary/.gitignore new file mode 100644 index 000000000..5ed502b20 --- /dev/null +++ b/src/control-plane-services/notary/.gitignore @@ -0,0 +1,23 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** +logs/** +tmp/** + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +.classpath +.factorypath +.project +.settings/ + +### secrets +vault-agent/certs +vault-agent/secrets + +### flatten-maven-plugin output +.flattened-pom.xml diff --git a/src/control-plane-services/notary/AGENTS.md b/src/control-plane-services/notary/AGENTS.md new file mode 100644 index 000000000..7aa8cc33f --- /dev/null +++ b/src/control-plane-services/notary/AGENTS.md @@ -0,0 +1,59 @@ +# AGENTS.md - Notary + +Notary is an OSS/self-hosted Java service in the root `nvcf` Bazel module. It +does not own a nested module, lockfile, Bazel configuration, or third-party +dependency hub. + +Maven POMs remain for coexistence. Bazel consumes nv-boot through direct source +labels and produces the executable application jar. Do not generate or publish +Maven-shaped Notary artifacts from Bazel. + +## Build and test + +Run commands from the monorepo root: + +```bash +export BAZEL_OUTPUT_USER_ROOT="${TMPDIR:-/tmp}/nvcf-bazel-cache" + +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + build //src/control-plane-services/notary/... + +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + test //src/control-plane-services/notary/... \ + --cache_test_results=no \ + --test_output=errors +``` + +The tests use in-process Spring Boot and WireMock servers. They do not require +Docker. CI therefore routes this component to the `build-container` lane. + +## Dependencies + +The root `MODULE.bazel` and `maven_install.json` own the shared +`@nv_third_party_deps` hub. BUILD targets declare direct compile and runtime +edges. A coordinate in the hub is not automatically on Notary's classpath. + +Use direct labels for nv-boot source targets: + +```text +//src/libraries/java/nv-boot-parent/nv-boot-starter-core:nv_boot_starter_core +``` + +The source Maven build pins `commons-collections4` 4.4. The monorepo shared +dependency graph selects the compatible 4.5.0 version. Keep that intentional +parity difference documented instead of adding a lower root pin. + +## NOTICE + +The component `NOTICE` is derived from jars under the Bazel-built +`notary-service:app`. Generate and check it through the root-owned rules: + +```bash +bazel run //src/control-plane-services/notary:generate_notice -- \ + --update-metadata --write +bazel test //src/control-plane-services/notary:notice_check_test +bazel build //src/control-plane-services/notary:osrb_dependency_delta +``` + +Do not restore the source repository's Maven NOTICE generator or copy its +standalone `LICENSE` or `NOTICE` files into this subtree. diff --git a/src/control-plane-services/notary/BAZEL.md b/src/control-plane-services/notary/BAZEL.md new file mode 100644 index 000000000..3d2c0923d --- /dev/null +++ b/src/control-plane-services/notary/BAZEL.md @@ -0,0 +1,251 @@ +# Bazel for Notary + +Notary is part of the root `nvcf` Bazel module. Run every Bazel command in this +guide from the monorepo root. Maven commands still run from +`src/control-plane-services/notary`. + +## Shared configuration + +The service does not own nested Bazel configuration: + +- `.bazelversion` pins the Bazel release used by Bazelisk. +- `.bazelrc` holds repository defaults, including Java 25 and + `--java_header_compilation=false`. +- `.bazel_downloader_config` rewrites supported download URLs. It does not + select dependency versions. +- `MODULE.bazel` declares rule modules, BOMs, and dependency roots. +- `maven_install.json` is the generated exact lock for third-party Java + coordinates. It does not publish Maven artifacts. +- `MODULE.bazel.lock` is the generated Bzlmod lock. + +The root uses `local_jdk`. Install a full JDK 25 and set `JAVA_HOME`. Diagnose +the selected target and host toolchains with: + +```bash +bazel cquery @bazel_tools//tools/jdk:current_java_runtime \ + --output=starlark \ + --starlark:expr='str(providers(target)["ToolchainInfo"].java_runtime.version)' + +bazel cquery @bazel_tools//tools/jdk:current_host_java_runtime \ + --output=starlark \ + --starlark:expr='str(providers(target)["ToolchainInfo"].java_runtime.version)' +``` + +Both commands must print `25`. `bazel info java-home` reports the Bazel server +JVM and is not compiler toolchain proof. + +## Output root and clean + +```bash +export BAZEL_OUTPUT_USER_ROOT="${TMPDIR:-/tmp}/nvcf-bazel-cache" + +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" clean +``` + +Use `clean --expunge` only to reset a corrupted cache. + +## Build + +```bash +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + build //src/control-plane-services/notary/... + +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + build //src/control-plane-services/notary/notary-core:notary_core + +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + build //src/control-plane-services/notary/notary-service:app +``` + +The executable output is: + +```text +bazel-bin/src/control-plane-services/notary/notary-service/app.jar +``` + +Inspect its launcher and generated build metadata: + +```bash +unzip -p \ + bazel-bin/src/control-plane-services/notary/notary-service/app.jar \ + META-INF/MANIFEST.MF + +unzip -p \ + bazel-bin/src/control-plane-services/notary/notary-service/app.jar \ + BOOT-INF/classes/git.properties +``` + +## Test + +Run every Notary test without cached results: + +```bash +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + test //src/control-plane-services/notary/... \ + --cache_test_results=no \ + --test_output=errors +``` + +Run one module: + +```bash +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + test //src/control-plane-services/notary/notary-core:tests \ + --cache_test_results=no \ + --test_output=errors +``` + +Run one class: + +```bash +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + test //src/control-plane-services/notary/notary-core:tests \ + --cache_test_results=no \ + --test_output=streamed \ + --test_arg=--exclude-classname='^(?!com.nvidia.notary.services.SigningServiceTest$).*$' +``` + +Run one method: + +```bash +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + test //src/control-plane-services/notary/notary-core:tests \ + --cache_test_results=no \ + --test_output=streamed \ + --test_arg=--exclude-classname='^(?!com.nvidia.notary.services.SigningServiceTest$).*$' \ + --test_arg=--include-methodname='.*#sign.*$' +``` + +Each module writes real Java test and coverage outputs under: + +```text +bazel-testlogs/src/control-plane-services/notary//tests/test.log +bazel-testlogs/src/control-plane-services/notary//tests/test.outputs/junit/TEST-junit-jupiter.xml +bazel-testlogs/src/control-plane-services/notary//tests/test.outputs/jacoco.exec +bazel-testlogs/src/control-plane-services/notary//tests/test.outputs/jacoco.xml +bazel-testlogs/src/control-plane-services/notary//tests/test.outputs/index.html +``` + +Use `test.outputs/junit/TEST-junit-jupiter.xml` for JUnit reporting. The nearby +outer `test.xml` describes one Bazel shell wrapper and is not the Java report. +Point Sonar at both module `jacoco.xml` files. + +## NOTICE and OSRB delta + +The component NOTICE is derived from exact jars nested under `BOOT-INF/lib`. +The component metadata adds only dependencies not already owned by nv-boot. + +```bash +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + run //src/control-plane-services/notary:generate_notice -- \ + --update-metadata --write + +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + test //src/control-plane-services/notary:notice_check_test + +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + build //src/control-plane-services/notary:osrb_dependency_delta +``` + +The generated inventory and exact license-grouped delta are under: + +```text +bazel-bin/src/control-plane-services/notary/runtime_inventory.json +bazel-bin/src/control-plane-services/notary/osrb_dependency_delta.json +bazel-bin/src/control-plane-services/notary/osrb_dependency_delta.md +``` + +## Dependency lock + +The root shared dependency graph selects compatible versions for all Java +components. Notary's Maven source pins `commons-collections4` 4.4; the shared +graph already selects 4.5.0. This is an intentional parity difference and is +not overridden with a lower root pin. + +The shared root pins `commons-logging` 1.4.0 because that active nv-boot parent +property override reaches the Maven executable runtime. The app also owns a +runtime edge to `spring-boot-jarmode-tools`, which Maven's Spring Boot repackage +goal injects into the executable jar. + +The remaining executable-jar inventory differences are packaging-only: + +- The shared graph selects JetBrains annotations 17.0.0 instead of Maven's + 13.0 release. +- Bazel omits Lombok from the runtime jar because it is a compile-time + annotation processor. +- Bazel retains Spring Boot starter marker jars. They contain metadata and no + classes; Maven's repackage goal omits them. +- Bazel first-party jars use target-derived names instead of Maven artifact + names, but contain the same Notary and nv-boot classes. + +Spring Framework 7 embeds the AOP Alliance API in `spring-aop`. The root excludes +the legacy standalone `aopalliance` jar so the Bazel executable matches Maven +and does not package duplicate `org.aopalliance` classes. + +After changing a root dependency input, regenerate the shared lock: + +```bash +REPIN=1 bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + run @nv_third_party_deps//:pin +``` + +Do not hand-edit `maven_install.json` or `MODULE.bazel.lock`. + +## Docker + +Build the app and resolve the real Bazel output directory: + +```bash +bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" \ + build //src/control-plane-services/notary/notary-service:app + +BAZEL_BIN_DIR="$( + bazel --output_user_root="${BAZEL_OUTPUT_USER_ROOT}" info bazel-bin +)" + +docker build \ + -f src/control-plane-services/notary/notary-service/Dockerfile \ + --build-arg APP_JAR=app.jar \ + -t notary:bazel \ + "${BAZEL_BIN_DIR}/src/control-plane-services/notary/notary-service" +``` + +The local profile needs an OAuth2 issuer, assertion issuer, and signing-key +file. Start it with values for your local issuer: + +```bash +docker run --rm \ + -p 8080:8080 \ + -p 8181:8181 \ + -e SPRING_PROFILES_ACTIVE=local \ + -e AUTH_TOKEN_ISSUER="${AUTH_TOKEN_ISSUER}" \ + -e ASSERTION_ISSUER_URL="${ASSERTION_ISSUER_URL}" \ + -e VAULT_SECRETS_JSON_PATH=file:/run/notary/vault-secrets.json \ + -v "$(pwd)/src/control-plane-services/notary/notary-core/src/test/resources/vault-agent/integration-test-vault.json:/run/notary/vault-secrets.json:ro" \ + notary:bazel +``` + +Readiness is `http://localhost:8181/actuator/health/readiness`. + +## Maven coexistence + +Maven remains independent: + +```bash +cd src/control-plane-services/notary +mvn clean verify +``` + +Bazel uses direct source labels for co-located nv-boot code. Do not add a local +source override or a nested dependency hub. + +## GitHub CI + +`bazel-java-ci.json` registers Notary with the root workflow. The workflow +discovers the component path, includes root Java tooling and dependency changes +as triggers, validates reverse dependencies after nv-boot changes, and uploads +the app jar, JUnit, JaCoCo, NOTICE, inventory, and OSRB delta outputs. + +Notary has no Docker-backed tests, so it uses the `build-container` lane. The +root workflow currently runs the full matrix on each pull request and push, +even though dependency-aware selection logic is available. diff --git a/src/control-plane-services/notary/BUILD.bazel b/src/control-plane-services/notary/BUILD.bazel new file mode 100644 index 000000000..e37d6b6fd --- /dev/null +++ b/src/control-plane-services/notary/BUILD.bazel @@ -0,0 +1,26 @@ +load("//rules/java:notice.bzl", "nvcf_notice", "nvcf_notice_delta") + +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "NOTICE", + "notice_metadata.json", +]) + +nvcf_notice( + checked_notice = ":NOTICE", + first_party_groups = ["com.nvidia.notary"], + inventory_name = "notary_runtime_inventory", + metadata = ":notice_metadata.json", + runtime_target = "//src/control-plane-services/notary/notary-service:app", + shared_metadata = [ + "//src/libraries/java/nv-boot-parent:notice_metadata.json", + ], +) + +nvcf_notice_delta( + baseline_inventories = [ + "//src/libraries/java/nv-boot-parent:runtime_inventory.json", + ], + inventory = ":runtime_inventory.json", +) diff --git a/src/control-plane-services/notary/CLAUDE.md b/src/control-plane-services/notary/CLAUDE.md new file mode 100644 index 000000000..43c994c2d --- /dev/null +++ b/src/control-plane-services/notary/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/src/control-plane-services/notary/CONTRIBUTING.md b/src/control-plane-services/notary/CONTRIBUTING.md new file mode 100644 index 000000000..2e325acf9 --- /dev/null +++ b/src/control-plane-services/notary/CONTRIBUTING.md @@ -0,0 +1,125 @@ + +# Contributing to NVCF Notary + +Thank you for your interest in contributing to NVCF Notary! We welcome +contributions from the community. + +## Developer Certificate of Origin (DCO) + +All contributions to this project must be accompanied by a Developer +Certificate of Origin (DCO) sign-off. The DCO is a lightweight mechanism for +contributors to certify that they wrote or otherwise have the right to submit +the contribution under the project's open-source license. The full text of +the DCO is available at : + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +### How to Sign Off + +Add a `Signed-off-by` line to each of your commit messages: + +``` +Signed-off-by: Your Name +``` + +You can do this automatically by using the `-s` flag with `git commit`: + +```bash +git commit -s -m "Your commit message" +``` + +The name and email used must match those configured in `git config`. + +## How to Contribute + +1. Fork the repository on GitHub. +2. Create a feature branch (`git checkout -b feature/my-feature`). +3. Make your changes. Ensure the code builds and all tests pass: + ```bash + mvn clean verify + ``` +4. Ensure all new files include the standard NVIDIA SPDX Apache-2.0 header. + See any existing source file in the repository for the canonical header + format (Java sources use the javadoc-style block comment). +5. If you add or upgrade a dependency, regenerate the runtime-derived + component `NOTICE` from the monorepo root and run its drift test: + ```bash + bazel run //src/control-plane-services/notary:generate_notice -- \ + --update-metadata --write + bazel test //src/control-plane-services/notary:notice_check_test + ``` + Commit `NOTICE`, `notice_metadata.json`, and dependency lock changes + together. +6. Commit your changes with DCO sign-off (`git commit -s`). +7. Push to your fork (`git push origin feature/my-feature`). +8. Open a Pull Request against `main`. + +## Code Style + +- Java sources follow the formatting enforced by the project's Checkstyle + configuration. Run `mvn -pl notary-core checkstyle:check` to validate. +- Public APIs should include Javadoc. +- Follow conventional commit style for commit subjects (e.g. `feat:`, + `fix:`, `chore:`, `docs:`). + +## Reporting Issues + +Please file issues on the project's GitHub issue tracker. Include: + +- A clear description of the problem or proposed enhancement +- Steps to reproduce (for bugs), including service version and JVM version +- Relevant logs (with secrets redacted) + +## License + +By contributing to this project, you agree that your contributions will be +licensed under the [Apache License 2.0](../../../LICENSE). diff --git a/src/control-plane-services/notary/NOTICE b/src/control-plane-services/notary/NOTICE new file mode 100644 index 000000000..65237f122 --- /dev/null +++ b/src/control-plane-services/notary/NOTICE @@ -0,0 +1,133 @@ + +Lists of 131 third-party dependencies. + (EPL-2.0) (LGPL-2.1-only) Logback Classic Module (ch.qos.logback:logback-classic:1.5.34 - http://logback.qos.ch) + (EPL-2.0) (LGPL-2.1-only) Logback Core Module (ch.qos.logback:logback-core:1.5.34 - http://logback.qos.ch) + (The Apache Software License, Version 2.0) Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.21 - https://github.com/FasterXML/jackson) + (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.21.4 - https://github.com/FasterXML/jackson-core) + (The Apache Software License, Version 2.0) jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.21.4 - https://github.com/FasterXML/jackson) + (The Apache Software License, Version 2.0) Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.4 - https://github.com/FasterXML/jackson-dataformats-text) + (The Apache Software License, Version 2.0) Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.4 - https://github.com/FasterXML/jackson-modules-java8) + (Apache License, Version 2.0) ClassMate (com.fasterxml:classmate:1.7.3 - https://github.com/FasterXML/java-classmate) + (Apache License, Version 2.0) Caffeine cache (com.github.ben-manes.caffeine:caffeine:3.2.4 - https://github.com/ben-manes/caffeine) + (Apache License, Version 2.0) Caffeine cache (com.github.ben-manes.caffeine:guava:3.2.4 - https://github.com/ben-manes/caffeine) + (Apache 2.0) error-prone annotations (com.google.errorprone:error_prone_annotations:2.49.0 - https://errorprone.info) + (Apache License, Version 2.0) Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.3 - https://github.com/google/guava) + (Apache License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:33.6.0-jre - https://github.com/google/guava) + (The Apache Software License, Version 2.0) Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava) + (Apache License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.1 - https://github.com/google/j2objc/) + (The Apache Software License, Version 2.0) Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:10.4 - https://bitbucket.org/connect2id/nimbus-jose-jwt) + (The Apache Software License, Version 2.0) okhttp (com.squareup.okhttp3:okhttp-jvm:5.2.1 - https://square.github.io/okhttp/) + (The Apache Software License, Version 2.0) okio (com.squareup.okio:okio-jvm:3.16.1 - https://github.com/square/okio/) + (Apache-2.0) Apache Commons Logging (commons-logging:commons-logging:1.4.0 - https://commons.apache.org/proper/commons-logging/) + (The Apache Software License, Version 2.0) context-propagation (io.micrometer:context-propagation:1.2.1 - https://github.com/micrometer-metrics/context-propagation) + (The Apache Software License, Version 2.0) micrometer-commons (io.micrometer:micrometer-commons:1.16.6 - https://github.com/micrometer-metrics/micrometer) + (The Apache Software License, Version 2.0) micrometer-core (io.micrometer:micrometer-core:1.16.6 - https://github.com/micrometer-metrics/micrometer) + (The Apache Software License, Version 2.0) micrometer-jakarta9 (io.micrometer:micrometer-jakarta9:1.16.6 - https://github.com/micrometer-metrics/micrometer) + (The Apache Software License, Version 2.0) micrometer-observation (io.micrometer:micrometer-observation:1.16.6 - https://github.com/micrometer-metrics/micrometer) + (The Apache Software License, Version 2.0) micrometer-registry-prometheus (io.micrometer:micrometer-registry-prometheus:1.16.6 - https://github.com/micrometer-metrics/micrometer) + (The Apache Software License, Version 2.0) micrometer-tracing (io.micrometer:micrometer-tracing:1.6.6 - https://github.com/micrometer-metrics/tracing) + (The Apache Software License, Version 2.0) micrometer-tracing-bridge-otel (io.micrometer:micrometer-tracing-bridge-otel:1.6.6 - https://github.com/micrometer-metrics/tracing) + (The Apache License, Version 2.0) OpenTelemetry Semantic Conventions Java (io.opentelemetry.semconv:opentelemetry-semconv:1.37.0 - https://github.com/open-telemetry/semantic-conventions-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-api:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-common:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-context:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-exporter-common:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-exporter-otlp:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-exporter-otlp-common:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-extension-trace-propagators:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-common:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-logs:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-metrics:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache License, Version 2.0) OpenTelemetry Java (io.opentelemetry:opentelemetry-sdk-trace:1.55.0 - https://github.com/open-telemetry/opentelemetry-java) + (The Apache Software License, Version 2.0) Prometheus Metrics Config (io.prometheus:prometheus-metrics-config:1.4.3 - http://github.com/prometheus/client_java) + (The Apache Software License, Version 2.0) Prometheus Metrics Core (io.prometheus:prometheus-metrics-core:1.4.3 - http://github.com/prometheus/client_java) + (The Apache Software License, Version 2.0) Prometheus Metrics Exposition Formats (io.prometheus:prometheus-metrics-exposition-formats:1.4.3 - http://github.com/prometheus/client_java) + (The Apache Software License, Version 2.0) Prometheus Metrics Exposition Text Formats (io.prometheus:prometheus-metrics-exposition-textformats:1.4.3 - http://github.com/prometheus/client_java) + (The Apache Software License, Version 2.0) Prometheus Metrics Model (io.prometheus:prometheus-metrics-model:1.4.3 - http://github.com/prometheus/client_java) + (The Apache Software License, Version 2.0) Prometheus Metrics Tracer Common (io.prometheus:prometheus-metrics-tracer-common:1.4.3 - http://github.com/prometheus/client_java) + (Apache License 2.0) swagger-annotations-jakarta (io.swagger.core.v3:swagger-annotations-jakarta:2.2.47 - https://github.com/swagger-api/swagger-core) + (Apache License 2.0) swagger-core-jakarta (io.swagger.core.v3:swagger-core-jakarta:2.2.47 - https://github.com/swagger-api/swagger-core) + (Apache License 2.0) swagger-models-jakarta (io.swagger.core.v3:swagger-models-jakarta:2.2.47 - https://github.com/swagger-api/swagger-core) + (EDL 1.0) Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.4 - https://github.com/jakartaee/jaf-api) + (EPL 2.0) (GPL2 w/ CPE) Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:3.0.0 - https://projects.eclipse.org/projects/ee4j.ca) + (Apache License 2.0) Jakarta Validation API (jakarta.validation:jakarta.validation-api:3.1.1 - https://beanvalidation.org) + (Eclipse Distribution License - v 1.0) Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.5 - https://github.com/jakartaee/jaxb-api) + (Apache-2.0) Apache Commons Collections (org.apache.commons:commons-collections4:4.5.0 - https://commons.apache.org/proper/commons-collections/) + (Apache-2.0) Apache Commons Lang (org.apache.commons:commons-lang3:3.20.0 - https://commons.apache.org/proper/commons-lang/) + (Apache-2.0) Apache Log4j API (org.apache.logging.log4j:log4j-api:2.25.4 - https://logging.apache.org/log4j/2.x/) + (Apache-2.0) Log4j API to SLF4J Adapter (org.apache.logging.log4j:log4j-to-slf4j:2.25.4 - https://logging.apache.org/log4j/2.x/) + (Apache License, Version 2.0) tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:11.0.22 - https://tomcat.apache.org/) + (Apache License, Version 2.0) tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:11.0.22 - https://tomcat.apache.org/) + (Apache License, Version 2.0) tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:11.0.22 - https://tomcat.apache.org/) + (Eclipse Public License - v 2.0) AspectJ Weaver (org.aspectj:aspectjweaver:1.9.25.1 - https://www.eclipse.org/aspectj/) + (Bouncy Castle Licence) Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.84 - https://www.bouncycastle.org/download/bouncy-castle-java/) + (Public Domain, per Creative Commons CC0) (BSD-2-Clause) HdrHistogram (org.hdrhistogram:HdrHistogram:2.2.2 - http://hdrhistogram.github.io/HdrHistogram/) + (Apache License 2.0) Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:9.0.1.Final - https://hibernate.org/validator) + (Apache License 2.0) JBoss Logging 3 (org.jboss.logging:jboss-logging:3.6.3.Final - https://www.jboss.org) + (Apache-2.0) Kotlin Stdlib (org.jetbrains.kotlin:kotlin-stdlib:2.2.21 - https://kotlinlang.org/) + (The Apache Software License, Version 2.0) JetBrains Java Annotations (org.jetbrains:annotations:17.0.0 - https://github.com/JetBrains/java-annotations) + (The Apache License, Version 2.0) JSpecify annotations (org.jspecify:jspecify:1.0.0 - http://jspecify.org/) + (Public Domain, per Creative Commons CC0) LatencyUtils (org.latencyutils:LatencyUtils:2.0.3 - http://latencyutils.github.io/LatencyUtils/) + (MIT) JUL to SLF4J bridge (org.slf4j:jul-to-slf4j:2.0.18 - http://www.slf4j.org) + (MIT) SLF4J API Module (org.slf4j:slf4j-api:2.0.18 - http://www.slf4j.org) + (The Apache License, Version 2.0) springdoc-openapi-starter-common (org.springdoc:springdoc-openapi-starter-common:3.0.3 - https://springdoc.org/) + (The Apache License, Version 2.0) springdoc-openapi-starter-webmvc-api (org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.3 - https://springdoc.org/) + (Apache License, Version 2.0) spring-boot (org.springframework.boot:spring-boot:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-actuator (org.springframework.boot:spring-boot-actuator:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-health (org.springframework.boot:spring-boot-health:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-http-client (org.springframework.boot:spring-boot-http-client:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-http-converter (org.springframework.boot:spring-boot-http-converter:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-jackson (org.springframework.boot:spring-boot-jackson:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-jarmode-tools (org.springframework.boot:spring-boot-jarmode-tools:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-micrometer-metrics (org.springframework.boot:spring-boot-micrometer-metrics:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-micrometer-observation (org.springframework.boot:spring-boot-micrometer-observation:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-micrometer-tracing (org.springframework.boot:spring-boot-micrometer-tracing:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-micrometer-tracing-opentelemetry (org.springframework.boot:spring-boot-micrometer-tracing-opentelemetry:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-opentelemetry (org.springframework.boot:spring-boot-opentelemetry:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-restclient (org.springframework.boot:spring-boot-restclient:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-security (org.springframework.boot:spring-boot-security:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-security-oauth2-resource-server (org.springframework.boot:spring-boot-security-oauth2-resource-server:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-servlet (org.springframework.boot:spring-boot-servlet:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter (org.springframework.boot:spring-boot-starter:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-aspectj (org.springframework.boot:spring-boot-starter-aspectj:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-jackson (org.springframework.boot:spring-boot-starter-jackson:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-logging (org.springframework.boot:spring-boot-starter-logging:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-micrometer-metrics (org.springframework.boot:spring-boot-starter-micrometer-metrics:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-security (org.springframework.boot:spring-boot-starter-security:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-security-oauth2-resource-server (org.springframework.boot:spring-boot-starter-security-oauth2-resource-server:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-tomcat-runtime (org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-validation (org.springframework.boot:spring-boot-starter-validation:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-starter-webmvc (org.springframework.boot:spring-boot-starter-webmvc:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-tomcat (org.springframework.boot:spring-boot-tomcat:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-validation (org.springframework.boot:spring-boot-validation:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-web-server (org.springframework.boot:spring-boot-web-server:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) spring-boot-webmvc (org.springframework.boot:spring-boot-webmvc:4.0.7 - https://spring.io/projects/spring-boot) + (Apache License, Version 2.0) Spring Cloud Commons (org.springframework.cloud:spring-cloud-commons:5.0.2 - https://projects.spring.io/spring-cloud/spring-cloud-commons/) + (Apache License, Version 2.0) Spring Cloud Context (org.springframework.cloud:spring-cloud-context:5.0.2 - https://projects.spring.io/spring-cloud/spring-cloud-context/) + (Apache License, Version 2.0) spring-cloud-starter (org.springframework.cloud:spring-cloud-starter:5.0.2 - https://projects.spring.io/spring-cloud) + (Apache License, Version 2.0) spring-cloud-starter-bootstrap (org.springframework.cloud:spring-cloud-starter-bootstrap:5.0.2 - https://projects.spring.io/spring-cloud) + (Apache License, Version 2.0) spring-security-config (org.springframework.security:spring-security-config:7.0.6 - https://spring.io/projects/spring-security) + (Apache License, Version 2.0) spring-security-core (org.springframework.security:spring-security-core:7.0.6 - https://spring.io/projects/spring-security) + (Apache License, Version 2.0) spring-security-crypto (org.springframework.security:spring-security-crypto:7.0.6 - https://spring.io/projects/spring-security) + (Apache License, Version 2.0) spring-security-oauth2-core (org.springframework.security:spring-security-oauth2-core:7.0.6 - https://spring.io/projects/spring-security) + (Apache License, Version 2.0) spring-security-oauth2-jose (org.springframework.security:spring-security-oauth2-jose:7.0.6 - https://spring.io/projects/spring-security) + (Apache License, Version 2.0) spring-security-oauth2-resource-server (org.springframework.security:spring-security-oauth2-resource-server:7.0.6 - https://spring.io/projects/spring-security) + (Apache License, Version 2.0) spring-security-web (org.springframework.security:spring-security-web:7.0.6 - https://spring.io/projects/spring-security) + (Apache License, Version 2.0) Spring AOP (org.springframework:spring-aop:7.0.8 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Beans (org.springframework:spring-beans:7.0.8 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Context (org.springframework:spring-context:7.0.8 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Core (org.springframework:spring-core:7.0.8 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Expression Language (SpEL) (org.springframework:spring-expression:7.0.8 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Web (org.springframework:spring-web:7.0.8 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) Spring Web MVC (org.springframework:spring-webmvc:7.0.8 - https://github.com/spring-projects/spring-framework) + (Apache License, Version 2.0) SnakeYAML (org.yaml:snakeyaml:2.5 - https://bitbucket.org/snakeyaml/snakeyaml) + (The Apache Software License, Version 2.0) Jackson-core (tools.jackson.core:jackson-core:3.1.4 - https://github.com/FasterXML/jackson-core) + (The Apache Software License, Version 2.0) jackson-databind (tools.jackson.core:jackson-databind:3.1.4 - https://github.com/FasterXML/jackson) + (The Apache Software License, Version 2.0) Jackson module: Blackbird (tools.jackson.module:jackson-module-blackbird:3.1.4 - https://github.com/FasterXML/jackson-modules-base) diff --git a/src/control-plane-services/notary/README.md b/src/control-plane-services/notary/README.md new file mode 100644 index 000000000..7a0df218c --- /dev/null +++ b/src/control-plane-services/notary/README.md @@ -0,0 +1,164 @@ +# NVCF Notary + +Notary is a microservice that issues short-lived signed JWTs ("assertions") +to authenticated callers. Services that need integrity-protected, verifiable +data carriers between two parties can deploy Notary alongside their stack +and have it sign arbitrary JSON payloads with an EC P-256 key whose public +half is published at `/.well-known/jwks.json`. + +A typical use: + +1. Caller obtains an OAuth2 access token from any OIDC-compliant token issuer. +2. Caller `POST`s a payload to Notary's `/sign` with that token in the + `Authorization: Bearer ...` header. +3. Notary validates the caller token (issuer + scope + audience), wraps the + payload as a JWT, signs it, and returns the assertion. +4. Downstream services verify the assertion against Notary's published JWKS. + +## License + +This project is licensed under the +[Apache License 2.0](../../../LICENSE). A complete attribution of third-party +libraries bundled in the Bazel-built application lives in [NOTICE](NOTICE). +Regenerate that file from the monorepo root: + +```bash +bazel run //src/control-plane-services/notary:generate_notice -- \ + --update-metadata --write +``` + +Run the NOTICE drift test after any dependency change: + +```bash +bazel test //src/control-plane-services/notary:notice_check_test +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution guide, including +the DCO sign-off requirement. + +## Modules + +| Module | Description | +|---|---| +| [notary-core](notary-core/) | Shared library: signing pipeline, validators, security config. Published to URM as `com.nvidia.notary:notary-core`. | +| [notary-service](notary-service/) | Spring Boot executable depending on `notary-core`. Built into the OSS Docker image. | + +## Minimum Requirements + +- [Eclipse Temurin OpenJDK 25](https://adoptium.net/temurin/releases/) +- [Maven 3.8.7](https://maven.apache.org/download.cgi) or higher +- [Docker](https://docs.docker.com/get-docker/) + +## Building + +```bash +mvn clean package +``` + +Produces `notary-service/target/app.jar`. + +## Running locally + +Notary requires three runtime inputs: + +1. An OAuth2 token issuer: Notary validates inbound caller tokens + against this issuer's JWKS. Any OIDC-compliant provider works. Set: + - `AUTH_TOKEN_ISSUER`: full issuer URI (e.g. `https://accounts.example.com` + or `https://my-tenant.auth0.com/`). Spring fetches the issuer's + `/.well-known/openid-configuration` (or `/.well-known/jwks.json`) to + validate caller tokens. + - `AUTH_TOKEN_SCOPE` (optional, defaults to `notary-sign`): required + scope on caller tokens. + - `NOTARY_REQUIRED_AUDIENCES_0`, `..._1`, ...: literal + audiences a caller token must carry in its `aud` claim. Required + by default (deployment fails at boot if unset); see + `NOTARY_REQUIRE_AUDIENCE` below to opt into issuer-only + validation instead. + - `NOTARY_REQUIRE_AUDIENCE` (optional): set to `false` to + disable the audience check. Validates caller JWTs against the + issuer alone. Defaults to `true` so a misconfigured deployment + fails fast rather than silently accepting any issuer-valid token. +2. `ASSERTION_ISSUER_URL`: value placed in the `iss` claim of the JWTs + Notary signs. +3. A signing key rendered into a vault secrets file. Use the [test + fixture](notary-core/src/test/resources/vault-agent/integration-test-vault.json) + as a template, or generate a fresh keyset with `generate_jwks.sh` (see + below). + +```bash +export SPRING_PROFILES_ACTIVE=local +export AUTH_TOKEN_ISSUER=https://your-oauth2-provider.example.com +export ASSERTION_ISSUER_URL=http://assertion.issuer.test +export VAULT_SECRETS_JSON_PATH=file:/absolute/path/to/vault-secrets.json + +java -jar notary-service/target/app.jar +``` + +Endpoints: + +- `POST /sign` (port 8080): main signing endpoint, requires bearer token. +- `GET /.well-known/jwks.json` (port 8080): public-key set for assertion verification. +- `GET /actuator/health`, `/actuator/prometheus` (port 8181): operational endpoints. + +## Configuration reference + +| Environment variable | Description | +|---|---| +| `SPRING_PROFILES_ACTIVE` | One of `local`, `ncp`, or your own profile. Defaults to `local`. | +| `ASSERTION_ISSUER_URL` | Value placed in the `iss` claim of signed assertions. | +| `AUTH_TOKEN_ISSUER` | Full issuer URI Spring Security validates inbound caller JWTs against. | +| `AUTH_TOKEN_SCOPE` | Scope required on caller tokens. Defaults to `notary-sign`. | +| `NOTARY_REQUIRED_AUDIENCES_0`, `..._1`, ... | Indexed list of literal audiences required on caller JWTs. Required unless `NOTARY_REQUIRE_AUDIENCE=false`. | +| `NOTARY_REQUIRE_AUDIENCE` | `true` (default): fail at boot if no audiences set. `false`: validate against issuer alone. | +| `MAX_REQUEST_BODY_SIZE_BYTES` | Maximum payload size in bytes. Defaults to `4096`. JWTs need to fit in HTTP headers downstream, so keep this conservative. | +| `VAULT_SECRETS_JSON_PATH` | Path to the rendered vault secrets file containing the signing key set. Defaults to `vault/vault-secrets.json`; prefix with `file:` for absolute paths. | + +### NCP profile + +When using `ncp`: + +- `AUTH_TOKEN_ISSUER`: issuer for inbound auth tokens. +- `AUTH_TOKEN_PUBLIC_KEYSET_URL`: URL of the public keyset for token validation. +- `AUTH_TOKEN_SCOPE`: required scope; defaults to `notary-sign`. + +## Generating signing keys + +The `generate_jwks.sh` helper script produces an EC P-256 keyset using only +OpenSSL and standard Unix utilities. No JVM is required. Keys carry timestamp +IDs in the format `kid-yyyyMMdd-HHmm`. + +```bash +./generate_jwks.sh --keyset # complete keyset (default) +./generate_jwks.sh --key # single key +./generate_jwks.sh --escaped # escaped keyset for vault put +``` + +Equivalent JVM-side test entrypoints exist in `notary-core` for environments +where the script is unavailable: + +```bash +mvn -q clean test -Dtest=KeyGeneratorTest#generateInitialKeySet +mvn -q clean test -Dtest=KeyGeneratorTest#generateSigningKey +mvn -q clean test -Dtest=KeyGeneratorTest#generateInitialKeySetEscaped +``` + +## Monitoring + +Notary exposes the standard Spring Boot Actuator surface on port 8181: + +- `/actuator/health/{liveness,readiness}`: Kubernetes probes. +- `/actuator/prometheus`: metrics for scraping. +- `/actuator/metrics`: programmatic metric inspection. + +The `/health` endpoint is also exposed on port 8080 without authentication, +for load-balancer health checks. + +## Logging + +Per-request log lines on `/sign` look like: + +``` +>>> signing using kid 'signing-kid' for client 'caller-client-id' and aud '[...]' with jti '...' at '...' +``` + +`com.nvidia.notary` is at `INFO` by default. Override per profile. diff --git a/src/control-plane-services/notary/bazel-java-ci.json b/src/control-plane-services/notary/bazel-java-ci.json new file mode 100644 index 000000000..fca45b876 --- /dev/null +++ b/src/control-plane-services/notary/bazel-java-ci.json @@ -0,0 +1,6 @@ +{ + "ci_lane": "build-container", + "component_kind": "java-service", + "id": "notary", + "tests_skip": false +} diff --git a/src/control-plane-services/notary/generate_jwks.sh b/src/control-plane-services/notary/generate_jwks.sh new file mode 100755 index 000000000..e80dba997 --- /dev/null +++ b/src/control-plane-services/notary/generate_jwks.sh @@ -0,0 +1,162 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -e + +# Debug mode flag - default is disabled +DEBUG=false + +# Function to generate a timestamp-based key ID in format "kid-yyyyMMdd-HHmm" +generate_kid() { + echo "kid-$(date '+%Y%m%d-%H%M')" +} + +# Function to base64url encode +base64url_encode() { + base64 | tr '/+' '_-' | tr -d '=' | tr -d '\n' +} + +# Generate a new signing key +generate_signing_key() { + # Generate timestamp (seconds since epoch) + TIMESTAMP=$(date +%s) + + # Generate a key ID with timestamp format + KID=$(generate_kid) + + # Create a temporary directory + TEMP_DIR=$(mktemp -d) + + # Set up cleanup to execute when the script exits + # This ensures the temporary directory is removed regardless of how the script exits: + # - Normal termination + # - Error conditions that cause exit + # - User interruption (like Ctrl+C) + # - Explicit exit commands + trap 'rm -rf "$TEMP_DIR"' EXIT + + # Redirect stderr based on debug mode + if [ "$DEBUG" = true ]; then + REDIRECT="" + else + REDIRECT="2>/dev/null" + fi + + # Generate EC private key + eval "openssl ecparam -name prime256v1 -genkey -noout -out \"$TEMP_DIR/private.pem\" $REDIRECT" + + # Extract public key + eval "openssl ec -in \"$TEMP_DIR/private.pem\" -pubout -out \"$TEMP_DIR/public.pem\" $REDIRECT" + + # Extract private key in DER format + eval "openssl ec -in \"$TEMP_DIR/private.pem\" -outform DER -out \"$TEMP_DIR/private.der\" $REDIRECT" + + # Extract parameters from the key + # First, get the private key 'd' value (last 32 bytes of DER) + D_VALUE=$(tail -c 32 "$TEMP_DIR/private.der" | base64url_encode) + + # Extract X and Y from public key + # The output format from OpenSSL is: 04 + X-coordinate (64 hex chars) + Y-coordinate (64 hex chars) + # where 04 indicates uncompressed point format for EC keys + PUB_HEX=$(eval "openssl ec -in \"$TEMP_DIR/private.pem\" -text -noout $REDIRECT" | grep -A 3 "pub:" | tail -n 3 | tr -d ' \n:' | sed 's/^.*pub//g') + # Skip the first 2 chars (04 prefix) and take the next 64 chars (32 bytes) for X coordinate + X_HEX=${PUB_HEX:2:64} + # Skip 2 chars for prefix + 64 chars for X (total 66) and take the next 64 chars for Y coordinate + Y_HEX=${PUB_HEX:66:64} + + # Convert to base64url + X_B64=$(echo -n "$X_HEX" | xxd -r -p | base64url_encode) + Y_B64=$(echo -n "$Y_HEX" | xxd -r -p | base64url_encode) + + # Create JSON output + cat < + + + 4.0.0 + + + com.nvidia.notary + notary-api + 0.0.1-SNAPSHOT + + + notary-core + Notary Core + jar + + NVCF Notary Core Library — shared business logic for signing assertions, consumable by + both open-source/self-hosted and managed environments. + + + + + + 4.4 + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + org.springframework.cloud + spring-cloud-context + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-jackson + + + org.springframework.boot + spring-boot-restclient + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-security-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-aspectj + + + org.springframework.boot + spring-boot-configuration-processor + + + org.springdoc + springdoc-openapi-starter-webmvc-api + + + io.micrometer + micrometer-registry-prometheus + + + org.apache.tomcat.embed + tomcat-embed-core + true + + + + + com.nvidia.boot + nv-boot-starter-core + + + com.nvidia.boot + nv-boot-starter-exceptions + + + com.nvidia.boot + nv-boot-starter-jwt + + + com.nvidia.boot + nv-boot-starter-reloadable-properties + + + com.nvidia.boot + nv-boot-starter-observability + + + + + org.apache.commons + commons-collections4 + ${common-collections4.version} + + + + + com.nvidia.boot + nv-boot-mock-servers-test + test + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + org.springframework + spring-test + test + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/ClockConfiguration.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/ClockConfiguration.java new file mode 100644 index 000000000..39d8c78bc --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/ClockConfiguration.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.config; + +import java.time.Clock; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ClockConfiguration { + + @Bean + public Clock getClock() { + return Clock.systemDefaultZone(); + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/ConfigurationValidator.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/ConfigurationValidator.java new file mode 100644 index 000000000..3583eef5b --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/ConfigurationValidator.java @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.config; + + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +/** + * Validates runtime dependencies that Jakarta Bean Validation on {@link NotaryProperties} can't + * express — specifically, that {@code signingKid} resolves to a private key in the loaded + * {@link JWKSet}. All other field-level constraints (blank/null/positive/URL/audience-element + * shape, plus the {@code requireAudience}/{@code requiredAudiences} cross-field rule) are + * enforced declaratively during {@code @ConfigurationProperties} binding. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ConfigurationValidator { + + private final NotaryProperties notaryProperties; + private final JWKSet keySet; + + public void validate() { + validateSigningKey(keySet); + } + + private void validateSigningKey(JWKSet keySet) { + String signingKid = notaryProperties.getSigningKid(); + // Defense-in-depth: @NotBlank on the field handles this in the normal Spring binding + // path, but a clearer error than "Signing key 'null' not found in key set" is cheap. + if (StringUtils.isBlank(signingKid)) { + throw new IllegalStateException("Signing key id is not set."); + } + if (keySet.isEmpty()) { + throw new IllegalStateException("Signing key set is empty."); + } + JWK signingKey = keySet.getKeyByKeyId(signingKid); + if (signingKey == null) { + throw new IllegalStateException( + "Signing key '%s' not found in key set.".formatted(signingKid)); + } + if (!signingKey.isPrivate()) { + throw new IllegalStateException("Signing key '%s' is not private.".formatted(signingKid)); + } + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/JwtConfiguration.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/JwtConfiguration.java new file mode 100644 index 000000000..cf998384a --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/JwtConfiguration.java @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.config; + +import com.nvidia.boot.jwt.configuration.JweKeysMapping; +import com.nvidia.boot.jwt.configuration.PrivateJwksString; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * JWT wiring for notary. Provides the {@code PrivateJwksString} and {@code JweKeysMapping} beans + * that nv-boot-starter-jwt's auto-configured {@code JwksConfiguration} consumes to register the + * (refresh-scoped) {@code jwkSet} and {@code jwtService}. + */ +@Slf4j +@Configuration +@RequiredArgsConstructor +public class JwtConfiguration { + + private final NotaryProperties notaryProperties; + + @Bean + @RefreshScope + public PrivateJwksString privateJwks() { + log.info("reloading private jwks"); + return new PrivateJwksString(notaryProperties.getPrivateJwks()); + } + + @Bean + public JweKeysMapping getJweKeysMapping() { + return JweKeysMapping.builder() + // JwtService requires a non-empty mapping even when notary never uses JWE. + .keysMapping(Map.of("dummy-jwe-key-name", "value")) + .build(); + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/NotaryProperties.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/NotaryProperties.java new file mode 100644 index 000000000..2b6351e00 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/NotaryProperties.java @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.config; + +import com.nimbusds.jose.JWSAlgorithm; +import jakarta.annotation.PostConstruct; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.constraints.URL; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +@Slf4j +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Configuration +@Validated +@ConfigurationProperties(prefix = "notary", ignoreUnknownFields = false) +public class NotaryProperties { + + @PostConstruct + private void postConstruct() { + log.info("recreating service config"); + } + + @NotBlank + @ToString.Exclude + private String privateJwks; + + @NotBlank + private String signingKid; + + @NotNull + private JWSAlgorithm signingAlgorithm; + + @NotBlank + @URL + private String issuerUrl; + + @NotBlank + private String signingScope; + + @Positive + private long maxAssertionsRequestSize; + + /** + * Audiences that must appear in the JWT {@code aud} claim. A token passes the audience check + * if its {@code aud} contains at least one entry from this list. An empty list disables the + * audience check entirely — but only when {@link #requireAudience} is {@code false}. With the + * default {@code requireAudience=true}, an empty list fails startup validation (enforced by + * {@link #isRequiredAudiencesValid()}) so that a misconfigured deployment cannot silently + * accept any issuer-valid token. Each entry must be non-blank, not end with {@code :}, and not + * contain an unresolved {@code ${...}} placeholder. + * + *

{@code @NotNull} guards against an explicit empty YAML/env value (e.g. + * {@code notary.required-audiences:}) being coerced to {@code null} by Spring's relaxed + * binder, which would bypass the element-level constraints and NPE downstream consumers. + */ + @NotNull + @Builder.Default + private List< + @NotBlank + @Pattern(regexp = "(?s)^(?!.*\\$\\{).+(? requiredAudiences = List.of(); + + /** + * Whether {@link #requiredAudiences} must be non-empty. Defaults to {@code true} so that an + * operator who forgets to set {@code NOTARY_REQUIRED_AUDIENCES_} fails at startup + * rather than silently accepting any issuer-valid JWT. Set to {@code false} only when issuer + * validation alone is sufficient (e.g. the NCP profile). + */ + @Builder.Default + private boolean requireAudience = true; + + /** + * Cross-field constraint: when {@link #requireAudience} is {@code true} (the default), + * {@link #requiredAudiences} must be non-empty. Method-level {@code @AssertTrue} on an + * {@code isXxx} method is the Jakarta Bean Validation idiom for cross-field rules — the + * spec requires a JavaBeans-compliant getter, so the {@code is} prefix is load-bearing. + * The trade-off is that the JavaBeans introspector exposes {@code requiredAudiencesValid} + * as a read-only computed property; harmless given {@code ignoreUnknownFields = false} on + * the class. + */ + @AssertTrue(message = "notary.required-audiences must be non-empty when " + + "notary.require-audience=true (the default); set literal values via " + + "NOTARY_REQUIRED_AUDIENCES_ env vars, or set NOTARY_REQUIRE_AUDIENCE=false " + + "to validate against issuer alone") + public boolean isRequiredAudiencesValid() { + return !requireAudience || (requiredAudiences != null && !requiredAudiences.isEmpty()); + } +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/ObservationConfiguration.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/ObservationConfiguration.java new file mode 100644 index 000000000..944055f1c --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/ObservationConfiguration.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.config; + +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.aop.ObservedAspect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class ObservationConfiguration { + + @Bean + public ObservedAspect observedAspect(ObservationRegistry observationRegistry) { + return new ObservedAspect(observationRegistry); + } +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/PublicKeySet.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/PublicKeySet.java new file mode 100644 index 000000000..1400429da --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/PublicKeySet.java @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.config; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.ListUtils; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Service; + +@Slf4j +@Getter +@RefreshScope +@Service +public class PublicKeySet { + + private final String value; + + /** + * Produces and caches the serialized value of public key set (JWKS). + * Value will be refreshed every time jwkSet is refreshed. + * + * @param jwkSet current jwk set. + */ + public PublicKeySet(JWKSet jwkSet) { + JWKSet publicJWKSet = jwkSet.toPublicJWKSet(); + + String publicKeyIds = ListUtils.emptyIfNull(publicJWKSet.getKeys()) + .stream() + .map(JWK::getKeyID) + .collect(Collectors.joining(",")); + log.info(">>> Using public keys set with key ids: {}", publicKeyIds); + + this.value = publicJWKSet.toString(); + } +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/SecurityConfiguration.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/SecurityConfiguration.java new file mode 100644 index 000000000..09f5b57da --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/config/SecurityConfiguration.java @@ -0,0 +1,167 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.config; + +import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; + +import jakarta.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.restclient.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.web.SecurityFilterChain; + + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfiguration { + + /** + * Incoming access tokens carry scopes in the {@code scopes} claim as a JSON array, not the + * standard OAuth 2 {@code scope} / {@code scp} space-delimited string — so Spring's default + * authorities converter does not work and we set this claim name explicitly. + */ + private static final String SCOPES_CLAIM = "scopes"; + + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuerUri; + + @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") + private String jwkSetUri; + + @Value("${spring.security.oauth2.resourceserver.jwt.jws-algorithms}") + private List jwsAlgorithms; + + /** + * Fail fast at bean-init time when {@code issuer-uri} is blank. {@link NimbusJwtDecoder} + * accepts an empty/relative {@code jwk-set-uri} at construction and only blows up at the + * first JWT validation, so without this guard a misconfigured pod would pass k8s readiness + * probes and return 401 silently on every {@code /sign} call. Throwing here aborts the bean + * factory, which in turn fails the readiness probe and blocks the rollout. + */ + @PostConstruct + void validateIssuerUri() { + if (StringUtils.isBlank(issuerUri)) { + throw new IllegalStateException( + "spring.security.oauth2.resourceserver.jwt.issuer-uri must be set; " + + "configure AUTH_TOKEN_ISSUER (or your profile's issuer-uri override). " + + "Without it Spring cannot validate caller JWTs and /sign would return 401 " + + "for every request, including those from valid issuers."); + } + } + + private final NotaryProperties notaryProperties; + + public SecurityConfiguration(NotaryProperties notaryProperties) { + this.notaryProperties = notaryProperties; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + // Disable CSRF since this service can be hit from anywhere + .csrf(AbstractHttpConfigurer::disable) //NOSONAR + .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) + .authorizeHttpRequests( + request -> request + .requestMatchers("/.well-known/jwks.json").permitAll() + .requestMatchers("/health").permitAll() + // everything under admin is not exposed via load balancer and only + // accessible via admin port + // it provides health readiness and liveness probes and metrics + .requestMatchers("/actuator/**").permitAll() + .anyRequest().authenticated()) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))) + .build(); + } + + /** + * Emits authorities raw (no {@code SCOPE_} prefix) to match the expression + * {@code hasAuthority(@notaryProperties.signingScope)} used by {@code NotaryController}. + */ + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(jwt -> { + List scopes = jwt.getClaimAsStringList(SCOPES_CLAIM); + if (scopes == null) { + return List.of(); + } + return scopes.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + }); + return converter; + } + + /** + * Builds a {@link JwtDecoder} that accepts the JWS algorithms listed in + * {@code spring.security.oauth2.resourceserver.jwt.jws-algorithms} and composes issuer + + * optional audience validation. When {@link NotaryProperties#getRequiredAudiences()} is empty, + * no audience check runs (used by the NCP profile, where the issuer check alone is + * sufficient). + * + *

The injected {@link RestTemplateBuilder} carries Spring Boot's + * {@code MetricsRestTemplateCustomizer}, so JWKS fetches are recorded as + * {@code http.client.requests} on the configured {@link io.micrometer.core.instrument.MeterRegistry}. + * Without this, {@link NimbusJwtDecoder} would build its own non-instrumented {@code RestTemplate} + * and the outbound JWKS call would never appear in {@code /actuator/prometheus}. + */ + @Bean + public JwtDecoder jwtDecoder(RestTemplateBuilder restTemplateBuilder) { + NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri) + .restOperations(restTemplateBuilder.build()) + .jwsAlgorithms(algs -> jwsAlgorithms.stream() + .map(SignatureAlgorithm::from) + .forEach(algs::add)) + .build(); + + List> validators = new ArrayList<>(); + validators.add(JwtValidators.createDefaultWithIssuer(issuerUri)); + List requiredAudiences = notaryProperties.getRequiredAudiences(); + if (!requiredAudiences.isEmpty()) { + validators.add(new JwtClaimValidator>( + "aud", + aud -> aud != null && requiredAudiences.stream().anyMatch(aud::contains))); + } + decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); + return decoder; + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/services/JtiGenerator.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/services/JtiGenerator.java new file mode 100644 index 000000000..484a1528e --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/services/JtiGenerator.java @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.services; + +import java.util.UUID; +import org.springframework.stereotype.Service; + +/** + * Wrapper class to allow mocking the generated id. + */ +@Service +public class JtiGenerator { + + public String generate() { + return UUID.randomUUID().toString(); + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/services/JwtResolver.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/services/JwtResolver.java new file mode 100644 index 000000000..2696b3bc1 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/services/JwtResolver.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.services; + +import com.nvidia.boot.exceptions.ForbiddenException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Service; + +@Service +public class JwtResolver { + + /** + * Retrieves callers JWT or Returns 403 if the caller authenticated in a way other than JWT. + * + * @return callers token. + */ + public Jwt getCallerToken() { + var auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.getPrincipal() instanceof Jwt jwt) { + return jwt; + } + + throw new ForbiddenException("authentication is not JWT"); + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/services/SigningService.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/services/SigningService.java new file mode 100644 index 000000000..6843dac7d --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/services/SigningService.java @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.services; + +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSHeader.Builder; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.nvidia.boot.jwt.services.JwtService; +import com.nvidia.notary.config.NotaryProperties; +import com.nvidia.notary.vo.AssertionRequestVo; +import java.time.Clock; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RefreshScope +@RequiredArgsConstructor +public class SigningService { + + public static final String CLAIM_ASSERTION = "assertion"; + + private final JwtService jwtService; + private final NotaryProperties notaryProperties; + private final Clock clock; + private final JtiGenerator jtiGenerator; + + /** + * Builds JWT claims for signing using validated request parameters and signs the token. + * + * @param validatedRequest value object for validated request parameters. + * @return signed JWT + */ + public SignedJWT sign(AssertionRequestVo validatedRequest) { + String callerClientId = validatedRequest.callerToken().getSubject(); + Instant now = clock.instant(); + String jti = jtiGenerator.generate(); + List aud = validatedRequest.audienceServiceIds(); + String kid = notaryProperties.getSigningKid(); + + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder() + .issuer(notaryProperties.getIssuerUrl()) + .jwtID(jti) + .subject(callerClientId) + .audience(aud) + .claim(CLAIM_ASSERTION, validatedRequest.data()) + .issueTime(Date.from(now)) + .build(); + + log.info(">>> signing using kid '{}' for client '{}' and aud '{}' with jti '{}' at '{}'", + kid, callerClientId, aud, jti, now); + + return getSignedToken(jwtClaimsSet, kid); + } + + private SignedJWT getSignedToken(JWTClaimsSet jwtClaimsSet, String signingKid) { + JWSHeader jwsHeader = new Builder(notaryProperties.getSigningAlgorithm()) + .keyID(signingKid) + .build(); + + SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaimsSet); + jwtService.signJwt(signedJWT, signingKid); + return signedJWT; + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/validators/AssertionRequestValidator.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/validators/AssertionRequestValidator.java new file mode 100644 index 000000000..3472ea023 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/validators/AssertionRequestValidator.java @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.validators; + +import com.nvidia.notary.config.NotaryProperties; +import com.nvidia.notary.services.JwtResolver; +import com.nvidia.notary.vo.AssertionRequestVo; +import com.nvidia.notary.web.dto.AssertionRequest; +import com.nvidia.boot.exceptions.BadRequestException; +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AssertionRequestValidator { + + private final NotaryProperties notaryProperties; + private final AudiencesValidator audiencesValidator; + private final JwtResolver jwtResolver; + + /** + * Validates assertion request parameters. + * + * @param requestBody Deserialized request body. + * @param request Reference to current request. + * + * @return valid assertion request parameters ready for issuing token. + */ + public AssertionRequestVo validate(AssertionRequest requestBody, HttpServletRequest request) { + if (requestBody == null || request == null || request.getContentLength() == 0) { + throw new BadRequestException("Content length must be greater than zero"); + } + + if (request.getContentLength() > notaryProperties.getMaxAssertionsRequestSize()) { + throw new BadRequestException("Content length is greater than the maximum allowed: " + + notaryProperties.getMaxAssertionsRequestSize()); + } + + Map data = requestBody.getData(); + if (data == null || data.isEmpty()) { + throw new BadRequestException("Request data is empty"); + } + + List validatedAudiences = audiencesValidator.getValidatedAudiences( + requestBody.getAudienceServiceIds()); + + Jwt callerToken = jwtResolver.getCallerToken(); + + return new AssertionRequestVo(callerToken, validatedAudiences, data); + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/validators/AudiencesValidator.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/validators/AudiencesValidator.java new file mode 100644 index 000000000..e29a18860 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/validators/AudiencesValidator.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.validators; + +import com.nvidia.boot.exceptions.BadRequestException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AudiencesValidator { + + /** + * Validates a list of target-service identifiers used for audience binding. The list must be + * non-empty. + * + * @param audienceServiceIds list of ids to validate. + * + * @return validated list. + */ + public List getValidatedAudiences(List audienceServiceIds) { + if (CollectionUtils.isEmpty(audienceServiceIds)) { + throw new BadRequestException("Audience services must have at least one audience"); + } + return audienceServiceIds; + } + + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/vo/AssertionRequestVo.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/vo/AssertionRequestVo.java new file mode 100644 index 000000000..80780e1d3 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/vo/AssertionRequestVo.java @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.vo; + +import java.util.List; +import java.util.Map; +import org.springframework.security.oauth2.jwt.Jwt; + +/** + * Value object that contains validated parameters for assertion signing. + * + * @param callerToken JWT with which caller was authenticated. + * @param audienceServiceIds unique identifiers of target services (as recognised by the auth provider). + * @param data deserialized JSON payload with assertions. + */ +public record AssertionRequestVo( + Jwt callerToken, + List audienceServiceIds, + Map data) { + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/web/NotaryController.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/web/NotaryController.java new file mode 100644 index 000000000..7d81d955d --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/web/NotaryController.java @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.web; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import com.nimbusds.jwt.SignedJWT; +import com.nvidia.notary.config.PublicKeySet; +import com.nvidia.notary.services.SigningService; +import com.nvidia.notary.validators.AssertionRequestValidator; +import com.nvidia.notary.web.dto.AssertionRequest; +import com.nvidia.notary.web.dto.AssertionResponse; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RefreshScope +@RestController +@RequiredArgsConstructor +public class NotaryController { + + private final SigningService signingService; + private final PublicKeySet publicKeySet; + private final AssertionRequestValidator requestValidator; + + @PreAuthorize("hasAuthority(@notaryProperties.signingScope)") + @Operation(summary = "Sign payload", description = "Returns signed payload.") + @PostMapping(path = "/sign", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.OK) + public AssertionResponse sign( + @RequestBody AssertionRequest requestBody, HttpServletRequest request) { + var validatedRequest = requestValidator.validate(requestBody, request); + SignedJWT signedJWT = signingService.sign(validatedRequest); + return new AssertionResponse(signedJWT.serialize()); + } + + @Operation(summary = "Returns public keys set") + @GetMapping(path = "/.well-known/jwks.json", produces = APPLICATION_JSON_VALUE) + public ResponseEntity getJwks() { + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(60, TimeUnit.MINUTES)) + .body(publicKeySet.getValue()); + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/web/dto/AssertionRequest.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/web/dto/AssertionRequest.java new file mode 100644 index 000000000..fab20842d --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/web/dto/AssertionRequest.java @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.web.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AssertionRequest { + + @JsonProperty("audience_service_ids") + private List audienceServiceIds; + + @JsonProperty("data") + private Map data; +} diff --git a/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/web/dto/AssertionResponse.java b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/web/dto/AssertionResponse.java new file mode 100644 index 000000000..3730c6ab2 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/main/java/com/nvidia/notary/web/dto/AssertionResponse.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AssertionResponse { + + private String assertion; + +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/NotaryTestApp.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/NotaryTestApp.java new file mode 100644 index 000000000..e6fcaad11 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/NotaryTestApp.java @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary; + +import com.nvidia.boot.observability.tracing.cassandra.CassandraTracingAutoConfiguration; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Minimal {@link SpringBootApplication} used only by integration tests in {@code notary-core}. + * The real {@code App} class lives in the {@code notary-service} module and must not be on + * this module's classpath. + * + *

Mirrors the auto-configuration exclusions on the production {@code App} so the test + * context boots the same beans the service does. + */ +@SpringBootApplication(exclude = {CassandraTracingAutoConfiguration.class}) +public class NotaryTestApp { + + public static void main(String[] args) { + SpringApplication.run(NotaryTestApp.class, args); + } +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/config/ConfigurationValidatorTest.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/config/ConfigurationValidatorTest.java new file mode 100644 index 000000000..59d2fb526 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/config/ConfigurationValidatorTest.java @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.config; + +import static com.nvidia.notary.utils.TestData.SIGNING_KEY_ID_1; +import static com.nvidia.notary.utils.TestUtils.assertThrowsExceptionWithMessage; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.when; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Covers the runtime checks {@link ConfigurationValidator} still performs after the field-level + * constraints moved to Jakarta Bean Validation on {@link NotaryProperties}. Specifically, the + * {@code signingKid} → {@link JWKSet} lookup contract: kid must resolve to a present, private key. + * Constraint-level checks (blank/null/positive/URL/audience shape/cross-field) are exercised by + * {@link NotaryPropertiesValidationTest}. + */ +@ExtendWith(MockitoExtension.class) +class ConfigurationValidatorTest { + + @Mock + private NotaryProperties notaryPropertiesMock; + + @Mock + private JWKSet jwkSetMock; + + @Mock + private JWK keyMock; + + @InjectMocks + private ConfigurationValidator validator; + + @Test + void validate_throwsIfSigningKidNotSet() { + when(notaryPropertiesMock.getSigningKid()).thenReturn(null); + + assertThrowsExceptionWithMessage( + IllegalStateException.class, () -> validator.validate(), + "Signing key id is not set."); + } + + @Test + void validate_throwsIfKeySetIsEmpty() { + when(notaryPropertiesMock.getSigningKid()).thenReturn(SIGNING_KEY_ID_1); + when(jwkSetMock.isEmpty()).thenReturn(true); + + assertThrowsExceptionWithMessage( + IllegalStateException.class, () -> validator.validate(), + "Signing key set is empty."); + } + + @Test + void validate_throwsIfKeyNotFoundInTheSet() { + when(notaryPropertiesMock.getSigningKid()).thenReturn(SIGNING_KEY_ID_1); + when(jwkSetMock.isEmpty()).thenReturn(false); + when(jwkSetMock.getKeyByKeyId(SIGNING_KEY_ID_1)).thenReturn(null); + + assertThrowsExceptionWithMessage( + IllegalStateException.class, () -> validator.validate(), + "Signing key 'signing-key-kid-1' not found in key set."); + } + + @Test + void validate_throwsIfKeyNotPrivate() { + when(notaryPropertiesMock.getSigningKid()).thenReturn(SIGNING_KEY_ID_1); + when(jwkSetMock.isEmpty()).thenReturn(false); + when(jwkSetMock.getKeyByKeyId(SIGNING_KEY_ID_1)).thenReturn(keyMock); + when(keyMock.isPrivate()).thenReturn(false); + + assertThrowsExceptionWithMessage( + IllegalStateException.class, () -> validator.validate(), + "Signing key 'signing-key-kid-1' is not private."); + } + + @Test + void validate_pass() { + when(notaryPropertiesMock.getSigningKid()).thenReturn(SIGNING_KEY_ID_1); + when(jwkSetMock.isEmpty()).thenReturn(false); + when(jwkSetMock.getKeyByKeyId(SIGNING_KEY_ID_1)).thenReturn(keyMock); + when(keyMock.isPrivate()).thenReturn(true); + + assertDoesNotThrow(() -> validator.validate()); + } +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/config/NotaryPropertiesValidationTest.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/config/NotaryPropertiesValidationTest.java new file mode 100644 index 000000000..d1df82ee8 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/config/NotaryPropertiesValidationTest.java @@ -0,0 +1,222 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.nimbusds.jose.JWSAlgorithm; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Exercises the Jakarta Bean Validation constraints declared on {@link NotaryProperties}. Spring + * runs these during {@code @ConfigurationProperties} binding; here we run them directly against a + * standalone {@link Validator} so failures are localized to the constraint that fired. + */ +class NotaryPropertiesValidationTest { + + private static ValidatorFactory factory; + private static Validator validator; + + @BeforeAll + static void setUpValidator() { + factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @AfterAll + static void tearDownValidator() { + factory.close(); + } + + /** Build a valid baseline so each test can mutate exactly one field. */ + private static NotaryProperties.NotaryPropertiesBuilder validBuilder() { + return NotaryProperties.builder() + .privateJwks("{\"keys\":[]}") + .signingKid("kid-1") + .signingAlgorithm(JWSAlgorithm.ES256) + .issuerUrl("https://issuer.example") + .signingScope("notary-sign") + .maxAssertionsRequestSize(4096L) + .requireAudience(true) + .requiredAudiences(List.of("s:service-id")); + } + + @Test + void valid_passes() { + Set> violations = validator.validate(validBuilder().build()); + assertThat(violations).isEmpty(); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", "\t"}) + void privateJwks_blankFails(String value) { + assertProperty("privateJwks", b -> b.privateJwks(value)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void signingKid_blankFails(String value) { + assertProperty("signingKid", b -> b.signingKid(value)); + } + + @Test + void signingAlgorithm_nullFails() { + assertProperty("signingAlgorithm", b -> b.signingAlgorithm(null)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", "not a url"}) + void issuerUrl_blankOrInvalidFails(String value) { + assertProperty("issuerUrl", b -> b.issuerUrl(value)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void signingScope_blankFails(String value) { + assertProperty("signingScope", b -> b.signingScope(value)); + } + + @ParameterizedTest + @ValueSource(longs = {-1L, 0L}) + void maxAssertionsRequestSize_nonPositiveFails(long value) { + assertProperty("maxAssertionsRequestSize", b -> b.maxAssertionsRequestSize(value)); + } + + @ParameterizedTest + @MethodSource("invalidAudienceEntries") + void requiredAudiences_invalidElementFails(String badEntry) { + Set> violations = validator.validate( + validBuilder().requiredAudiences(List.of(badEntry)).build()); + assertThat(violations) + .as("violations for entry '%s'", badEntry) + .isNotEmpty(); + assertThat(violations) + .extracting(v -> v.getPropertyPath().toString()) + .anyMatch(p -> p.startsWith("requiredAudiences")); + } + + private static Stream invalidAudienceEntries() { + return Stream.of( + "", // empty + " ", // blank + "s:", // trailing colon + "${UNRESOLVED}", // placeholder leaked through + "s:foo${BAR}" // placeholder embedded + ); + } + + @Test + void requiredAudiences_emptyFailsWhenRequireAudienceTrue() { + Set> violations = validator.validate( + validBuilder().requireAudience(true).requiredAudiences(List.of()).build()); + // The method-level @AssertTrue on isRequiredAudiencesValid fires. + assertThat(violations) + .extracting(v -> v.getPropertyPath().toString()) + .contains("requiredAudiencesValid"); + } + + @Test + void requiredAudiences_emptyPassesWhenRequireAudienceFalse() { + Set> violations = validator.validate( + validBuilder().requireAudience(false).requiredAudiences(List.of()).build()); + assertThat(violations).isEmpty(); + } + + /** + * @NotNull on the field rejects an explicit empty YAML/env binding that Spring's relaxed + * binder would otherwise coerce to {@code null}, leaving downstream consumers exposed to NPEs. + */ + @Test + void requiredAudiences_nullFailsAlways() { + Set> violationsRequiredTrue = validator.validate( + validBuilder().requireAudience(true).requiredAudiences(null).build()); + assertThat(violationsRequiredTrue) + .extracting(v -> v.getPropertyPath().toString()) + .contains("requiredAudiences"); + + Set> violationsRequiredFalse = validator.validate( + validBuilder().requireAudience(false).requiredAudiences(null).build()); + assertThat(violationsRequiredFalse) + .extracting(v -> v.getPropertyPath().toString()) + .contains("requiredAudiences"); + } + + /** + * Audience entries can technically contain a newline (the JWT {@code aud} claim is just a + * string). Without the DOTALL flag, {@code .} doesn't cross newlines and the negative + * lookahead {@code (?!.*\$\{)} would only scan the first line, letting {@code ${...}} on a + * second line slip past. {@code (?s)} pins the regex to match across the whole string. + */ + @Test + void requiredAudiences_multilinePlaceholderIsRejected() { + Set> violations = validator.validate( + validBuilder().requiredAudiences(List.of("s:foo\n${BAR}")).build()); + assertThat(violations) + .as("multi-line ${BAR} should be rejected by the DOTALL-anchored regex") + .isNotEmpty(); + assertThat(violations) + .extracting(v -> v.getPropertyPath().toString()) + .anyMatch(p -> p.startsWith("requiredAudiences")); + } + + /** + * Element-level {@code @Pattern} and the class-level {@code @AssertTrue} are independent — + * an invalid element should still surface even when the cross-field rule is satisfied + * (non-empty list, requireAudience=true). Confirms both layers fire together. + */ + @Test + void requiredAudiences_invalidElementFailsAlongsideCrossFieldRule() { + Set> violations = validator.validate( + validBuilder().requireAudience(true).requiredAudiences(List.of("s:")).build()); + // Element-level @Pattern fires; @AssertTrue passes because the list is non-empty. + assertThat(violations) + .extracting(v -> v.getPropertyPath().toString()) + .anyMatch(p -> p.startsWith("requiredAudiences")) + .noneMatch(p -> p.equals("requiredAudiencesValid")); + } + + private void assertProperty(String expectedProperty, + Consumer mutate) { + NotaryProperties.NotaryPropertiesBuilder b = validBuilder(); + mutate.accept(b); + Set> violations = validator.validate(b.build()); + assertThat(violations) + .as("violations for property %s", expectedProperty) + .isNotEmpty(); + assertThat(violations) + .extracting(v -> v.getPropertyPath().toString()) + .anyMatch(p -> p.equals(expectedProperty) || p.startsWith(expectedProperty)); + } +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/config/TestClockConfiguration.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/config/TestClockConfiguration.java new file mode 100644 index 000000000..163d06bb2 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/config/TestClockConfiguration.java @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.config; + +import com.nvidia.notary.utils.TestClock; +import java.time.Clock; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; + +@Configuration +@Profile({"integrationtest"}) +public class TestClockConfiguration { + + @Bean + @Primary + public Clock getClock() { + return new TestClock(); + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/integration/BaseIntegrationTest.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/integration/BaseIntegrationTest.java new file mode 100644 index 000000000..49887b00b --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/integration/BaseIntegrationTest.java @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.integration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpMethod.GET; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nvidia.boot.mock.oauth2.MockOAuth2TokenServer; +import com.nvidia.boot.mock.oauth2.OAuth2TestUtils; +import com.nvidia.boot.mock.oauth2.OAuth2TokenServerConfigurationProperties; +import com.nvidia.notary.services.JtiGenerator; +import com.nvidia.notary.utils.TestData; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; +import java.util.Map; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.web.context.request.RequestContextHolder; + +public abstract class BaseIntegrationTest { + + protected static MockOAuth2TokenServer oauth2MockServer; + protected static WireMockServer oauth2WireMockServer; + + private static final OAuth2TokenServerConfigurationProperties OAUTH2_TOKEN_SERVER_CONFIGURATION = + new OAuth2TokenServerConfigurationProperties( + "http://xqlnhnfz6tjcz-ashudoahdkz6zrpjttwhaxjhaketm.localhost.local:8081", + "http://xqlnhnfz6tjcz-ashudoahdkz6zrpjttwhaxjhaketm.localhost.local:8081/.well-known/jwks.json", + "ES256", + List.of(), List.of(), null); + + + @Autowired + protected TestRestTemplate restTemplate; + + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + protected String oauth2Issuer; + + @MockitoBean + protected JtiGenerator jtiGeneratorMock; + + @BeforeAll + static void init() { + oauth2MockServer = new MockOAuth2TokenServer(OAUTH2_TOKEN_SERVER_CONFIGURATION); + oauth2WireMockServer = oauth2MockServer.start(); + } + + @AfterAll + static void afterAll() { + oauth2MockServer.stop(); + } + + @BeforeEach + public void initBefore() { + RequestContextHolder.resetRequestAttributes(); + } + + + @SneakyThrows + protected String getAccessToken(List scopes, String sub, String issuer, Instant issuedAt) { + return getAccessTokenWithAudiences( + scopes, + sub, + issuer, + issuedAt, + List.of("s:" + TestData.SERVICE_ID_1, sub, "s:localhost")); + } + + @SneakyThrows + protected String getAccessTokenWithAudiences(List scopes, String sub, String issuer, + Instant issuedAt, List audiences) { + Instant expiresAt = issuedAt.plus(15, ChronoUnit.MINUTES); + var claimsSetBuilder = new JWTClaimsSet.Builder() + .subject(sub) + .issueTime(Date.from(issuedAt)) + .expirationTime(Date.from(expiresAt)) + .claim("scopes", scopes) + .claim("token_type", "service_account") + .claim("service", Map.of( + "id", TestData.SERVICE_ID_1, + "name", "actor-service-name")) + .audience(audiences) + .issuer(issuer) + .claim("azp", sub); + + return OAuth2TestUtils.getJwt(claimsSetBuilder); + } + + protected String getNotaryServicePublicKeys() { + var result = restTemplate.exchange( + "/.well-known/jwks.json", GET, HttpEntity.EMPTY, String.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(result.getBody()).isEqualTo(""" + {"keys":[\ + {"kty":"EC","crv":"P-256","kid":"E4a21067-0089-4c64-b3de-dc76e2f95e7f",\ + "x":"VrKv7bxxmB5F8hS6w8mnVspfY_wASD2AF8sB0biHZZo",\ + "y":"v9Kfxtd9ePO_QpPmaH3Qp8aG4zx_Wz7XamLOhb9KZJk",\ + "alg":"ES256"},\ + {"kty":"EC","crv":"P-256","kid":"f4a21067-0082-4c64-b3de-dc76e2f95e7f",\ + "x":"VrKv7bxxmB5F8hS6w8mnVspfY_wASD2AF8sB0biHZZo",\ + "y":"v9Kfxtd9ePO_QpPmaH3Qp8aG4zx_Wz7XamLOhb9KZJk",\ + "alg":"ES256"}]}"""); + + return result.getBody(); + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/integration/SigningIntegrationTest.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/integration/SigningIntegrationTest.java new file mode 100644 index 000000000..bc69bcf06 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/integration/SigningIntegrationTest.java @@ -0,0 +1,300 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.integration; + + +import static com.nimbusds.jose.JWSAlgorithm.ES256; +import static com.nvidia.notary.utils.TestData.REQUEST_DATA_SERIALIZED; +import static com.nvidia.notary.utils.TestData.SERVICE_ID_1; +import static com.nvidia.notary.utils.TestData.TEST_FIXED_JTI; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.LONG; +import static org.assertj.core.api.InstanceOfAssertFactories.STRING; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpMethod.POST; + +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.ECDSAVerifier; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nvidia.notary.NotaryTestApp; +import com.nvidia.notary.config.TestClockConfiguration; +import com.nvidia.notary.utils.TestClock; +import com.nvidia.notary.web.dto.AssertionResponse; +import jakarta.validation.constraints.NotNull; +import java.security.interfaces.ECPublicKey; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +@Slf4j +@AutoConfigureTestRestTemplate +@SpringBootTest( + classes = {NotaryTestApp.class, TestClockConfiguration.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.active:integrationtest") +class SigningIntegrationTest extends BaseIntegrationTest { + + @AfterEach + void cleanup() { + TestClock.resetToDefaults(); + } + + @Test + void signRejected_invalidScope() { + HttpHeaders headers = getRequestHeadersWithTokenForScope( + "invalid-scope", oauth2Issuer, Instant.now()); + HttpEntity request = new HttpEntity<>(REQUEST_DATA_SERIALIZED, headers); + + var result = restTemplate.exchange("/sign", POST, request, String.class); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(result.getHeaders().get("WWW-Authenticate")).containsExactly( + """ + Bearer error="insufficient_scope", \ + error_description="The request requires higher privileges than provided by the access token.", \ + error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"\ + """); + } + + @Test + void signRejected_expiredToken() { + Instant issuedAt = Instant.now().minus(1, ChronoUnit.DAYS); + HttpHeaders headers = getRequestHeadersWithTokenForScope( + "invalid-scope", oauth2Issuer, issuedAt); + HttpEntity request = new HttpEntity<>(REQUEST_DATA_SERIALIZED, headers); + + var result = restTemplate.exchange("/sign", POST, request, String.class); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + List responseHeaders = result.getHeaders().get("WWW-Authenticate"); + assertThat(responseHeaders).hasSize(1); + assertThat(responseHeaders.getFirst()).matches( + """ + Bearer error="invalid_token", \ + error_description="An error occurred while attempting to decode the Jwt: Jwt expired at \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z", \ + error_uri="https://tools.ietf.org/html/rfc6750#section-3.1".*\ + """); + } + + @Test + void signRejected_invalidIssuer() { + HttpHeaders headers = getRequestHeadersWithTokenForScope( + "notary-test-sign", + "http://invalid-service-id.localhost.local:8081", + Instant.now()); + HttpEntity request = new HttpEntity<>(REQUEST_DATA_SERIALIZED, headers); + + var result = restTemplate.exchange("/sign", POST, request, String.class); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + assertThat(result.getHeaders().get("WWW-Authenticate")) + .hasSize(1) + .first(STRING) + .startsWith("Bearer error=\"invalid_token\", " + + "error_description=\"An error occurred while " + + "attempting to decode the Jwt: The iss claim is not valid\", " + + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""); + } + + @Test + void signRejected_audienceMissingBinding() { + // Token is otherwise valid (correct issuer, valid scope, unexpired, signed by the mock), + // but its audience does NOT contain any entry from notary.required-audiences. + // This should be rejected by the JwtClaimValidator on the aud claim. + Instant now = Instant.now(); + String token = getAccessTokenWithAudiences( + List.of("notary-test-sign"), + "oauth2-client-id", + oauth2Issuer, + now, + List.of("s:some-other-service")); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.add("Authorization", "Bearer " + token); + HttpEntity request = new HttpEntity<>(REQUEST_DATA_SERIALIZED, headers); + + var result = restTemplate.exchange("/sign", POST, request, String.class); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + assertThat(result.getHeaders().get("WWW-Authenticate")) + .first(STRING) + .contains("error=\"invalid_token\""); + } + + @Test + void signRejected_tokenWithoutSignature() { + String tokenWithoutSignature = """ + eyJraWQiOiJiNmEwMWMwMy00ZjM4LTQxY2YtYTA3NC0zYTM0NWYyNzRkMTAiLCJhbGciOiJFUzI1NiJ9.\ + eyJzdWIiOiJzc2EtY2xpZW50LWlkIiwiYXVkIjpbInM6eHFsbmhuZno2dGpjei1hc2h1ZG9haGRrejZ6c\ + nBqdHR3aGF4amhha2V0bSIsInNzYS1jbGllbnQtaWQiLCJzOmxvY2FsaG9zdCJdLCJzZXJ2aWNlIjp7Im\ + 5hbWUiOiJhY3Rvci1zZXJ2aWNlLW5hbWUiLCJpZCI6InhxbG5obmZ6NnRqY3otYXNodWRvYWhka3o2enJ\ + wanR0d2hheGpoYWtldG0ifSwiYXpwIjoic3NhLWNsaWVudC1pZCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhv\ + c3Q6ODA4MSIsInNjb3BlcyI6WyJub3RhcnktdGVzdC1zaWduIl0sImV4cCI6MTY5NjMyMjQyNCwidG9rZ\ + W5fdHlwZSI6InNlcnZpY2VfYWNjb3VudCIsImlhdCI6MTY5NjMyMTUxNH0."""; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_PROBLEM_JSON)); + headers.add("Authorization", "Bearer " + tokenWithoutSignature); + HttpEntity request = new HttpEntity<>(REQUEST_DATA_SERIALIZED, headers); + var result = restTemplate.exchange("/sign", POST, request, String.class); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + assertThat(result.getHeaders().get("WWW-Authenticate")) + .hasSize(1) + .first(STRING) + .startsWith(""" + Bearer error="invalid_token", \ + error_description="An error occurred while \ + attempting to decode the Jwt: Malformed token", \ + error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"\ + """); + } + + @Test + void shouldErrorIfJsonInvalid() { + when(jtiGeneratorMock.generate()).thenReturn(TEST_FIXED_JTI); + + HttpHeaders headers = getRequestHeadersWithTokenForScope( + "notary-test-sign", oauth2Issuer, Instant.now()); + + HttpEntity request = new HttpEntity<>("invalid-input:", headers); + + var result = restTemplate.exchange("/sign", POST, request, String.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(result.getBody()).isEqualTo( + """ + {\ + "detail":"Failed to read request",\ + "instance":"/sign",\ + "status":400,\ + "title":"Bad Request"}\ + """); + } + + @Test + void shouldErrorIfRequestDataInvalid() { + when(jtiGeneratorMock.generate()).thenReturn(TEST_FIXED_JTI); + + HttpHeaders headers = getRequestHeadersWithTokenForScope( + "notary-test-sign", oauth2Issuer, Instant.now()); + + String requestBody = """ + { + "audience_service_ids": ["xqlnhnfz6tjcz-ashudoahdkz6zrpjttwhaxjhaketm"], + "data": {} + } + """; + HttpEntity request = new HttpEntity<>(requestBody, headers); + + var result = restTemplate.exchange("/sign", POST, request, String.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(result.getBody()).isEqualTo( + """ + {\ + "detail":"Request data is empty",\ + "instance":"/sign",\ + "status":400,\ + "title":"Bad Request",\ + "type":"urn:nv-boot:problem-details:bad-request"}\ + """); + + } + + @Test + void shouldSignAssertion() { + when(jtiGeneratorMock.generate()).thenReturn(TEST_FIXED_JTI); + + HttpHeaders headers = getRequestHeadersWithTokenForScope( + "notary-test-sign", + oauth2Issuer, + Instant.now()); + HttpEntity request = new HttpEntity<>(REQUEST_DATA_SERIALIZED, headers); + + var result = restTemplate.exchange("/sign", POST, request, AssertionResponse.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + String assertion = Objects.requireNonNull(result.getBody()).getAssertion(); + var parsedToken = getValidatedToken(assertion); + Map claim = parsedToken.getPayload().toJSONObject(); + assertThat(claim) + .containsEntry("iss", "http://assertion.issuer.test") + .containsEntry("sub", "oauth2-client-id") + .containsEntry("aud", SERVICE_ID_1) + .containsEntry("jti", TEST_FIXED_JTI) + .extractingByKey("iat", LONG) // narrows the assertion type to Long + .isNotNull(); + + + assertThat(claim.get("assertion")).hasToString( + """ + {string=Example String, number=123, boolean=true, null=null, \ + object={nestedString=Nested Example, nestedNumber=456, \ + nestedObject={deepString=Deeply Nested Example, deepArray=[7, 8, 9], \ + deepObject={deeperString=Deeper Level, deeperNumber=101112, \ + deeperObject={deepestString=Deepest Level, deepestList=[10, eleven, true]}}}}, \ + array=[1, two, false, null, {objectInArray=789}], \ + arrayOfObjects=[{id=1, name=Item One}, {id=2, name=Item Two}], \ + specialCharacters=<>&"'/, escapedCharacters=\\t\\n\\r\\b\\f\\"\\\\}"""); + } + + private @NotNull HttpHeaders getRequestHeadersWithTokenForScope( + String scope, String issuer, Instant issuedAt) { + String token = getAccessToken(List.of(scope), "oauth2-client-id", issuer, issuedAt); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.add("Authorization", "Bearer " + token); + headers.add("Content-Type", "application/json"); + return headers; + } + + @SneakyThrows + private SignedJWT getValidatedToken(String signedTokenString) { + String jwkSet = getNotaryServicePublicKeys(); + JWKSet jwks = JWKSet.parse(jwkSet); + JWKSource keySource = new ImmutableJWKSet<>(jwks.toPublicJWKSet()); + ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(new JWSVerificationKeySelector<>(ES256, keySource)); + SignedJWT signedJWT = SignedJWT.parse(signedTokenString); + JWK jwk = jwks.getKeyByKeyId(signedJWT.getHeader().getKeyID()); + ECPublicKey ecPublicKey = ((ECKey) jwk).toECPublicKey(); + JWSVerifier verifier = new ECDSAVerifier(ecPublicKey); + assertThat(signedJWT.verify(verifier)).isTrue(); + return signedJWT; + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/JtiGeneratorTest.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/JtiGeneratorTest.java new file mode 100644 index 000000000..ff0204e72 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/JtiGeneratorTest.java @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.services; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class JtiGeneratorTest { + + @Test + void generate_returnsUniqueValues() { + JtiGenerator generator = new JtiGenerator(); + Set values = new HashSet<>(); + for (int i = 0; i < 100; i++) { + values.add(generator.generate()); + } + assertThat(values).size().isEqualTo(100); + } +} \ No newline at end of file diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/JwtResolverTest.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/JwtResolverTest.java new file mode 100644 index 000000000..892d43d2b --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/JwtResolverTest.java @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.services; + +import static com.nvidia.notary.utils.TestUtils.assertThrowsExceptionWithDetails; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.nvidia.boot.exceptions.ForbiddenException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; + +@ExtendWith(MockitoExtension.class) +class JwtResolverTest { + + @InjectMocks + private JwtResolver jwtResolver; + + @Mock + private Authentication authenticationMock; + + @Mock + private Jwt jwtMock; + + @AfterEach + void cleanup() { + SecurityContextHolder.clearContext(); + } + + @Test + void getCallerToken_throwsIfNoAuthentication() { + SecurityContextHolder.clearContext(); + assertThrowsExceptionWithDetails( + ForbiddenException.class, + () -> jwtResolver.getCallerToken(), + "authentication is not JWT"); + } + + @Test + void getCallerToken_throwsIfPrincipalIsNull() { + SecurityContextHolder.getContext().setAuthentication(authenticationMock); + assertThrowsExceptionWithDetails( + ForbiddenException.class, + () -> jwtResolver.getCallerToken(), + "authentication is not JWT"); + } + + + @Test + void getCallerToken_throwsIfPrincipalIsNotJwt() { + SecurityContextHolder.getContext().setAuthentication(authenticationMock); + when(authenticationMock.getPrincipal()).thenReturn("client-id"); + assertThrowsExceptionWithDetails( + ForbiddenException.class, + () -> jwtResolver.getCallerToken(), + "authentication is not JWT"); + } + + @Test + void getCallerToken_returnsJwt() { + SecurityContextHolder.getContext().setAuthentication(authenticationMock); + when(authenticationMock.getPrincipal()).thenReturn(jwtMock); + assertThat(jwtResolver.getCallerToken()).isEqualTo(jwtMock); + } +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/KeyGeneratorTest.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/KeyGeneratorTest.java new file mode 100644 index 000000000..3af59876d --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/KeyGeneratorTest.java @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.services; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.gen.ECKeyGenerator; +import java.util.Date; +import java.util.UUID; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; + +@Slf4j +public class KeyGeneratorTest { + + /** + * Use this test to generate signing keys. + */ + @Test + public void generateSigningKey() { + ECKey jwk = makeNewEcKey(); + // Output the private key + System.out.println(jwk); + } + + /** + * Use this test to generate key set. + */ + @Test + public void generateInitialKeySet() { + ECKey jwk = makeNewEcKey(); + System.out.printf("{\"keys\":[%s]}%n", jwk); + } + + /** + * Use this test to generate key set. + */ + @Test + public void generateInitialKeySetEscaped() { + ECKey jwk = makeNewEcKey(); + String keySet = "{\"keys\":[%s]}%n".formatted(jwk); + System.out.print(keySet.replaceAll("\"", "\\\\\\\\\\\\\"")); + } + + @SneakyThrows + private static ECKey makeNewEcKey() { + return new ECKeyGenerator(Curve.P_256) + .keyUse(KeyUse.SIGNATURE) + .keyID(UUID.randomUUID().toString()) + .algorithm(JWSAlgorithm.ES256) + .issueTime(new Date()) + .generate(); + + } +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/SigningServiceTest.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/SigningServiceTest.java new file mode 100644 index 000000000..36da7ca9f --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/services/SigningServiceTest.java @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.services; + +import static com.nvidia.notary.utils.TestData.SERVICE_ID_1; +import static com.nvidia.notary.utils.TestData.SERVICE_ID_2; +import static com.nvidia.notary.utils.TestData.TEST_JTI; +import static com.nvidia.notary.utils.TestData.TEST_TIME_DATE; +import static com.nvidia.notary.utils.TestData.TEST_TIME_INSTANT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jwt.SignedJWT; +import com.nvidia.boot.jwt.services.JwtService; +import com.nvidia.notary.config.NotaryProperties; +import com.nvidia.notary.vo.AssertionRequestVo; +import java.text.ParseException; +import java.time.Clock; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.oauth2.jwt.Jwt; + +@ExtendWith(MockitoExtension.class) +class SigningServiceTest { + + @Mock + private JwtService jwtServiceMock; + @Spy + private NotaryProperties notaryPropertiesMock = NotaryProperties.builder() + .signingKid("signing-kid") + .issuerUrl("issuer-url") + .signingAlgorithm(JWSAlgorithm.ES256) + .build(); + @Mock + private Clock clockMock; + @Mock + private JtiGenerator jtiGeneratorMock; + @Mock + private Jwt jwtMock; + @Captor + private ArgumentCaptor signedJWTArgumentCaptor; + + @InjectMocks + private SigningService signingService; + + @Test + void sign() + throws ParseException { + List validServiceIds = List.of(SERVICE_ID_1, SERVICE_ID_2); + Map data = Map.of("key", List.of("v1", "v2")); + + when(clockMock.instant()).thenReturn(TEST_TIME_INSTANT); + when(jtiGeneratorMock.generate()).thenReturn(TEST_JTI); + when(jwtMock.getSubject()).thenReturn("caller-client-id"); + + var validatedRequest = new AssertionRequestVo(jwtMock, validServiceIds, data); + SignedJWT signedJWT = signingService.sign(validatedRequest); + + verify(jwtServiceMock).signJwt(signedJWTArgumentCaptor.capture(), eq("signing-kid")); + assertThat(signedJWT).isEqualTo(signedJWTArgumentCaptor.getValue()); + + assertNotNull(signedJWT); + + assertThat(signedJWT.getJWTClaimsSet().getClaims()).containsExactlyInAnyOrderEntriesOf( + Map.of( + "iss", "issuer-url", + "jti", TEST_JTI, + "sub", "caller-client-id", + "aud", validServiceIds, + "assertion", data, + "iat", TEST_TIME_DATE + )); + assertThat(signedJWT.getHeader().toJSONObject()).containsExactlyInAnyOrderEntriesOf( + Map.of( + "kid", "signing-kid", + "alg", "ES256" + )); + } +} \ No newline at end of file diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/utils/TestClock.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/utils/TestClock.java new file mode 100644 index 000000000..54aa13ced --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/utils/TestClock.java @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.utils; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import lombok.Getter; +import lombok.Setter; + +public class TestClock extends Clock { + + @Setter + private static Clock baseClock = Clock.systemUTC(); + + @Getter + @Setter + private static Duration offset = Duration.ZERO; + + @Override + public ZoneId getZone() { + return baseClock.getZone(); + } + + @Override + public Clock withZone(ZoneId zone) { + return baseClock.withZone(zone); + } + + @Override + public Instant instant() { + return baseClock.instant().plus(offset); + } + + public static void resetToDefaults() { + offset = Duration.ZERO; + baseClock = Clock.systemUTC(); + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/utils/TestData.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/utils/TestData.java new file mode 100644 index 000000000..34d34db88 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/utils/TestData.java @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.utils; + +import java.time.Instant; +import java.util.Date; + +public class TestData { + + public static final String TEST_TIME_STRING = "2023-10-03T08:25:24.00Z"; + public static final Instant TEST_TIME_INSTANT = Instant.parse(TEST_TIME_STRING); + public static final Date TEST_TIME_DATE = Date.from(TEST_TIME_INSTANT); + + public static final String TEST_JTI = "13273c57-75d6-4d9d-9d26-7e337aa9d6e6"; + + public static final String SERVICE_ID_1 = "xqlnhnfz6tjcz-ashudoahdkz6zrpjttwhaxjhaketm"; + public static final String SERVICE_ID_2 = "jkpojec1fc1cfinnsllwmd2qnfaxsu042e-ilpy1koi"; + + public static final String SIGNING_KEY_ID_1 = "signing-key-kid-1"; + + public static final String TEST_FIXED_JTI = "2a280586-1809-4ac6-a75d-58666fe12eda"; + + public static final String REQUEST_DATA_SERIALIZED = """ + { + "audience_service_ids": ["xqlnhnfz6tjcz-ashudoahdkz6zrpjttwhaxjhaketm"], + "data": { + "string": "Example String", + "number": 123, + "boolean": true, + "null": null, + "object": { + "nestedString": "Nested Example", + "nestedNumber": 456, + "nestedObject": { + "deepString": "Deeply Nested Example", + "deepArray": [ + 7, + 8, + 9 + ], + "deepObject": { + "deeperString": "Deeper Level", + "deeperNumber": 101112, + "deeperObject": { + "deepestString": "Deepest Level", + "deepestList": [ + 10, + "eleven", + true + ] + } + } + } + }, + "array": [ + 1, + "two", + false, + null, + { + "objectInArray": 789 + } + ], + "arrayOfObjects": [ + { + "id": 1, + "name": "Item One" + }, + { + "id": 2, + "name": "Item Two" + } + ], + "specialCharacters": "\\u003c\\u003e\\u0026\\u0022\\u0027\\/", + "escapedCharacters": "\\\\t\\\\n\\\\r\\\\b\\\\f\\\\\\"\\\\\\\\" + } + } + """; + + public static final String EXPECTED_ASSERTION_CLAIMS = """ + {\ + "iss":"http://assertion.issuer.test",\ + "sub":"oauth2-client-id",\ + "aud":"xqlnhnfz6tjcz-ashudoahdkz6zrpjttwhaxjhaketm",\ + "assertion":{\ + "string":"Example String",\ + "number":123,\ + "boolean":true,\ + "null":null,\ + "object":{\ + "nestedString":"Nested Example",\ + "nestedNumber":456,\ + "nestedObject":{"deepString":"Deeply Nested Example",\ + "deepArray":[7,8,9],"deepObject":{\ + "deeperString":"Deeper Level","deeperNumber":101112,\ + "deeperObject":{"deepestString":"Deepest Level",\ + "deepestList":[10,"eleven",true]}}}},\ + "array":[1,"two",false,null,{"objectInArray":789}],\ + "arrayOfObjects":[{"id":1,"name":"Item One"},{"id":2,"name":"Item Two"}],\ + "specialCharacters":"<>&\\"'/",\ + "escapedCharacters":"\\\\t\\\\n\\\\r\\\\b\\\\f\\\\\\"\\\\\\\\"},\ + "iat":1696321524,"jti":"2a280586-1809-4ac6-a75d-58666fe12eda"}"""; +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/utils/TestUtils.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/utils/TestUtils.java new file mode 100644 index 000000000..d1fd645a6 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/utils/TestUtils.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.utils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.function.Executable; +import org.springframework.web.ErrorResponseException; + +public class TestUtils { + + public static T assertThrowsExceptionWithDetails( + Class expectedThrowable, Executable executable, String expectedErrorDetails) { + T exception = assertThrows(expectedThrowable, executable); + assertThat(exception.getBody().getDetail()).isEqualTo(expectedErrorDetails); + return exception; + } + + public static T assertThrowsExceptionWithMessage( + Class expectedThrowable, Executable executable, String expectedErrorDetails) { + T exception = assertThrows(expectedThrowable, executable); + assertThat(exception.getMessage()).isEqualTo(expectedErrorDetails); + return exception; + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/validators/AssertionRequestValidatorTest.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/validators/AssertionRequestValidatorTest.java new file mode 100644 index 000000000..1089459e6 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/validators/AssertionRequestValidatorTest.java @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.validators; + +import static com.nvidia.notary.utils.TestData.SERVICE_ID_1; +import static com.nvidia.notary.utils.TestUtils.assertThrowsExceptionWithDetails; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.nvidia.notary.config.NotaryProperties; +import com.nvidia.notary.services.JwtResolver; +import com.nvidia.notary.vo.AssertionRequestVo; +import com.nvidia.notary.web.dto.AssertionRequest; +import com.nvidia.boot.exceptions.BadRequestException; +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.oauth2.jwt.Jwt; + +@ExtendWith(MockitoExtension.class) +class AssertionRequestValidatorTest { + + private static final List VALID_SERVICE_ID_LIST = List.of(SERVICE_ID_1); + private static final Map VALID_DATA = Map.of("key", "value"); + + @Mock + private NotaryProperties notaryPropertiesMock; + @Mock + private AudiencesValidator audiencesValidatorMock; + @Mock + private JwtResolver jwtResolverMock; + @Mock + private HttpServletRequest httpServletRequestMock; + @Mock + private Jwt jwtMock; + @InjectMocks + private AssertionRequestValidator validator; + + @Test + void validate_throwsIfRequestBodyIsNull() { + assertThrowsExceptionWithDetails( + BadRequestException.class, + () -> validator.validate(null, httpServletRequestMock), + "Content length must be greater than zero"); + } + + @Test + void validate_throwsIfServletRequestIsNull() { + AssertionRequest requestBody = new AssertionRequest(); + assertThrowsExceptionWithDetails( + BadRequestException.class, + () -> validator.validate(requestBody, null), + "Content length must be greater than zero"); + } + + @Test + void validate_throwsIfServletRequestContent() { + AssertionRequest requestBody = new AssertionRequest(); + when(httpServletRequestMock.getContentLength()).thenReturn(0); + assertThrowsExceptionWithDetails( + BadRequestException.class, + () -> validator.validate(requestBody, httpServletRequestMock), + "Content length must be greater than zero"); + } + + @Test + void validate_throwsIfServletRequestContentTooLong() { + AssertionRequest requestBody = new AssertionRequest(); + when(httpServletRequestMock.getContentLength()).thenReturn(10000); + when(notaryPropertiesMock.getMaxAssertionsRequestSize()).thenReturn(8192L); + assertThrowsExceptionWithDetails( + BadRequestException.class, + () -> validator.validate(requestBody, httpServletRequestMock), + "Content length is greater than the maximum allowed: 8192"); + } + + @ParameterizedTest + @NullAndEmptySource + void validate_throwsIfDataIsEmpty(Map data) { + AssertionRequest requestBody = new AssertionRequest(VALID_SERVICE_ID_LIST, data); + when(httpServletRequestMock.getContentLength()).thenReturn(1000); + when(notaryPropertiesMock.getMaxAssertionsRequestSize()).thenReturn(8192L); + assertThrowsExceptionWithDetails( + BadRequestException.class, + () -> validator.validate(requestBody, httpServletRequestMock), + "Request data is empty"); + } + + @Test + void validate_pass() { + when(httpServletRequestMock.getContentLength()).thenReturn(1000); + when(notaryPropertiesMock.getMaxAssertionsRequestSize()).thenReturn(8192L); + when(audiencesValidatorMock.getValidatedAudiences(VALID_SERVICE_ID_LIST)) + .thenReturn(VALID_SERVICE_ID_LIST); + when(jwtResolverMock.getCallerToken()).thenReturn(jwtMock); + + AssertionRequest requestBody = new AssertionRequest(VALID_SERVICE_ID_LIST, VALID_DATA); + assertThat(validator.validate(requestBody, httpServletRequestMock)) + .isEqualTo(new AssertionRequestVo(jwtMock, VALID_SERVICE_ID_LIST, VALID_DATA)); + } +} \ No newline at end of file diff --git a/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/validators/AudiencesValidatorTest.java b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/validators/AudiencesValidatorTest.java new file mode 100644 index 000000000..162d3b153 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/java/com/nvidia/notary/validators/AudiencesValidatorTest.java @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary.validators; + +import static com.nvidia.notary.utils.TestData.SERVICE_ID_1; +import static com.nvidia.notary.utils.TestData.SERVICE_ID_2; +import static com.nvidia.notary.utils.TestUtils.assertThrowsExceptionWithDetails; +import static org.assertj.core.api.Assertions.assertThat; + +import com.nvidia.boot.exceptions.BadRequestException; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AudiencesValidatorTest { + + @InjectMocks + private AudiencesValidator validator; + + private static Stream validServicesLists() { + return Stream.of( + Arguments.of(List.of(SERVICE_ID_2)), + Arguments.of(List.of(SERVICE_ID_2, SERVICE_ID_1)) + ); + } + + @MethodSource("validServicesLists") + @ParameterizedTest + void getValidatedAudiences_passForValidAudiences(List audiences) { + assertThat(validator.getValidatedAudiences(audiences)).isEqualTo(audiences); + } + + @ParameterizedTest + @NullAndEmptySource + void getValidatedAudiences_throwsForEmptyAudiences(List audiences) { + assertThrowsExceptionWithDetails( + BadRequestException.class, + () -> validator.getValidatedAudiences(audiences), + "Audience services must have at least one audience"); + } + +} diff --git a/src/control-plane-services/notary/notary-core/src/test/resources/application-integrationtest.yaml b/src/control-plane-services/notary/notary-core/src/test/resources/application-integrationtest.yaml new file mode 100644 index 000000000..8f7c6ff69 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/resources/application-integrationtest.yaml @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +spring: + jmx: + enabled: false + autoconfigure: + exclude: org.springframework.boot.webmvc.autoconfigure.error.ErrorMvcAutoConfiguration + cloud: + discovery: + enabled: false + main: + allow-bean-definition-overriding: true + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://localhost:8081 + jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/.well-known/jwks.json + jws-algorithms: ES256 + web: + error: + whitelabel: + enabled: false + +logging: + level: + root: INFO + com.nvidia: DEBUG + org.springframework.web: DEBUG + org.springframework.security.oauth2.jwt: TRACE + org.springframework.web.servlet.mvc.method.annotation: TRACE + +management: + endpoints: + web: + base-path: /actuator + exposure: + include: health,metrics + jmx: + exposure: + exclude: '*' + domain: localhost + endpoint: + health: + probes: + enabled: true + show-details: always + group: + readiness: + include: + - readinessState + liveness: + include: + - livenessState + health: + livenessState: + enabled: true + readinessState: + enabled: true + vault: + enabled: false + metrics: + enable: + all: true + tags: + env: ${spring.application.env} + micro_service: ${spring.application.name} + host_id: ${spring.application.host.id} + host_dc: ${spring.application.host.dc} + observations: + annotations: + enabled: true + forward-headers-strategy: framework + +notary: + issuer-url: http://assertion.issuer.test + signing-scope: notary-test-sign + private-jwks: ${kv.private-key-jwks.keys} + signing-kid: ${kv.signing-key.kid} + signing-algorithm: ${kv.signing-key.alg} + max-assertions-request-size: 2048 + required-audiences: + - "s:xqlnhnfz6tjcz-ashudoahdkz6zrpjttwhaxjhaketm" + +springdoc: + api-docs: + enabled: false diff --git a/src/control-plane-services/notary/notary-core/src/test/resources/bootstrap-integrationtest.yaml b/src/control-plane-services/notary/notary-core/src/test/resources/bootstrap-integrationtest.yaml new file mode 100644 index 000000000..a113a1f0e --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/resources/bootstrap-integrationtest.yaml @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +spring: + application: + name: notary-service-test + env: default + version: 0.0.0-test + host: + id: ${HOSTNAME:notary-service-test-host} + dc: ${AWS_REGION:notary-service-test-dc} + cloud: + context.aws.enabled: false + +nv-boot: + reloadable-properties: + enabled: true + file: classpath:vault-agent/integration-test-vault.json + poll-duration: 300s + +management: + tracing: + export: + enabled: false + propagation: + type: w3c diff --git a/src/control-plane-services/notary/notary-core/src/test/resources/vault-agent/integration-test-vault.json b/src/control-plane-services/notary/notary-core/src/test/resources/vault-agent/integration-test-vault.json new file mode 100644 index 000000000..5d7b4bb83 --- /dev/null +++ b/src/control-plane-services/notary/notary-core/src/test/resources/vault-agent/integration-test-vault.json @@ -0,0 +1,15 @@ +{ + "kv": { + "tracing": { + "accessToken": "mock-lightstep-token" + }, + "signing-key": { + "kid": "f4a21067-0082-4c64-b3de-dc76e2f95e7f", + "alg": "ES256" + }, + "private-key-jwks": { + "keys": "{ \"keys\":[{\"alg\":\"ES256\",\"kid\":\"E4a21067-0089-4c64-b3de-dc76e2f95e7f\",\"kty\":\"EC\",\"d\":\"lIKIP8YQm352rWF65BckZ0Htt9n24ytnlVnrk3g1Nn8\",\"crv\":\"P-256\",\"x\":\"VrKv7bxxmB5F8hS6w8mnVspfY_wASD2AF8sB0biHZZo\",\"y\":\"v9Kfxtd9ePO_QpPmaH3Qp8aG4zx_Wz7XamLOhb9KZJk\"},{\"alg\":\"ES256\",\"kty\":\"EC\",\"d\":\"lIKIP8YQm352rWF65BckZ0Htt9n24ytnlVnrk3g1Nn8\",\"crv\":\"P-256\",\"kid\":\"f4a21067-0082-4c64-b3de-dc76e2f95e7f\",\"x\":\"VrKv7bxxmB5F8hS6w8mnVspfY_wASD2AF8sB0biHZZo\",\"y\":\"v9Kfxtd9ePO_QpPmaH3Qp8aG4zx_Wz7XamLOhb9KZJk\"}]}" + } + } +} + diff --git a/src/control-plane-services/notary/notary-service/BUILD.bazel b/src/control-plane-services/notary/notary-service/BUILD.bazel new file mode 100644 index 000000000..3cc767f30 --- /dev/null +++ b/src/control-plane-services/notary/notary-service/BUILD.bazel @@ -0,0 +1,59 @@ +load("//rules/java:defs.bzl", "nv_boot_library", "nv_boot_library_test") +load("//rules/java:spring.bzl", "spring_boot_app") + +package(default_visibility = ["//visibility:public"]) + +NOTARY_SERVICE_SRCS = glob(["src/main/java/**/*.java"]) + +NOTARY_SERVICE_RESOURCES = glob(["src/main/resources/**"]) + +NOTARY_SERVICE_TEST_SRCS = glob(["src/test/java/**/*.java"]) + +nv_boot_library( + name = "app_classes", + srcs = NOTARY_SERVICE_SRCS, + deps = [ + "//src/control-plane-services/notary/notary-core:notary_core", + "//src/libraries/java/nv-boot-parent/nv-boot-starter-observability:nv_boot_starter_observability", + "@nv_third_party_deps//:org_slf4j_slf4j_api", + "@nv_third_party_deps//:org_springframework_boot_spring_boot", + "@nv_third_party_deps//:org_springframework_boot_spring_boot_autoconfigure", + "@nv_third_party_deps//:org_springframework_boot_spring_boot_micrometer_tracing", + "@nv_third_party_deps//:org_springframework_spring_context", + ], + resource_strip_prefix = "src/control-plane-services/notary/notary-service/src/main/resources", + resources = NOTARY_SERVICE_RESOURCES, + runtime_deps = [ + "@nv_third_party_deps//:org_springframework_boot_spring_boot_jarmode_tools", + ], + visibility = ["//visibility:private"], +) + +spring_boot_app( + name = "app", + application = ":app_classes", + artifact_id = "notary-service-oss", + artifact_name = "Notary Service OSS", + artifact_version = "0.0.1-SNAPSHOT", + description = "NVCF Notary OSS executable", + group_id = "com.nvidia.notary", + main_class = "com.nvidia.notary.App", +) + +nv_boot_library_test( + name = "tests", + coverage_library = ":app_classes", + srcs = NOTARY_SERVICE_TEST_SRCS, + deps = [ + ":app_classes", + "//src/control-plane-services/notary/notary-core:notary_core", + "//src/control-plane-services/notary/notary-core:notary_core_test_fixtures", + "@nv_third_party_deps//:org_springframework_boot_spring_boot", + "@nv_third_party_deps//:org_springframework_boot_spring_boot_resttestclient", + "@nv_third_party_deps//:org_springframework_boot_spring_boot_starter_webmvc_test", + "@nv_third_party_deps//:org_springframework_boot_spring_boot_web_server", + "@nv_third_party_deps//:org_springframework_spring_beans", + "@nv_third_party_deps//:org_springframework_spring_web", + ], + timeout = "long", +) diff --git a/src/control-plane-services/notary/notary-service/Dockerfile b/src/control-plane-services/notary/notary-service/Dockerfile new file mode 100644 index 000000000..c4ca6d6f7 --- /dev/null +++ b/src/control-plane-services/notary/notary-service/Dockerfile @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Pull base image. +FROM nvcr.io/nvidia/distroless/java:25-jdk-v4.0.5 + +# Add the service jar +ARG APP_JAR=notary-service/target/app.jar +COPY ${APP_JAR} /usr/share/app.jar + +# enable setting the ulimit to max on boot +ENV ULIMIT_FLAG=1 + +# use this instead of JAVA_OPTS +ENV JDK_JAVA_OPTIONS="" + +# set profile one of "local", "stage", "prod", "ncp" +ENV SPRING_PROFILES_ACTIVE="" + +# The full OAuth2 issuer URI Spring Security validates inbound caller JWTs against. +ENV AUTH_TOKEN_ISSUER="" + +# Used to configure the iss value included in the assertions produced by the Notary Service +ENV ASSERTION_ISSUER_URL="" + +# The Notary Service will accept only OAuth2 tokens with the specified scope +ENV AUTH_TOKEN_SCOPE="notary-sign" + +# Used to control the maximum size of the assertions produced by the Notary Service +ENV MAX_REQUEST_BODY_SIZE_BYTES=4096 + +# Overrides where service is looking for secrets file prepared by vault +ENV VAULT_SECRETS_JSON_PATH=vault/vault-secrets.json + +WORKDIR /home/app +CMD ["java", "-jar", "/usr/share/app.jar"] diff --git a/src/control-plane-services/notary/notary-service/pom.xml b/src/control-plane-services/notary/notary-service/pom.xml new file mode 100644 index 000000000..e17b6dd4d --- /dev/null +++ b/src/control-plane-services/notary/notary-service/pom.xml @@ -0,0 +1,108 @@ + + + + 4.0.0 + + + com.nvidia.notary + notary-api + 0.0.1-SNAPSHOT + + + notary-service-oss + jar + Notary Service OSS + + NVCF Notary — OSS executable + + + + com.nvidia.notary.App + + + + + com.nvidia.notary + notary-core + ${project.version} + + + + + + + + com.nvidia.notary + notary-core + ${project.version} + test-jar + tests + test + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + com.vaadin.external.google + android-json + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.vaadin.external.google + android-json + + + + + + + app + + + src/main/resources + true + + + + + org.springframework.boot + spring-boot-maven-plugin + + + io.github.git-commit-id + git-commit-id-maven-plugin + + + ${project.parent.basedir}/.git + + + + + diff --git a/src/control-plane-services/notary/notary-service/src/main/java/com/nvidia/notary/App.java b/src/control-plane-services/notary/notary-service/src/main/java/com/nvidia/notary/App.java new file mode 100644 index 000000000..04a63258d --- /dev/null +++ b/src/control-plane-services/notary/notary-service/src/main/java/com/nvidia/notary/App.java @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary; + +import com.nvidia.boot.observability.tracing.cassandra.CassandraTracingAutoConfiguration; +import com.nvidia.notary.config.ConfigurationValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.micrometer.tracing.autoconfigure.prometheus.PrometheusExemplarsAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; + +// CassandraTracingAutoConfiguration is shipped by nv-boot-starter-observability 1.9.0+ and +// references com.datastax.oss:java-driver-core. Notary does not use Cassandra; excluding it +// avoids NoClassDefFoundError on RequestTracker when Spring enhances the @Configuration class. +@SpringBootApplication( + exclude = {PrometheusExemplarsAutoConfiguration.class, CassandraTracingAutoConfiguration.class}) +public class App { + + private static final Logger log = LoggerFactory.getLogger(App.class); + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(App.class, args); + var configValidator = context.getBean(ConfigurationValidator.class); + try { + configValidator.validate(); + } catch (IllegalStateException e) { + // ConfigurationValidator.validate() reports invalid signing keys, scope, issuer URL, + // and audience binding via IllegalStateException — see notary-core ConfigurationValidator. + log.error(e.getMessage()); + SpringApplication.exit(context, () -> 1); + } + } +} diff --git a/src/control-plane-services/notary/notary-service/src/main/resources/application-local.yaml b/src/control-plane-services/notary/notary-service/src/main/resources/application-local.yaml new file mode 100644 index 000000000..7d0df805f --- /dev/null +++ b/src/control-plane-services/notary/notary-service/src/main/resources/application-local.yaml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +spring: + application: + version: local + +notary: + # Local dev defaults to issuer-only validation so developers don't have to wire + # an explicit audience list to boot. Set NOTARY_REQUIRE_AUDIENCE=true + # (and NOTARY_REQUIRED_AUDIENCES_0=...) when you want to exercise the + # audience-check path locally. + require-audience: false + required-audiences: [] + +logging: + level: + root: INFO + org.springframework.security.web.access: DEBUG + org.springframework.web.HttpLogging: DEBUG + org.springframework.web.servlet.mvc: TRACE diff --git a/src/control-plane-services/notary/notary-service/src/main/resources/application-ncp.yaml b/src/control-plane-services/notary/notary-service/src/main/resources/application-ncp.yaml new file mode 100644 index 000000000..d5b7e771a --- /dev/null +++ b/src/control-plane-services/notary/notary-service/src/main/resources/application-ncp.yaml @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +spring: + application: + version: ncp + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://api.nvcf.svc.cluster.local + jwk-set-uri: http://openbao-server.vault-system.svc:8200/v1/services/nvcf-api/jwt/jwks + +logging: + level: + root: WARN + +notary: + issuer-url: http://notary.nvcf.svc.cluster.local:8080 + signing-scope: ${AUTH_TOKEN_SCOPE:notary-sign} + # NCP does not bind tokens by audience — the issuer check alone is sufficient. + # require-audience=false opts out of the default fail-closed behavior, allowing + # required-audiences to be empty. + require-audience: false + required-audiences: [] diff --git a/src/control-plane-services/notary/notary-service/src/main/resources/application.yaml b/src/control-plane-services/notary/notary-service/src/main/resources/application.yaml new file mode 100644 index 000000000..5971ee755 --- /dev/null +++ b/src/control-plane-services/notary/notary-service/src/main/resources/application.yaml @@ -0,0 +1,129 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#Spring Config +spring: + jmx: + enabled: false + autoconfigure: + exclude: org.springframework.boot.webmvc.autoconfigure.error.ErrorMvcAutoConfiguration + cloud: + discovery: + enabled: false + security: + oauth2: + resourceserver: + jwt: + # Full OAuth2 issuer URI Spring Security validates inbound caller JWTs + # against. Operators set this directly via env. No hostname suffix is + # templated by this image — the OSS schema is provider-agnostic. + issuer-uri: ${AUTH_TOKEN_ISSUER:} + jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/.well-known/jwks.json + jws-algorithms: ES256 + web: + error: + whitelabel: + enabled: false + +logging: + level: + root: INFO + com.nvidia.notary: INFO + +management: + endpoints: + web: + base-path: /actuator + exposure: + include: "*" + jmx: + exposure: + exclude: '*' + domain: localhost + endpoint: + health: + probes: + enabled: true + show-details: always + group: + readiness: + include: + - readinessState + liveness: + include: + - livenessState + health: + livenessState: + enabled: true + readinessState: + enabled: true + vault: + enabled: false + metrics: + enable: + all: true + tags: + env: ${spring.application.env} + micro_service: ${spring.application.name} + service_version: ${spring.application.version} + host_id: ${spring.application.host.id} + host_dc: ${spring.application.host.dc} + distribution: + percentiles-histogram: + "[http.server.requests]": true + slo: + "[http.server.requests]": "5ms,10ms,25ms,50ms,75ms,100ms,200ms,300ms,400ms,500ms,750ms,1s,2s,3s,4s,5s,10s,15s,20s,30s" + observations: + annotations: + enabled: true + enable: + spring.security: false + prometheus: + metrics: + export: + enabled: true + server: + port: 8181 + +server: + port: 8080 + compression: + enabled: true + mime-types: application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css + forward-headers-strategy: framework + tomcat: + basedir: /tmp/tomcat + accesslog: + enabled: true + pattern: '%{X-Forwarded-For}i %l %u %t "%r" %s %b "%{User-Agent}i" "%{Referer}i" %D' + buffered: false + +notary: + issuer-url: ${ASSERTION_ISSUER_URL} + signing-scope: ${AUTH_TOKEN_SCOPE:notary-sign} + private-jwks: ${kv.private-key-jwks.keys} + signing-kid: ${kv.signing-key.kid} + signing-algorithm: ${kv.signing-key.alg} + max-assertions-request-size: ${MAX_REQUEST_BODY_SIZE_BYTES:4096} + # Audiences the caller's JWT must carry in its `aud` claim. At least one entry must + # match for the token to be accepted. Set literal values via env (indexed list + # binding: `NOTARY_REQUIRED_AUDIENCES_0=...`, `..._1=...`, ...) or via + # a profile YAML override. Empty list disables the audience check — the issuer + # check alone is sufficient (NCP uses this). + required-audiences: [] + +springdoc: + api-docs: + enabled: false diff --git a/src/control-plane-services/notary/notary-service/src/main/resources/bootstrap-local.yaml b/src/control-plane-services/notary/notary-service/src/main/resources/bootstrap-local.yaml new file mode 100644 index 000000000..f7756b57c --- /dev/null +++ b/src/control-plane-services/notary/notary-service/src/main/resources/bootstrap-local.yaml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +spring: + application: + env: local + +nv-boot: + reloadable-properties: + poll-duration: 15s + +management: + tracing: + enabled: false + otlp: + tracing: + endpoint: http://localhost:8360/v1/traces diff --git a/src/control-plane-services/notary/notary-service/src/main/resources/bootstrap-ncp.yaml b/src/control-plane-services/notary/notary-service/src/main/resources/bootstrap-ncp.yaml new file mode 100644 index 000000000..c8765964e --- /dev/null +++ b/src/control-plane-services/notary/notary-service/src/main/resources/bootstrap-ncp.yaml @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +spring: + application: + env: ncp + +nv-boot: + reloadable-properties: + file: file:/home/app/vault/secrets.json + +management: + tracing: + enabled: false diff --git a/src/control-plane-services/notary/notary-service/src/main/resources/bootstrap.yaml b/src/control-plane-services/notary/notary-service/src/main/resources/bootstrap.yaml new file mode 100644 index 000000000..61f4cf3dd --- /dev/null +++ b/src/control-plane-services/notary/notary-service/src/main/resources/bootstrap.yaml @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +spring: + application: + name: nvcf-notary-service + env: default + # Satisfies nv-boot-starter-core's ValidateEnvironmentPostProcessor during the bootstrap + # phase (where application-{profile}.yaml has not yet been loaded). BootCoreEnvironmentPostProcessor + # overrides this with git.closest.tag.name / git.build.version / git.commit.id.abbrev from + # classpath:git.properties when present; profile YAMLs may also override. + version: 0.0.0-unknown + host: + id: ${HOSTNAME:${spring.application.name}-${random.int}-host} + dc: ${AWS_REGION:${spring.application.name}-${random.uuid}-dc} + cloud: + context.aws.enabled: false + +nv-boot: + reloadable-properties: + enabled: true + file: ${VAULT_SECRETS_JSON_PATH:vault-agent/secrets/vault-secrets.json} + poll-duration: 300s + +management: + tracing: + propagation: + type: w3c + export: + enabled: false diff --git a/src/control-plane-services/notary/notary-service/src/test/java/com/nvidia/notary/NotaryServiceIntegrationTest.java b/src/control-plane-services/notary/notary-service/src/test/java/com/nvidia/notary/NotaryServiceIntegrationTest.java new file mode 100644 index 000000000..682ab408f --- /dev/null +++ b/src/control-plane-services/notary/notary-service/src/test/java/com/nvidia/notary/NotaryServiceIntegrationTest.java @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.notary; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.web.client.RestTemplate; + +/** + * OSS executable ({@link App} + {@code notary-core}) smoke test. Boots the real service + * application context under the shared {@code integrationtest} profile — its signing keys + * (vault-agent fixture) and OAuth2 issuer config are sourced from the {@code notary-core} + * test-jar — and asserts the service serves its public JWKS and reports healthy. Parallels + * the {@code nvcf-service} and {@code nvct-service} integration smoke tests. + * + *

No OAuth2 token server is required: the asserted endpoints are unauthenticated + * ({@code permitAll}) and the {@code JwtDecoder} fetches the JWK set lazily, so the context + * boots without an issuer reachable on the wire. + */ +@SpringBootTest( + classes = App.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.active=integrationtest") +@AutoConfigureTestRestTemplate +class NotaryServiceIntegrationTest { + + @Autowired + private TestRestTemplate testRestTemplate; + + @LocalManagementPort + private int managementPort; + + @Test + void jwksEndpointReturnsOk() { + var response = testRestTemplate.exchange( + RequestEntity.get(URI.create("/.well-known/jwks.json")).build(), + String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).contains("keys"); + } + + @Test + void actuatorHealthReturnsOk() { + var endpoint = URI.create("http://localhost:" + managementPort + "/actuator/health"); + var response = new RestTemplate().exchange(RequestEntity.get(endpoint).build(), String.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).containsIgnoringCase("status"); + } +} diff --git a/src/control-plane-services/notary/notice_metadata.json b/src/control-plane-services/notary/notice_metadata.json new file mode 100644 index 000000000..515655738 --- /dev/null +++ b/src/control-plane-services/notary/notice_metadata.json @@ -0,0 +1,131 @@ +{ + "artifacts": { + "io.micrometer:micrometer-registry-prometheus:1.16.6": { + "licenses": [ + "The Apache Software License, Version 2.0" + ], + "name": "micrometer-registry-prometheus", + "url": "https://github.com/micrometer-metrics/micrometer" + }, + "io.prometheus:prometheus-metrics-config:1.4.3": { + "licenses": [ + "The Apache Software License, Version 2.0" + ], + "name": "Prometheus Metrics Config", + "url": "http://github.com/prometheus/client_java" + }, + "io.prometheus:prometheus-metrics-core:1.4.3": { + "licenses": [ + "The Apache Software License, Version 2.0" + ], + "name": "Prometheus Metrics Core", + "url": "http://github.com/prometheus/client_java" + }, + "io.prometheus:prometheus-metrics-exposition-formats:1.4.3": { + "licenses": [ + "The Apache Software License, Version 2.0" + ], + "name": "Prometheus Metrics Exposition Formats", + "url": "http://github.com/prometheus/client_java" + }, + "io.prometheus:prometheus-metrics-exposition-textformats:1.4.3": { + "licenses": [ + "The Apache Software License, Version 2.0" + ], + "name": "Prometheus Metrics Exposition Text Formats", + "url": "http://github.com/prometheus/client_java" + }, + "io.prometheus:prometheus-metrics-model:1.4.3": { + "licenses": [ + "The Apache Software License, Version 2.0" + ], + "name": "Prometheus Metrics Model", + "url": "http://github.com/prometheus/client_java" + }, + "io.prometheus:prometheus-metrics-tracer-common:1.4.3": { + "licenses": [ + "The Apache Software License, Version 2.0" + ], + "name": "Prometheus Metrics Tracer Common", + "url": "http://github.com/prometheus/client_java" + }, + "org.apache.commons:commons-collections4:4.5.0": { + "licenses": [ + "Apache-2.0" + ], + "name": "Apache Commons Collections", + "url": "https://commons.apache.org/proper/commons-collections/" + }, + "org.apache.tomcat.embed:tomcat-embed-core:11.0.22": { + "licenses": [ + "Apache License, Version 2.0" + ], + "name": "tomcat-embed-core", + "url": "https://tomcat.apache.org/" + }, + "org.apache.tomcat.embed:tomcat-embed-websocket:11.0.22": { + "licenses": [ + "Apache License, Version 2.0" + ], + "name": "tomcat-embed-websocket", + "url": "https://tomcat.apache.org/" + }, + "org.aspectj:aspectjweaver:1.9.25.1": { + "licenses": [ + "Eclipse Public License - v 2.0" + ], + "name": "AspectJ Weaver", + "url": "https://www.eclipse.org/aspectj/" + }, + "org.springframework.boot:spring-boot-jarmode-tools:4.0.7": { + "licenses": [ + "Apache License, Version 2.0" + ], + "name": "spring-boot-jarmode-tools", + "url": "https://spring.io/projects/spring-boot" + }, + "org.springframework.boot:spring-boot-restclient:4.0.7": { + "licenses": [ + "Apache License, Version 2.0" + ], + "name": "spring-boot-restclient", + "url": "https://spring.io/projects/spring-boot" + }, + "org.springframework.boot:spring-boot-starter-aspectj:4.0.7": { + "licenses": [ + "Apache License, Version 2.0" + ], + "name": "spring-boot-starter-aspectj", + "url": "https://spring.io/projects/spring-boot" + }, + "org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.7": { + "licenses": [ + "Apache License, Version 2.0" + ], + "name": "spring-boot-starter-tomcat-runtime", + "url": "https://spring.io/projects/spring-boot" + }, + "org.springframework.boot:spring-boot-starter-tomcat:4.0.7": { + "licenses": [ + "Apache License, Version 2.0" + ], + "name": "spring-boot-starter-tomcat", + "url": "https://spring.io/projects/spring-boot" + }, + "org.springframework.boot:spring-boot-starter-webmvc:4.0.7": { + "licenses": [ + "Apache License, Version 2.0" + ], + "name": "spring-boot-starter-webmvc", + "url": "https://spring.io/projects/spring-boot" + }, + "org.springframework.boot:spring-boot-tomcat:4.0.7": { + "licenses": [ + "Apache License, Version 2.0" + ], + "name": "spring-boot-tomcat", + "url": "https://spring.io/projects/spring-boot" + } + }, + "generated_by": "tools/bazel/java/generate_notice.py --update-metadata" +} diff --git a/src/control-plane-services/notary/pom.xml b/src/control-plane-services/notary/pom.xml new file mode 100644 index 000000000..ba4b57858 --- /dev/null +++ b/src/control-plane-services/notary/pom.xml @@ -0,0 +1,81 @@ + + + + 4.0.0 + + + com.nvidia.boot + nv-boot-parent + 2.0.1 + + + + com.nvidia.notary + notary-api + 0.0.1-SNAPSHOT + pom + + Notary API + NVCF Notary — aggregator for core library and OSS executable. + + + + + ${project.basedir} + 2.0.1 + + + + + + com.nvidia.boot + nv-boot-bom + ${nv-boot.version} + pom + import + + + + + + notary-core + notary-service + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + 120 + ${root.dir} + + + + + diff --git a/src/libraries/java/nv-boot-parent/NOTICE b/src/libraries/java/nv-boot-parent/NOTICE index 400531af2..c641877d2 100644 --- a/src/libraries/java/nv-boot-parent/NOTICE +++ b/src/libraries/java/nv-boot-parent/NOTICE @@ -1,6 +1,5 @@ -Lists of 208 third-party dependencies. - (Public Domain) AOP alliance (aopalliance:aopalliance:1.0 - http://aopalliance.sourceforge.net) +Lists of 207 third-party dependencies. (Apache License, Version 2.0) LZ4 Java Compression (at.yawk.lz4:lz4-java:1.10.3 - https://github.com/yawkat/lz4-java) (EPL-2.0) (LGPL-2.1-only) Logback Classic Module (ch.qos.logback:logback-classic:1.5.34 - http://logback.qos.ch) (EPL-2.0) (LGPL-2.1-only) Logback Core Module (ch.qos.logback:logback-core:1.5.34 - http://logback.qos.ch) @@ -37,7 +36,7 @@ Lists of 208 third-party dependencies. (The Apache Software License, Version 2.0) okio (com.squareup.okio:okio-jvm:3.16.1 - https://github.com/square/okio/) (Apache-2.0) config (com.typesafe:config:1.4.1 - https://github.com/lightbend/config) (Apache-2.0) Apache Commons Codec (commons-codec:commons-codec:1.19.0 - https://commons.apache.org/proper/commons-codec/) - (Apache-2.0) Apache Commons Logging (commons-logging:commons-logging:1.3.6 - https://commons.apache.org/proper/commons-logging/) + (Apache-2.0) Apache Commons Logging (commons-logging:commons-logging:1.4.0 - https://commons.apache.org/proper/commons-logging/) (The Apache Software License, Version 2.0) CloudEvents - API (io.cloudevents:cloudevents-api:4.1.1 - https://cloudevents.github.io/sdk-java/) (The Apache Software License, Version 2.0) CloudEvents - Core (io.cloudevents:cloudevents-core:4.1.1 - https://cloudevents.github.io/sdk-java/) (The Apache Software License, Version 2.0) CloudEvents - JSON Jackson (io.cloudevents:cloudevents-json-jackson:4.1.1 - https://cloudevents.github.io/sdk-java/) diff --git a/src/libraries/java/nv-boot-parent/notice_metadata.json b/src/libraries/java/nv-boot-parent/notice_metadata.json index a84cb5b29..6e8078f45 100644 --- a/src/libraries/java/nv-boot-parent/notice_metadata.json +++ b/src/libraries/java/nv-boot-parent/notice_metadata.json @@ -302,7 +302,7 @@ "name": "Apache Commons Codec", "url": "https://commons.apache.org/proper/commons-codec/" }, - "commons-logging:commons-logging:1.3.6": { + "commons-logging:commons-logging:1.4.0": { "licenses": [ "Apache-2.0" ], diff --git a/src/libraries/java/nv-boot-parent/nv-boot-starter-observability/BUILD.bazel b/src/libraries/java/nv-boot-parent/nv-boot-starter-observability/BUILD.bazel index a78a84da5..beb76f0cb 100644 --- a/src/libraries/java/nv-boot-parent/nv-boot-starter-observability/BUILD.bazel +++ b/src/libraries/java/nv-boot-parent/nv-boot-starter-observability/BUILD.bazel @@ -10,6 +10,7 @@ OBSERVABILITY_REQUIRED_DEPS = [ "@nv_third_party_deps//:io_micrometer_micrometer_tracing", "@nv_third_party_deps//:io_micrometer_micrometer_tracing_bridge_otel", "@nv_third_party_deps//:io_opentelemetry_opentelemetry_api", + "@nv_third_party_deps//:io_opentelemetry_opentelemetry_exporter_otlp", "@nv_third_party_deps//:io_opentelemetry_opentelemetry_sdk_common", "@nv_third_party_deps//:io_opentelemetry_opentelemetry_sdk_trace", "@nv_third_party_deps//:io_opentelemetry_semconv_opentelemetry_semconv",