@hoanghainh/komagu 2.5.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/ai-run-stats.json +87 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/auth-model.md +120 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/index.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/injection-001.json +383 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/injection-002.json +205 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/privilege-001.json +838 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/privilege-002.json +124 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-001.json +778 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-002.json +546 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-003.json +661 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-004.json +659 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-005.json +612 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/batches/routes-006.json +106 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/coverage.json +19 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-001.json +516 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-002.json +332 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-003.json +218 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-004.json +561 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-005.json +316 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/bola-routes-006.json +14 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/inj-injection-001.json +782 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/inj-injection-002.json +418 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/priv-privilege-001.json +627 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/priv-privilege-002.json +253 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-1.json +68 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-10.json +68 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-11.json +61 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-12.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-13.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-14.json +66 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-15.json +69 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-16.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-17.json +61 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-18.json +69 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-19.json +64 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-2.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-20.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-21.json +61 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-22.json +50 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-3.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-4.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-5.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-6.json +70 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-7.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-8.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings/verified-9.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/findings.json +4857 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/report.ja.md +2112 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/report.md +2112 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/score.json +82 -0
- package/assess/lsa/examples/eval-run-6/gptoss-a/stack-profile.json +225 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/ai-run-stats.json +91 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/auth-model.md +120 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/index.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/injection-001.json +383 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/injection-002.json +205 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/privilege-001.json +838 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/privilege-002.json +124 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-001.json +778 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-002.json +546 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-003.json +661 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-004.json +659 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-005.json +612 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/batches/routes-006.json +106 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/coverage.json +21 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-001.json +593 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-002.json +367 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-003.json +207 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-004.json +477 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-005.json +341 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/bola-routes-006.json +14 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/inj-injection-001.json +821 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/inj-injection-002.json +410 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/priv-privilege-001.json +649 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/priv-privilege-002.json +222 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-1.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-10.json +67 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-11.json +68 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-12.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-13.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-14.json +64 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-15.json +70 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-16.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-17.json +61 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-18.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-19.json +65 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-2.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-20.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-21.json +62 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-22.json +50 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-3.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-4.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-5.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-6.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-7.json +64 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-8.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings/verified-9.json +60 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/findings.json +4966 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/score.json +87 -0
- package/assess/lsa/examples/eval-run-6/gptoss-b/stack-profile.json +225 -0
- package/assess/lsa/examples/eval-run-6/ground-truth.json +851 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/ai-run-stats.json +96 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/auth-model.md +191 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/index.json +60 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/injection-001.json +383 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/injection-002.json +205 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/privilege-001.json +838 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/privilege-002.json +124 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-001.json +778 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-002.json +546 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-003.json +661 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-004.json +659 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-005.json +612 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/batches/routes-006.json +106 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/coverage.json +31 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-001.json +527 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-002.json +568 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-003.json +467 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-004.json +342 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-005.json +693 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/bola-routes-006.json +16 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/inj-injection-001.json +1123 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/inj-injection-002.json +391 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/priv-privilege-001.json +726 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/priv-privilege-002.json +306 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-1.json +137 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-10.json +106 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-11.json +81 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-12.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-13.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-14.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-15.json +110 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-16.json +116 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-17.json +141 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-18.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-19.json +140 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-2.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-20.json +82 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-21.json +74 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-22.json +99 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-3.json +140 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-4.json +145 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-5.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-6.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-7.json +87 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-8.json +125 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings/verified-9.json +135 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/findings.json +6989 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/memory.log +36 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/score.json +106 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-nothink/stack-profile.json +225 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/auth-model.md +517 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-001.json +5 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-002.json +428 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-003.json +388 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-004.json +463 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-005.json +5 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/bola-routes-006.json +67 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings/inj-injection-001.json +964 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/findings.partial.json +1872 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/memory.log +194 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/score.partial.json +108 -0
- package/assess/lsa/examples/eval-run-6/qwen-16k-partial/stack-profile.json +225 -0
- package/assess/lsa/scripts/laravel_routes.py +102 -0
- package/assess/lsa/scripts/map_routes.py +4 -1
- package/assess/lsa/tests/EVAL-RESULTS.md +126 -0
- package/assess/lsa/tests/test_eval_run_6.py +104 -0
- package/assess/lsa/tests/test_laravel_routes.py +95 -0
- package/examples/member-portal/backend-dotnet/Auth/DemoAuthenticationHandler.cs +75 -0
- package/examples/member-portal/backend-dotnet/Program.cs +1 -72
- package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/Application.kt +4 -0
- package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/controller/Controllers.kt +3 -0
- package/examples/member-portal/backend-spring/src/main/kotlin/io/komagu/portal/model/Entities.kt +4 -0
- package/examples/member-portal/tests/test_d6_demo_app.py +4 -4
- package/examples/member-portal/tests/test_d6_remediation_verification.py +6 -6
- package/operate/ai/client.py +27 -10
- package/operate/ai/runner.py +4 -2
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/scan.py +8 -1
- package/operate/cross_user_tester/runner.py +4 -1
- package/operate/devsecops/sbom.py +12 -4
- package/operate/simulate/scenarios.py +11 -2
- package/operate/tests/test_ai_reasoning_effort.py +65 -0
- package/operate/tests/test_ai_route_inventory.py +53 -0
- package/operate/tests/test_audit_minimum_versions.py +89 -0
- package/operate/tests/test_cli.py +1 -1
- package/operate/tests/test_scheme_and_xml_guards.py +67 -0
- package/package.json +1 -1
- package/remediate/authz/dotnet/src/Komagu.Security.Authorization/LoggingSecurityAuditSink.cs +19 -3
- package/remediate/authz/golang/authz_test.go +5 -2
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/authorization/DefaultKomaguSecurityManager.kt +74 -78
- package/remediate/authz/spring/src/main/kotlin/io/komagu/security/spring/OwnershipAspect.kt +129 -158
- package/remediate/deps/patcher.py +2 -1
- package/remediate/edge/mock-backend/app.py +2 -1
- package/remediate/edge/tests/test_edge_security.py +5 -5
- package/remediate/ekyc/requirements.txt +1 -1
- package/remediate/identity/dotnet/src/Komagu.Security.Identity/OidcServiceCollectionExtensions.cs +19 -6
- package/remediate/identity/python/requirements.txt +2 -2
- package/remediate/identity/spring/src/main/kotlin/io/komagu/security/identity/KomaguIdentityAutoConfiguration.kt +31 -38
- package/remediate/legacy_shims/tests/test_legacy_shims.py +8 -8
- package/remediate/query/dotnet/src/Komagu.Security.Query/SafeSortAllowList.cs +5 -2
- package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/SafeSortAllowList.kt +10 -11
- package/remediate/query/spring/src/main/kotlin/io/komagu/security/query/detekt/StringBuiltSqlQueryRule.kt +19 -24
|
@@ -39,93 +39,85 @@ class DefaultKomaguSecurityManager(
|
|
|
39
39
|
tenantIdExtractor: ((T) -> String)?
|
|
40
40
|
): AuthorizationDecision {
|
|
41
41
|
val authentication = SecurityContextHolder.getContext().authentication
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
emitDenialAudit(authentication, resource, operation, reason)
|
|
46
|
-
return AuthorizationDecision.denied(reason)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
val currentUserId = try {
|
|
50
|
-
getCurrentUserId(authentication)
|
|
51
|
-
} catch (ex: Exception) {
|
|
52
|
-
emitDenialAudit(authentication, resource, operation, ex.message ?: "Invalid user identity")
|
|
53
|
-
return AuthorizationDecision.denied(ex.message ?: "Invalid user identity")
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
val currentTenantId = try {
|
|
57
|
-
getCurrentTenantId(authentication)
|
|
58
|
-
} catch (ex: Exception) {
|
|
59
|
-
emitDenialAudit(authentication, resource, operation, ex.message ?: "Invalid tenant identity")
|
|
60
|
-
return AuthorizationDecision.denied(ex.message ?: "Invalid tenant identity")
|
|
42
|
+
val denial = findDenial(authentication, resource, ownerUserIdExtractor, tenantIdExtractor)
|
|
43
|
+
?: return AuthorizationDecision.granted()
|
|
44
|
+
if (denial.audited) {
|
|
45
|
+
emitDenialAudit(authentication, resource, operation, denial.reason, denial.resourceId)
|
|
61
46
|
}
|
|
47
|
+
return AuthorizationDecision.denied(denial.reason)
|
|
48
|
+
}
|
|
62
49
|
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
/** Why the caller may not act on [resource], or null when it may. Checks run in order; the first failure wins. */
|
|
51
|
+
private fun <T> findDenial(
|
|
52
|
+
authentication: Authentication?,
|
|
53
|
+
resource: T?,
|
|
54
|
+
ownerUserIdExtractor: ((T) -> String)?,
|
|
55
|
+
tenantIdExtractor: ((T) -> String)?
|
|
56
|
+
): Denial? {
|
|
57
|
+
val caller = authentication?.takeIf { it.isAuthenticated && it !is AnonymousAuthenticationToken }
|
|
58
|
+
val userId = caller?.let { runCatching { getCurrentUserId(it) } }
|
|
59
|
+
val tenantId = caller?.let { runCatching { getCurrentTenantId(it) } }
|
|
60
|
+
return when {
|
|
61
|
+
userId == null || tenantId == null -> Denial("Caller is unauthenticated")
|
|
62
|
+
userId.isFailure -> Denial(userId.exceptionOrNull()?.message ?: "Invalid user identity")
|
|
63
|
+
tenantId.isFailure -> Denial(tenantId.exceptionOrNull()?.message ?: "Invalid tenant identity")
|
|
64
|
+
resource == null -> Denial("Resource is null", audited = false)
|
|
65
|
+
else -> ownershipDenial(
|
|
66
|
+
resourceIdentity(resource, ownerUserIdExtractor, tenantIdExtractor),
|
|
67
|
+
userId.getOrThrow(),
|
|
68
|
+
tenantId.getOrThrow()
|
|
69
|
+
)
|
|
65
70
|
}
|
|
71
|
+
}
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
private fun ownershipDenial(identity: ResourceIdentity, currentUserId: String, currentTenantId: String): Denial? {
|
|
74
|
+
val owner = identity.ownerUserId?.trim()
|
|
75
|
+
val tenant = identity.tenantId?.trim()
|
|
76
|
+
val reason = when {
|
|
77
|
+
owner.isNullOrEmpty() || tenant.isNullOrEmpty() ->
|
|
78
|
+
"Target resource does not expose ownerUserId and tenantId"
|
|
79
|
+
// 1. Tenant boundary check
|
|
80
|
+
tenant != currentTenantId ->
|
|
81
|
+
"Tenant boundary violation: current tenant '$currentTenantId' != resource tenant '$tenant'"
|
|
82
|
+
// 2. User ownership check
|
|
83
|
+
owner != currentUserId ->
|
|
84
|
+
"User ownership violation: current user '$currentUserId' != resource owner '$owner'"
|
|
85
|
+
else -> null
|
|
86
|
+
}
|
|
87
|
+
return reason?.let { Denial(it, resourceId = identity.id) }
|
|
88
|
+
}
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
/** Owner, tenant and id from [OwnedResource], the extractors, then getters as a fallback. */
|
|
91
|
+
private fun <T> resourceIdentity(
|
|
92
|
+
resource: T & Any,
|
|
93
|
+
ownerUserIdExtractor: ((T) -> String)?,
|
|
94
|
+
tenantIdExtractor: ((T) -> String)?
|
|
95
|
+
): ResourceIdentity {
|
|
96
|
+
val owned = resource as? OwnedResource<*>
|
|
97
|
+
val identity = ResourceIdentity(
|
|
98
|
+
ownerUserId = ownerUserIdExtractor?.invoke(resource) ?: owned?.ownerUserId,
|
|
99
|
+
tenantId = tenantIdExtractor?.invoke(resource) ?: owned?.tenantId,
|
|
100
|
+
id = owned?.id?.toString()
|
|
101
|
+
)
|
|
102
|
+
if (identity.ownerUserId != null && identity.tenantId != null) {
|
|
103
|
+
return identity
|
|
83
104
|
}
|
|
84
|
-
|
|
85
105
|
// Reflection fallback if not implementing OwnedResource directly
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
val tenantField = clazz.methods.firstOrNull { it.name == "getTenantId" || it.name == "tenantId" }
|
|
91
|
-
val idField = clazz.methods.firstOrNull { it.name == "getId" || it.name == "id" }
|
|
92
|
-
|
|
93
|
-
resourceOwnerUserId = resourceOwnerUserId ?: ownerField?.invoke(resource)?.toString()
|
|
94
|
-
resourceTenantId = resourceTenantId ?: tenantField?.invoke(resource)?.toString()
|
|
95
|
-
resourceId = resourceId ?: idField?.invoke(resource)?.toString()
|
|
96
|
-
} catch (_: Exception) {
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
val trimmedOwner = resourceOwnerUserId?.trim()
|
|
101
|
-
val trimmedTenant = resourceTenantId?.trim()
|
|
102
|
-
|
|
103
|
-
if (trimmedOwner.isNullOrEmpty() || trimmedTenant.isNullOrEmpty()) {
|
|
104
|
-
val reason = "Target resource does not expose ownerUserId and tenantId"
|
|
105
|
-
emitDenialAudit(authentication, resource, operation, reason, resourceId)
|
|
106
|
-
return AuthorizationDecision.denied(reason)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 1. Tenant boundary check
|
|
110
|
-
if (trimmedTenant != currentTenantId) {
|
|
111
|
-
val reason = "Tenant boundary violation: current tenant '$currentTenantId' != resource tenant '$trimmedTenant'"
|
|
112
|
-
emitDenialAudit(authentication, resource, operation, reason, resourceId)
|
|
113
|
-
return AuthorizationDecision.denied(reason)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// 2. User ownership check
|
|
117
|
-
if (trimmedOwner != currentUserId) {
|
|
118
|
-
val reason = "User ownership violation: current user '$currentUserId' != resource owner '$trimmedOwner'"
|
|
119
|
-
emitDenialAudit(authentication, resource, operation, reason, resourceId)
|
|
120
|
-
return AuthorizationDecision.denied(reason)
|
|
106
|
+
fun read(vararg names: String): String? = try {
|
|
107
|
+
resource::class.java.methods.firstOrNull { it.name in names }?.invoke(resource)?.toString()
|
|
108
|
+
} catch (_: Exception) {
|
|
109
|
+
null
|
|
121
110
|
}
|
|
122
|
-
|
|
123
|
-
|
|
111
|
+
return ResourceIdentity(
|
|
112
|
+
ownerUserId = identity.ownerUserId ?: read("getOwnerUserId", "ownerUserId"),
|
|
113
|
+
tenantId = identity.tenantId ?: read("getTenantId", "tenantId"),
|
|
114
|
+
id = identity.id ?: read("getId", "id")
|
|
115
|
+
)
|
|
124
116
|
}
|
|
125
117
|
|
|
126
118
|
override fun getCurrentUserId(authentication: Authentication?): String {
|
|
127
119
|
val auth = authentication ?: SecurityContextHolder.getContext().authentication
|
|
128
|
-
?:
|
|
120
|
+
?: error("No authentication present in security context")
|
|
129
121
|
|
|
130
122
|
when (val principal = auth.principal) {
|
|
131
123
|
is Jwt -> {
|
|
@@ -141,12 +133,12 @@ class DefaultKomaguSecurityManager(
|
|
|
141
133
|
return name
|
|
142
134
|
}
|
|
143
135
|
|
|
144
|
-
|
|
136
|
+
error("Verified user identity claim missing in token context")
|
|
145
137
|
}
|
|
146
138
|
|
|
147
139
|
override fun getCurrentTenantId(authentication: Authentication?): String {
|
|
148
140
|
val auth = authentication ?: SecurityContextHolder.getContext().authentication
|
|
149
|
-
?:
|
|
141
|
+
?: error("No authentication present in security context")
|
|
150
142
|
|
|
151
143
|
when (val principal = auth.principal) {
|
|
152
144
|
is Jwt -> {
|
|
@@ -166,7 +158,7 @@ class DefaultKomaguSecurityManager(
|
|
|
166
158
|
}
|
|
167
159
|
}
|
|
168
160
|
|
|
169
|
-
|
|
161
|
+
error("Verified tenant identity claim missing in token context")
|
|
170
162
|
}
|
|
171
163
|
|
|
172
164
|
private fun <T> emitDenialAudit(
|
|
@@ -195,4 +187,8 @@ class DefaultKomaguSecurityManager(
|
|
|
195
187
|
// Auditing failures must never alter the denial outcome
|
|
196
188
|
}
|
|
197
189
|
}
|
|
190
|
+
|
|
191
|
+
private class Denial(val reason: String, val resourceId: String? = null, val audited: Boolean = true)
|
|
192
|
+
|
|
193
|
+
private class ResourceIdentity(val ownerUserId: String?, val tenantId: String?, val id: String?)
|
|
198
194
|
}
|
|
@@ -49,6 +49,7 @@ class OwnershipAspect(
|
|
|
49
49
|
joinPoint: ProceedingJoinPoint,
|
|
50
50
|
requireOwnership: RequireOwnership
|
|
51
51
|
): Any? {
|
|
52
|
+
val operation = requireOwnership.operation
|
|
52
53
|
val targetClass = if (requireOwnership.entityClass != Any::class) {
|
|
53
54
|
requireOwnership.entityClass.java
|
|
54
55
|
} else null
|
|
@@ -58,83 +59,21 @@ class OwnershipAspect(
|
|
|
58
59
|
} catch (_: Exception) {
|
|
59
60
|
null
|
|
60
61
|
}
|
|
61
|
-
val
|
|
62
|
-
val paramNames = signature?.parameterNames ?: emptyArray()
|
|
63
|
-
val args = joinPoint.args ?: emptyArray()
|
|
64
|
-
|
|
65
|
-
var preInvocationChecked = false
|
|
66
|
-
val isMutation = requireOwnership.operation == ResourceOperation.UPDATE ||
|
|
67
|
-
requireOwnership.operation == ResourceOperation.DELETE
|
|
62
|
+
val args: Array<Any?> = joinPoint.args ?: emptyArray()
|
|
68
63
|
|
|
69
64
|
// 1. Determine if an ID parameter is present for ID-based resolution
|
|
70
|
-
|
|
71
|
-
val
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} else if (paramNames.isEmpty()) {
|
|
75
|
-
// Fallback ONLY when parameter names are not preserved in bytecode (no -parameters flag):
|
|
76
|
-
if (args.size == 1 && (args[0] is Number || args[0] is String || args[0] is java.util.UUID)) {
|
|
77
|
-
idArg = args[0]
|
|
78
|
-
} else if (targetClass != null) {
|
|
79
|
-
idArg = args.firstOrNull { it is Number || it is String || it is java.util.UUID }
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (idArg != null) {
|
|
84
|
-
// ID-addressed operation: The resource MUST be resolved server-side from the ID.
|
|
85
|
-
// Client-supplied body objects MUST NEVER be used as the authorization subject.
|
|
86
|
-
if (targetClass != null) {
|
|
87
|
-
val resolver = resolvers.find { it.supports(targetClass) }
|
|
88
|
-
?: throw ResourceNotFoundException(
|
|
89
|
-
"No ResourceOwnershipResolver found for entity ${targetClass.simpleName} to resolve id '$idArg'"
|
|
90
|
-
)
|
|
91
|
-
val resolved = resolver.resolve(idArg)
|
|
92
|
-
?: throw ResourceNotFoundException("Resource of type ${targetClass.simpleName} with id '$idArg' not found")
|
|
93
|
-
|
|
94
|
-
securityManager.requireOwnership(resolved, requireOwnership.operation)
|
|
95
|
-
|
|
96
|
-
// A bound object of the entity type must describe the same resource: same id when it
|
|
97
|
-
// carries one, and no change of owner or tenant (else the method could act on body.id).
|
|
98
|
-
for (arg in args) {
|
|
99
|
-
if (arg == null || arg === idArg) continue
|
|
100
|
-
if ((arg is OwnedResource<*> || targetClass.isInstance(arg)) &&
|
|
101
|
-
!describesSameResource(arg, resolved, idArg)
|
|
102
|
-
) {
|
|
103
|
-
throw ResourceNotFoundException(
|
|
104
|
-
"Argument of type ${arg.javaClass.simpleName} does not match resolved ${targetClass.simpleName} '$idArg'"
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
preInvocationChecked = true
|
|
110
|
-
} else {
|
|
111
|
-
// targetClass not specified but id is present: fail-closed
|
|
112
|
-
throw ResourceNotFoundException(
|
|
113
|
-
"Cannot resolve resource by ID '$idArg' because entityClass is not specified on @RequireOwnership"
|
|
114
|
-
)
|
|
115
|
-
}
|
|
65
|
+
val idArg = findIdArg(signature?.parameterNames ?: emptyArray(), args, requireOwnership.idParam, targetClass)
|
|
66
|
+
val preInvocationChecked = if (idArg != null) {
|
|
67
|
+
verifyById(idArg, args, targetClass, operation)
|
|
68
|
+
true
|
|
116
69
|
} else {
|
|
117
|
-
|
|
118
|
-
// missing id means a misconfigured idParam: never authorize a client-supplied object instead.
|
|
119
|
-
if (isMutation) {
|
|
120
|
-
throw ResourceNotFoundException(
|
|
121
|
-
"Access denied: '${requireOwnership.operation}' requires the '${requireOwnership.idParam}' parameter to resolve the resource server-side"
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
// Non-ID operation (e.g. creation or batch operations): inspect input parameters for OwnedResource / targetClass instances
|
|
125
|
-
for (i in args.indices) {
|
|
126
|
-
val arg = args[i] ?: continue
|
|
127
|
-
if (arg is OwnedResource<*> || (targetClass != null && targetClass.isInstance(arg))) {
|
|
128
|
-
securityManager.requireOwnership(arg, requireOwnership.operation)
|
|
129
|
-
preInvocationChecked = true
|
|
130
|
-
}
|
|
131
|
-
}
|
|
70
|
+
verifyInputs(args, targetClass, requireOwnership)
|
|
132
71
|
}
|
|
133
72
|
|
|
134
73
|
// 2. Pre-mutation gate: for UPDATE / DELETE, fail closed before proceed() if no resource was verified
|
|
135
|
-
if (isMutation && !preInvocationChecked) {
|
|
136
|
-
|
|
137
|
-
"Access denied: mutating operation '$
|
|
74
|
+
if (isMutation(operation) && !preInvocationChecked) {
|
|
75
|
+
deny(
|
|
76
|
+
"Access denied: mutating operation '$operation' requires pre-invocation ownership verification, but no valid resource could be resolved"
|
|
138
77
|
)
|
|
139
78
|
}
|
|
140
79
|
|
|
@@ -142,108 +81,140 @@ class OwnershipAspect(
|
|
|
142
81
|
val result = joinPoint.proceed()
|
|
143
82
|
|
|
144
83
|
// 4. Inspect return value for READ operations
|
|
145
|
-
|
|
146
|
-
val extractedResources = unwrapValue(result)
|
|
147
|
-
for (res in extractedResources) {
|
|
148
|
-
if (res is OwnedResource<*> || (targetClass != null && targetClass.isInstance(res))) {
|
|
149
|
-
securityManager.requireOwnership(res, requireOwnership.operation)
|
|
150
|
-
postInvocationChecked = true
|
|
151
|
-
}
|
|
152
|
-
}
|
|
84
|
+
val postInvocationChecked = verifyOwned(unwrapValue(result), targetClass, operation)
|
|
153
85
|
|
|
154
86
|
// 5. Fail-closed: if neither input nor output could be verified, deny access
|
|
155
87
|
if (!preInvocationChecked && !postInvocationChecked) {
|
|
156
|
-
|
|
157
|
-
"Access denied: @RequireOwnership enforced on '${method?.name ?: "method"}' but no resource was verified"
|
|
88
|
+
deny(
|
|
89
|
+
"Access denied: @RequireOwnership enforced on '${signature?.method?.name ?: "method"}' but no resource was verified"
|
|
158
90
|
)
|
|
159
91
|
}
|
|
160
92
|
|
|
161
93
|
return result
|
|
162
94
|
}
|
|
163
95
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
96
|
+
/**
|
|
97
|
+
* ID-addressed operation: the resource MUST be resolved server-side from the ID.
|
|
98
|
+
* Client-supplied body objects MUST NEVER be used as the authorization subject.
|
|
99
|
+
*/
|
|
100
|
+
private fun verifyById(idArg: Any, args: Array<Any?>, targetClass: Class<*>?, operation: ResourceOperation) {
|
|
101
|
+
// targetClass not specified but id is present: fail-closed
|
|
102
|
+
val type = targetClass ?: deny(
|
|
103
|
+
"Cannot resolve resource by ID '$idArg' because entityClass is not specified on @RequireOwnership"
|
|
104
|
+
)
|
|
105
|
+
val resolver = resolvers.find { it.supports(type) } ?: deny(
|
|
106
|
+
"No ResourceOwnershipResolver found for entity ${type.simpleName} to resolve id '$idArg'"
|
|
107
|
+
)
|
|
108
|
+
val resolved = resolver.resolve(idArg)
|
|
109
|
+
?: deny("Resource of type ${type.simpleName} with id '$idArg' not found")
|
|
110
|
+
|
|
111
|
+
securityManager.requireOwnership(resolved, operation)
|
|
112
|
+
|
|
113
|
+
// A bound object of the entity type must describe the same resource: same id when it
|
|
114
|
+
// carries one, and no change of owner or tenant (else the method could act on body.id).
|
|
115
|
+
val mismatch = args.firstOrNull { arg ->
|
|
116
|
+
arg != null && arg !== idArg &&
|
|
117
|
+
(arg is OwnedResource<*> || type.isInstance(arg)) &&
|
|
118
|
+
!describesSameResource(arg, resolved, idArg)
|
|
119
|
+
}
|
|
120
|
+
if (mismatch != null) {
|
|
121
|
+
deny("Argument of type ${mismatch.javaClass.simpleName} does not match resolved ${type.simpleName} '$idArg'")
|
|
168
122
|
}
|
|
169
|
-
for (field in listOf("ownerUserId", "tenantId")) {
|
|
170
|
-
val claimed = readProperty(candidate, field)?.toString()
|
|
171
|
-
if (!claimed.isNullOrEmpty() && claimed != readProperty(resolved, field)?.toString()) {
|
|
172
|
-
return false
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return true
|
|
176
123
|
}
|
|
177
124
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
val getter = "get" + name.replaceFirstChar { it.uppercase() }
|
|
188
|
-
return try {
|
|
189
|
-
target.javaClass.getMethod(getter).invoke(target)
|
|
190
|
-
} catch (_: Exception) {
|
|
191
|
-
null
|
|
125
|
+
/** No id under the configured parameter name: check owned objects passed in. Returns true if one was verified. */
|
|
126
|
+
private fun verifyInputs(args: Array<Any?>, targetClass: Class<*>?, requireOwnership: RequireOwnership): Boolean {
|
|
127
|
+
// Mutations are id-addressed by definition, so a missing id means a misconfigured idParam:
|
|
128
|
+
// never authorize a client-supplied object instead.
|
|
129
|
+
if (isMutation(requireOwnership.operation)) {
|
|
130
|
+
deny(
|
|
131
|
+
"Access denied: '${requireOwnership.operation}' requires the '${requireOwnership.idParam}' parameter to resolve the resource server-side"
|
|
132
|
+
)
|
|
192
133
|
}
|
|
134
|
+
// Non-ID operation (e.g. creation or batch operations): inspect input parameters for OwnedResource / targetClass instances
|
|
135
|
+
return verifyOwned(args.filterNotNull(), targetClass, requireOwnership.operation)
|
|
193
136
|
}
|
|
194
137
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (className.endsWith("Page") || className.endsWith("Slice")) {
|
|
240
|
-
try {
|
|
241
|
-
val contentMethod = value.javaClass.getMethod("getContent")
|
|
242
|
-
val content = contentMethod.invoke(value)
|
|
243
|
-
return unwrapValue(content)
|
|
244
|
-
} catch (_: Exception) {}
|
|
138
|
+
/** Requires ownership of every owned value; returns true if at least one was checked. */
|
|
139
|
+
private fun verifyOwned(values: List<Any>, targetClass: Class<*>?, operation: ResourceOperation): Boolean {
|
|
140
|
+
val owned = values.filter { it is OwnedResource<*> || (targetClass != null && targetClass.isInstance(it)) }
|
|
141
|
+
owned.forEach { securityManager.requireOwnership(it, operation) }
|
|
142
|
+
return owned.isNotEmpty()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private fun isMutation(operation: ResourceOperation): Boolean =
|
|
147
|
+
operation == ResourceOperation.UPDATE || operation == ResourceOperation.DELETE
|
|
148
|
+
|
|
149
|
+
private fun isIdValue(value: Any?): Boolean = value is Number || value is String || value is java.util.UUID
|
|
150
|
+
|
|
151
|
+
private fun findIdArg(paramNames: Array<String>, args: Array<Any?>, idParam: String, targetClass: Class<*>?): Any? {
|
|
152
|
+
val idParamIndex = paramNames.indexOf(idParam)
|
|
153
|
+
return when {
|
|
154
|
+
idParamIndex != -1 && idParamIndex < args.size -> args[idParamIndex]
|
|
155
|
+
paramNames.isNotEmpty() -> null
|
|
156
|
+
// Fallback ONLY when parameter names are not preserved in bytecode (no -parameters flag):
|
|
157
|
+
args.size == 1 && isIdValue(args[0]) -> args[0]
|
|
158
|
+
targetClass != null -> args.firstOrNull { isIdValue(it) }
|
|
159
|
+
else -> null
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Deny by default: every refusal is a 404, so callers cannot probe which resources exist. */
|
|
164
|
+
private fun deny(reason: String): Nothing = throw ResourceNotFoundException(reason)
|
|
165
|
+
|
|
166
|
+
private fun describesSameResource(candidate: Any, resolved: Any, idArg: Any): Boolean {
|
|
167
|
+
val candidateId = readProperty(candidate, "id")?.toString()
|
|
168
|
+
val sameId = candidateId.isNullOrEmpty() || candidateId == idArg.toString()
|
|
169
|
+
return sameId && listOf("ownerUserId", "tenantId").all { field ->
|
|
170
|
+
val claimed = readProperty(candidate, field)?.toString()
|
|
171
|
+
claimed.isNullOrEmpty() || claimed == readProperty(resolved, field)?.toString()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private fun readProperty(target: Any, name: String): Any? {
|
|
176
|
+
if (target is OwnedResource<*>) {
|
|
177
|
+
return when (name) {
|
|
178
|
+
"id" -> target.id
|
|
179
|
+
"ownerUserId" -> target.ownerUserId
|
|
180
|
+
"tenantId" -> target.tenantId
|
|
181
|
+
else -> null
|
|
245
182
|
}
|
|
246
|
-
// Default single value
|
|
247
|
-
return listOf(value)
|
|
248
183
|
}
|
|
184
|
+
val getter = "get" + name.replaceFirstChar { it.uppercase() }
|
|
185
|
+
return try {
|
|
186
|
+
target.javaClass.getMethod(getter).invoke(target)
|
|
187
|
+
} catch (_: Exception) {
|
|
188
|
+
null
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private fun unwrapValue(value: Any?): List<Any> = when (value) {
|
|
193
|
+
null -> emptyList()
|
|
194
|
+
is java.util.Optional<*> -> if (value.isPresent) unwrapValue(value.get()) else emptyList()
|
|
195
|
+
// Iterable (e.g., List, Set)
|
|
196
|
+
is Iterable<*> -> value.filterNotNull().flatMap { unwrapValue(it) }
|
|
197
|
+
is Array<*> -> value.filterNotNull().flatMap { unwrapValue(it) }
|
|
198
|
+
// Map values
|
|
199
|
+
is Map<*, *> -> value.values.filterNotNull().flatMap { unwrapValue(it) }
|
|
200
|
+
// Java Stream: convert to a list first, then recursively unwrap each element.
|
|
201
|
+
is java.util.stream.Stream<*> -> value.filter { it != null }.toList().flatMap { unwrapValue(it) }
|
|
202
|
+
// Reactor Mono would need Reactor on the classpath; it is not unwrapped.
|
|
203
|
+
is java.util.concurrent.CompletableFuture<*> -> unwrapValue(value.get())
|
|
204
|
+
else -> unwrapContainer(value)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** ResponseEntity body and Spring Data Page / Slice content, read by reflection; anything else is a single value. */
|
|
208
|
+
private fun unwrapContainer(value: Any): List<Any> {
|
|
209
|
+
val className = value.javaClass.name
|
|
210
|
+
val accessor = when {
|
|
211
|
+
className == "org.springframework.http.ResponseEntity" -> "getBody"
|
|
212
|
+
className.endsWith("Page") || className.endsWith("Slice") -> "getContent"
|
|
213
|
+
else -> null
|
|
214
|
+
}
|
|
215
|
+
// Any failure, including one while unwrapping the inner value, keeps the container as a single value.
|
|
216
|
+
val inner = accessor?.let { name ->
|
|
217
|
+
runCatching { unwrapValue(value.javaClass.getMethod(name).invoke(value)) }.getOrNull()
|
|
218
|
+
}
|
|
219
|
+
return inner ?: listOf(value)
|
|
249
220
|
}
|
|
@@ -202,7 +202,8 @@ class CVEAdvisoryCatalog:
|
|
|
202
202
|
)
|
|
203
203
|
results: List[CVEAdvisoryEntry] = []
|
|
204
204
|
try:
|
|
205
|
-
|
|
205
|
+
# Fixed https://api.osv.dev URL; no caller-supplied scheme.
|
|
206
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # nosec B310
|
|
206
207
|
data = json.loads(resp.read().decode("utf-8"))
|
|
207
208
|
for vuln in data.get("vulns", []):
|
|
208
209
|
vuln_id = vuln.get("id", "OSV-UNKNOWN")
|
|
@@ -63,7 +63,8 @@ class EchoHandler(BaseHTTPRequestHandler):
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def run(server_class=HTTPServer, handler_class=EchoHandler, port=5000):
|
|
66
|
-
|
|
66
|
+
# Runs inside the edge test container; Kong reaches it over the Docker network.
|
|
67
|
+
server_address = ("0.0.0.0", port) # nosec B104
|
|
67
68
|
httpd = server_class(server_address, handler_class)
|
|
68
69
|
print(f"Mock upstream backend listening on port {port}...")
|
|
69
70
|
httpd.serve_forever()
|
|
@@ -186,7 +186,7 @@ class TestMockBackendTraffic(unittest.TestCase):
|
|
|
186
186
|
"X-Custom-Header": "safe-value-unredacted",
|
|
187
187
|
}
|
|
188
188
|
)
|
|
189
|
-
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
189
|
+
with urllib.request.urlopen(req, timeout=3) as resp: # nosec B310 # local test server or KOMAGU_GATEWAY_URL under test
|
|
190
190
|
self.assertEqual(resp.status, 200)
|
|
191
191
|
data = json.loads(resp.read().decode("utf-8"))
|
|
192
192
|
|
|
@@ -388,7 +388,7 @@ class TestCorazaWafContainer(unittest.TestCase):
|
|
|
388
388
|
data = form.encode("ascii")
|
|
389
389
|
req = urllib.request.Request(f"http://127.0.0.1:{cls.port}{path}", data=data, headers=headers)
|
|
390
390
|
try:
|
|
391
|
-
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
391
|
+
with urllib.request.urlopen(req, timeout=5) as resp: # nosec B310 # local test server or KOMAGU_GATEWAY_URL under test
|
|
392
392
|
return resp.status
|
|
393
393
|
except urllib.error.HTTPError as e:
|
|
394
394
|
return e.code
|
|
@@ -505,7 +505,7 @@ class TestLiveEdgeSecurity(unittest.TestCase):
|
|
|
505
505
|
def setUpClass(cls):
|
|
506
506
|
try:
|
|
507
507
|
req = urllib.request.Request(f"{cls.BASE_URL}/api/v1/health")
|
|
508
|
-
with urllib.request.urlopen(req, timeout=1) as resp:
|
|
508
|
+
with urllib.request.urlopen(req, timeout=1) as resp: # nosec B310 # local test server or KOMAGU_GATEWAY_URL under test
|
|
509
509
|
cls.live_available = True
|
|
510
510
|
except Exception:
|
|
511
511
|
cls.live_available = False
|
|
@@ -516,7 +516,7 @@ class TestLiveEdgeSecurity(unittest.TestCase):
|
|
|
516
516
|
|
|
517
517
|
def test_live_legitimate_request_returns_200(self):
|
|
518
518
|
req = urllib.request.Request(f"{self.BASE_URL}/api/v1/orders")
|
|
519
|
-
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
519
|
+
with urllib.request.urlopen(req, timeout=3) as resp: # nosec B310 # local test server or KOMAGU_GATEWAY_URL under test
|
|
520
520
|
self.assertEqual(resp.status, 200)
|
|
521
521
|
self.assertIn("X-Correlation-ID", resp.headers)
|
|
522
522
|
|
|
@@ -524,7 +524,7 @@ class TestLiveEdgeSecurity(unittest.TestCase):
|
|
|
524
524
|
sqli_url = f"{self.BASE_URL}/api/v1/orders?id=1%27+OR+1%3D1--"
|
|
525
525
|
req = urllib.request.Request(sqli_url)
|
|
526
526
|
try:
|
|
527
|
-
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
527
|
+
with urllib.request.urlopen(req, timeout=3) as resp: # nosec B310 # local test server or KOMAGU_GATEWAY_URL under test
|
|
528
528
|
self.fail(f"Expected 403 Forbidden but received {resp.status}")
|
|
529
529
|
except urllib.error.HTTPError as e:
|
|
530
530
|
self.assertEqual(e.code, 403)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
cryptography>=
|
|
1
|
+
cryptography>=50.0.0
|
package/remediate/identity/dotnet/src/Komagu.Security.Identity/OidcServiceCollectionExtensions.cs
CHANGED
|
@@ -11,7 +11,7 @@ using Microsoft.IdentityModel.Tokens;
|
|
|
11
11
|
/// <summary>
|
|
12
12
|
/// Service collection extension methods for configuring Komagu OIDC / JWT authentication.
|
|
13
13
|
/// </summary>
|
|
14
|
-
public static class OidcServiceCollectionExtensions
|
|
14
|
+
public static partial class OidcServiceCollectionExtensions
|
|
15
15
|
{
|
|
16
16
|
/// <summary>
|
|
17
17
|
/// Configures strict JWT Bearer token authentication conforming to RFC 9700 and Boilerplate 1 standards.
|
|
@@ -79,11 +79,14 @@ public static class OidcServiceCollectionExtensions
|
|
|
79
79
|
var logger = context.HttpContext.RequestServices.GetService<ILoggerFactory>()
|
|
80
80
|
?.CreateLogger("Komagu.Security.Identity.Authentication");
|
|
81
81
|
|
|
82
|
-
logger
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
if (logger is not null)
|
|
83
|
+
{
|
|
84
|
+
LogTokenValidationFailed(
|
|
85
|
+
logger,
|
|
86
|
+
context.Exception,
|
|
87
|
+
context.Exception.Message,
|
|
88
|
+
context.HttpContext.Connection.RemoteIpAddress);
|
|
89
|
+
}
|
|
87
90
|
|
|
88
91
|
return Task.CompletedTask;
|
|
89
92
|
},
|
|
@@ -105,4 +108,14 @@ public static class OidcServiceCollectionExtensions
|
|
|
105
108
|
};
|
|
106
109
|
});
|
|
107
110
|
}
|
|
111
|
+
|
|
112
|
+
[LoggerMessage(
|
|
113
|
+
EventId = 1,
|
|
114
|
+
Level = LogLevel.Warning,
|
|
115
|
+
Message = "[AUTH FAILURE] Token validation failed: {Message} | RemoteIP: {RemoteIp}")]
|
|
116
|
+
private static partial void LogTokenValidationFailed(
|
|
117
|
+
ILogger logger,
|
|
118
|
+
Exception exception,
|
|
119
|
+
string message,
|
|
120
|
+
System.Net.IPAddress? remoteIp);
|
|
108
121
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Python dependencies for Komagu OIDC Identity Token Validation (D2)
|
|
2
|
-
pyjwt>=2.
|
|
3
|
-
cryptography>=
|
|
2
|
+
pyjwt>=2.15.0
|
|
3
|
+
cryptography>=50.0.0
|