generated from coulomb/repo-seed
Implement policy package loader
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
# CARING examples
|
# CARING examples
|
||||||
|
|
||||||
Small fixtures for the executable CARING 0.4.0-RC2 profile used by
|
Small fixtures for the executable CARING 0.4.0-RC2 profile used by
|
||||||
`FLEX-WP-0002 P2.1`.
|
`FLEX-WP-0002`.
|
||||||
|
|
||||||
These are intentionally compact. They are not policy-engine fixtures yet;
|
These are intentionally compact. They prove that the canonical descriptor,
|
||||||
they prove that the canonical descriptor, request, decision, registry, and
|
request, decision, registry, audit, and Rego-in-Markdown policy package
|
||||||
audit shapes can round-trip through `pkg/api`.
|
shapes can round-trip through `pkg/api` and `internal/policy`.
|
||||||
|
|||||||
137
examples/caring/policy_package.md
Normal file
137
examples/caring/policy_package.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
id: markitect.documents.internal-read
|
||||||
|
name: Markitect internal document read
|
||||||
|
namespace: markitect:document
|
||||||
|
version: v1
|
||||||
|
status: draft
|
||||||
|
package: flexauth.markitect.documents
|
||||||
|
actions:
|
||||||
|
- read
|
||||||
|
owner: team:platform-architecture
|
||||||
|
fixtures:
|
||||||
|
- policy_fixture.yaml
|
||||||
|
caring:
|
||||||
|
profile: caring-0.4.0-rc2
|
||||||
|
enforce: false
|
||||||
|
canonical_roles:
|
||||||
|
- Doer
|
||||||
|
organization_relations:
|
||||||
|
- Customer
|
||||||
|
scopes:
|
||||||
|
- level: Resource
|
||||||
|
id: document:internal-note
|
||||||
|
tenant: tenant:alpha
|
||||||
|
planes:
|
||||||
|
- Data
|
||||||
|
capabilities:
|
||||||
|
- View
|
||||||
|
exposure_modes:
|
||||||
|
- Masked
|
||||||
|
- Plaintext
|
||||||
|
conditions:
|
||||||
|
- PurposeBound
|
||||||
|
- Logged
|
||||||
|
restrictions:
|
||||||
|
- ExportBlocked
|
||||||
|
activation:
|
||||||
|
mode: local
|
||||||
|
metadata:
|
||||||
|
source: examples/caring/policy_package.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Markitect Internal Document Read
|
||||||
|
|
||||||
|
This package authorizes read access to an internal Markitect document when
|
||||||
|
the request carries a CARING descriptor for a customer Doer with View
|
||||||
|
capability on the document resource and an explicit ExportBlocked restriction.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
```rego
|
||||||
|
import future.keywords.if
|
||||||
|
import future.keywords.in
|
||||||
|
|
||||||
|
default decision := {"effect": "deny", "reason": "no_matching_rule"}
|
||||||
|
|
||||||
|
decision := {
|
||||||
|
"effect": "allow",
|
||||||
|
"reason": "reader_relation",
|
||||||
|
"conformance_findings": [{
|
||||||
|
"code": "CARING-EXPORT-SEPARATION",
|
||||||
|
"severity": "info",
|
||||||
|
"message": "View is allowed, but Exportable exposure remains separately blocked."
|
||||||
|
}]
|
||||||
|
} if {
|
||||||
|
input.action == "read"
|
||||||
|
input.resource.system == "markitect-tool"
|
||||||
|
input.resource.type == "document"
|
||||||
|
input.caring_context.profile == "caring-0.4.0-rc2"
|
||||||
|
input.caring_context.organization_relation == "Customer"
|
||||||
|
input.caring_context.canonical_role == "Doer"
|
||||||
|
"View" in input.caring_context.capabilities
|
||||||
|
"ExportBlocked" in input.caring_context.restrictions
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```rego test
|
||||||
|
package flexauth.markitect.documents_test
|
||||||
|
|
||||||
|
import future.keywords.if
|
||||||
|
import data.flexauth.markitect.documents
|
||||||
|
|
||||||
|
test_reader_relation_allows if {
|
||||||
|
documents.decision.effect == "allow" with input as {
|
||||||
|
"action": "read",
|
||||||
|
"resource": {
|
||||||
|
"id": "document:internal-note",
|
||||||
|
"type": "document",
|
||||||
|
"system": "markitect-tool",
|
||||||
|
"tenant": "tenant:alpha"
|
||||||
|
},
|
||||||
|
"caring_context": {
|
||||||
|
"profile": "caring-0.4.0-rc2",
|
||||||
|
"organization_relation": "Customer",
|
||||||
|
"canonical_role": "Doer",
|
||||||
|
"capabilities": ["View"],
|
||||||
|
"restrictions": ["ExportBlocked"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test_missing_caring_context_denies if {
|
||||||
|
documents.decision.effect == "deny" with input as {
|
||||||
|
"action": "read",
|
||||||
|
"resource": {
|
||||||
|
"id": "document:internal-note",
|
||||||
|
"type": "document",
|
||||||
|
"system": "markitect-tool",
|
||||||
|
"tenant": "tenant:alpha"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fixtures
|
||||||
|
|
||||||
|
```yaml fixture
|
||||||
|
id: fixture:markitect-internal-read-deny
|
||||||
|
request:
|
||||||
|
id: check:tenant-alpha-internal-note-deny
|
||||||
|
subject:
|
||||||
|
id: user:bob
|
||||||
|
type: Human
|
||||||
|
tenant: tenant:alpha
|
||||||
|
action: read
|
||||||
|
resource:
|
||||||
|
id: document:internal-note
|
||||||
|
type: document
|
||||||
|
system: markitect-tool
|
||||||
|
tenant: tenant:alpha
|
||||||
|
expect:
|
||||||
|
effect: deny
|
||||||
|
reason: no_matching_rule
|
||||||
|
metadata:
|
||||||
|
source: examples/caring/policy_package.md
|
||||||
|
```
|
||||||
38
go.mod
38
go.mod
@@ -2,4 +2,40 @@ module github.com/netkingdom/flex-auth
|
|||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.1
|
require (
|
||||||
|
github.com/open-policy-agent/opa v0.70.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||||
|
github.com/agnivade/levenshtein v1.2.0 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
github.com/prometheus/common v0.55.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
|
github.com/yashtewari/glob-intersection v0.2.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||||
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
152
go.sum
152
go.sum
@@ -1,4 +1,154 @@
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||||
|
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
|
||||||
|
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||||
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||||
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
|
||||||
|
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
|
||||||
|
github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
|
||||||
|
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||||
|
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||||
|
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||||
|
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||||
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||||
|
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||||
|
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
|
||||||
|
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
|
||||||
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
|
||||||
|
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
|
||||||
|
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||||
|
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/open-policy-agent/opa v0.70.0 h1:B3cqCN2iQAyKxK6+GI+N40uqkin+wzIrM7YA60t9x1U=
|
||||||
|
github.com/open-policy-agent/opa v0.70.0/go.mod h1:Y/nm5NY0BX0BqjBriKUiV81sCl8XOjjvqQG7dXrggtI=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
|
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||||
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
|
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||||
|
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||||
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
|
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
|
||||||
|
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
|
||||||
|
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
|
||||||
|
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
|
||||||
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||||
|
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||||
|
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||||
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||||
|
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||||
|
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||||
|
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||||
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||||
|
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||||
|
|||||||
684
internal/policy/package.go
Normal file
684
internal/policy/package.go
Normal file
@@ -0,0 +1,684 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/ast"
|
||||||
|
"github.com/open-policy-agent/opa/rego"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/netkingdom/flex-auth/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Diagnostic is a validation message emitted while loading or evaluating a
|
||||||
|
// policy package.
|
||||||
|
type Diagnostic struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Fields []string `json:"fields,omitempty"`
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CodeBlock is a fenced code block extracted from a policy package document.
|
||||||
|
type CodeBlock struct {
|
||||||
|
Language string `json:"language"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
Info string `json:"info,omitempty"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
StartLine int `json:"start_line"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package is a loaded Rego-in-Markdown policy package.
|
||||||
|
type Package struct {
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Metadata api.PolicyPackageMetadata `json:"metadata"`
|
||||||
|
Prose string `json:"prose,omitempty"`
|
||||||
|
RuleBlocks []CodeBlock `json:"rule_blocks,omitempty"`
|
||||||
|
TestBlocks []CodeBlock `json:"test_blocks,omitempty"`
|
||||||
|
FixtureBlocks []CodeBlock `json:"fixture_blocks,omitempty"`
|
||||||
|
RegoModule string `json:"rego_module,omitempty"`
|
||||||
|
TestModule string `json:"test_module,omitempty"`
|
||||||
|
TestPackage string `json:"test_package,omitempty"`
|
||||||
|
Fixtures []api.PolicyFixture `json:"fixtures,omitempty"`
|
||||||
|
Validation ValidationResult `json:"validation,omitempty"`
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationResult captures OPA and CARING validation outcomes for a package.
|
||||||
|
type ValidationResult struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
Diagnostics []Diagnostic `json:"diagnostics,omitempty"`
|
||||||
|
CaringFindings []api.CaringConformanceFinding `json:"caring_findings,omitempty"`
|
||||||
|
Tests []TestResult `json:"tests,omitempty"`
|
||||||
|
Fixtures []FixtureResult `json:"fixtures,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResult captures one OPA test rule result.
|
||||||
|
type TestResult struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Passed bool `json:"passed"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FixtureResult captures one embedded or referenced fixture evaluation result.
|
||||||
|
type FixtureResult struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Passed bool `json:"passed"`
|
||||||
|
Expected api.DecisionExpectation `json:"expected"`
|
||||||
|
Actual api.DecisionExpectation `json:"actual"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile loads a policy package Markdown document from disk.
|
||||||
|
func LoadFile(path string) (*Package, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read policy package: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg, err := Load(data, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := pkg.loadExternalFixtures(filepath.Dir(path)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pkg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load parses a policy package Markdown document.
|
||||||
|
func Load(data []byte, source string) (*Package, error) {
|
||||||
|
frontmatter, body, err := splitFrontmatter(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata api.PolicyPackageMetadata
|
||||||
|
if err := yaml.Unmarshal([]byte(frontmatter), &metadata); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal policy package frontmatter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
extracted, err := extractMarkdown(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg := &Package{
|
||||||
|
Source: source,
|
||||||
|
Metadata: metadata,
|
||||||
|
Prose: strings.TrimSpace(extracted.prose),
|
||||||
|
RuleBlocks: extracted.ruleBlocks,
|
||||||
|
TestBlocks: extracted.testBlocks,
|
||||||
|
FixtureBlocks: extracted.fixtureBlocks,
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg.RegoModule, _ = normalizeRuleModule(metadata.Package, pkg.RuleBlocks)
|
||||||
|
pkg.TestModule, pkg.TestPackage, _ = normalizeTestModule(metadata.Package, pkg.TestBlocks)
|
||||||
|
|
||||||
|
for _, block := range pkg.FixtureBlocks {
|
||||||
|
fixtures, err := parseFixtureYAML(block.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse fixture block at line %d: %w", block.StartLine, err)
|
||||||
|
}
|
||||||
|
pkg.Fixtures = append(pkg.Fixtures, fixtures...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAndValidateFile loads a policy package and immediately validates it.
|
||||||
|
func LoadAndValidateFile(ctx context.Context, path string) (*Package, error) {
|
||||||
|
pkg, err := LoadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pkg.Validate(ctx)
|
||||||
|
return pkg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate runs metadata, CARING, OPA parse/test, and fixture validation.
|
||||||
|
func (p *Package) Validate(ctx context.Context) ValidationResult {
|
||||||
|
result := ValidationResult{}
|
||||||
|
|
||||||
|
result.Diagnostics = append(result.Diagnostics, p.metadataDiagnostics()...)
|
||||||
|
result.CaringFindings = append(result.CaringFindings, p.caringFindings()...)
|
||||||
|
if len(p.Fixtures) == 0 {
|
||||||
|
result.Diagnostics = append(result.Diagnostics, Diagnostic{
|
||||||
|
Code: "POLICY-FIXTURE-MISSING",
|
||||||
|
Severity: "error",
|
||||||
|
Message: "policy package must include at least one embedded or referenced fixture",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
regoModule, regoDiagnostics := normalizeRuleModule(p.Metadata.Package, p.RuleBlocks)
|
||||||
|
result.Diagnostics = append(result.Diagnostics, regoDiagnostics...)
|
||||||
|
p.RegoModule = regoModule
|
||||||
|
|
||||||
|
testModule, testPackage, testDiagnostics := normalizeTestModule(p.Metadata.Package, p.TestBlocks)
|
||||||
|
result.Diagnostics = append(result.Diagnostics, testDiagnostics...)
|
||||||
|
p.TestModule = testModule
|
||||||
|
p.TestPackage = testPackage
|
||||||
|
|
||||||
|
parseOK := true
|
||||||
|
if p.RegoModule != "" {
|
||||||
|
if _, err := ast.ParseModule(moduleFilename(p.Source, "rego"), p.RegoModule); err != nil {
|
||||||
|
parseOK = false
|
||||||
|
result.Diagnostics = append(result.Diagnostics, Diagnostic{
|
||||||
|
Code: "OPA-PARSE",
|
||||||
|
Severity: "error",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.TestModule != "" {
|
||||||
|
if _, err := ast.ParseModule(moduleFilename(p.Source, "test.rego"), p.TestModule); err != nil {
|
||||||
|
parseOK = false
|
||||||
|
result.Diagnostics = append(result.Diagnostics, Diagnostic{
|
||||||
|
Code: "OPA-TEST-PARSE",
|
||||||
|
Severity: "error",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parseOK && p.RegoModule != "" {
|
||||||
|
result.Tests = p.runTests(ctx)
|
||||||
|
result.Fixtures = p.runFixtures(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Valid = validationPassed(result, p.Metadata.Caring.Enforce)
|
||||||
|
p.Validation = result
|
||||||
|
p.Valid = result.Valid
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) loadExternalFixtures(baseDir string) error {
|
||||||
|
for _, fixturePath := range p.Metadata.Fixtures {
|
||||||
|
path := fixturePath
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
path = filepath.Join(baseDir, fixturePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read policy fixture %q: %w", fixturePath, err)
|
||||||
|
}
|
||||||
|
fixtures, err := parseFixtureYAML(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse policy fixture %q: %w", fixturePath, err)
|
||||||
|
}
|
||||||
|
p.Fixtures = append(p.Fixtures, fixtures...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) metadataDiagnostics() []Diagnostic {
|
||||||
|
var diagnostics []Diagnostic
|
||||||
|
if p.Metadata.ID == "" {
|
||||||
|
diagnostics = append(diagnostics, requiredDiagnostic("POLICY-METADATA-ID", "id", "policy package id is required"))
|
||||||
|
}
|
||||||
|
if p.Metadata.Version == "" {
|
||||||
|
diagnostics = append(diagnostics, requiredDiagnostic("POLICY-METADATA-VERSION", "version", "policy package version is required"))
|
||||||
|
}
|
||||||
|
if p.Metadata.Package == "" {
|
||||||
|
diagnostics = append(diagnostics, requiredDiagnostic("POLICY-METADATA-PACKAGE", "package", "OPA package path is required"))
|
||||||
|
}
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) caringFindings() []api.CaringConformanceFinding {
|
||||||
|
var findings []api.CaringConformanceFinding
|
||||||
|
caring := p.Metadata.Caring
|
||||||
|
if caring.Profile == "" {
|
||||||
|
findings = append(findings, caringFinding("CARING-POLICY-PROFILE", "error", "policy package must declare a CARING profile", "caring.profile"))
|
||||||
|
} else if caring.Profile != api.CaringProfileCaring040RC2 {
|
||||||
|
findings = append(findings, caringFinding("CARING-POLICY-PROFILE", "error", fmt.Sprintf("unsupported CARING profile %q", caring.Profile), "caring.profile"))
|
||||||
|
}
|
||||||
|
|
||||||
|
addMissing := func(empty bool, field, label string) {
|
||||||
|
if empty {
|
||||||
|
findings = append(findings, caringFinding("CARING-POLICY-MISSING-DIMENSION", "warning", "policy package should declare governed "+label, field))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addMissing(len(caring.CanonicalRoles) == 0, "caring.canonical_roles", "canonical roles")
|
||||||
|
addMissing(len(caring.OrganizationRelations) == 0, "caring.organization_relations", "organization relations")
|
||||||
|
addMissing(len(caring.Scopes) == 0, "caring.scopes", "scopes")
|
||||||
|
addMissing(len(caring.Planes) == 0, "caring.planes", "planes")
|
||||||
|
addMissing(len(caring.Capabilities) == 0, "caring.capabilities", "capabilities")
|
||||||
|
addMissing(len(caring.ExposureModes) == 0, "caring.exposure_modes", "exposure modes")
|
||||||
|
addMissing(len(caring.Conditions) == 0, "caring.conditions", "conditions")
|
||||||
|
addMissing(len(caring.Restrictions) == 0, "caring.restrictions", "restrictions")
|
||||||
|
return findings
|
||||||
|
}
|
||||||
|
|
||||||
|
func requiredDiagnostic(code, field, message string) Diagnostic {
|
||||||
|
return Diagnostic{
|
||||||
|
Code: code,
|
||||||
|
Severity: "error",
|
||||||
|
Message: message,
|
||||||
|
Fields: []string{field},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func caringFinding(code, severity, message, field string) api.CaringConformanceFinding {
|
||||||
|
return api.CaringConformanceFinding{
|
||||||
|
Code: code,
|
||||||
|
Severity: severity,
|
||||||
|
Message: message,
|
||||||
|
Fields: []string{field},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) runTests(ctx context.Context) []TestResult {
|
||||||
|
if p.TestModule == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
module, err := ast.ParseModule(moduleFilename(p.Source, "test.rego"), p.TestModule)
|
||||||
|
if err != nil {
|
||||||
|
return []TestResult{{Name: "parse", Passed: false, Error: err.Error()}}
|
||||||
|
}
|
||||||
|
|
||||||
|
var names []string
|
||||||
|
for _, rule := range module.Rules {
|
||||||
|
name := string(rule.Head.Name)
|
||||||
|
if strings.HasPrefix(name, "test_") {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
if len(names) == 0 {
|
||||||
|
return []TestResult{{
|
||||||
|
Name: "test_*",
|
||||||
|
Passed: false,
|
||||||
|
Error: "test module does not contain any test_* rules",
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]TestResult, 0, len(names))
|
||||||
|
for _, name := range names {
|
||||||
|
query := "data." + p.TestPackage + "." + name
|
||||||
|
passed, err := p.evalBool(ctx, query, nil, true)
|
||||||
|
result := TestResult{Name: name, Passed: passed}
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
} else if !passed {
|
||||||
|
result.Error = "test rule evaluated to false or undefined"
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) runFixtures(ctx context.Context) []FixtureResult {
|
||||||
|
results := make([]FixtureResult, 0, len(p.Fixtures))
|
||||||
|
for _, fixture := range p.Fixtures {
|
||||||
|
actual, err := p.evaluateDecision(ctx, fixture.Request)
|
||||||
|
result := FixtureResult{
|
||||||
|
ID: fixture.ID,
|
||||||
|
Expected: fixture.Expect,
|
||||||
|
Actual: actual,
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
} else if expectationMatches(fixture.Expect, actual) {
|
||||||
|
result.Passed = true
|
||||||
|
} else {
|
||||||
|
result.Error = "fixture expectation did not match actual decision"
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) evaluateDecision(ctx context.Context, request api.CheckRequest) (api.DecisionExpectation, error) {
|
||||||
|
input, err := toRegoInput(request)
|
||||||
|
if err != nil {
|
||||||
|
return api.DecisionExpectation{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := "data." + p.Metadata.Package + ".decision"
|
||||||
|
results, err := p.eval(ctx, query, input, false)
|
||||||
|
if err != nil {
|
||||||
|
return api.DecisionExpectation{}, err
|
||||||
|
}
|
||||||
|
if len(results) == 0 || len(results[0].Expressions) == 0 {
|
||||||
|
return api.DecisionExpectation{}, fmt.Errorf("decision query %q was undefined", query)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(results[0].Expressions[0].Value)
|
||||||
|
if err != nil {
|
||||||
|
return api.DecisionExpectation{}, fmt.Errorf("marshal decision result: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decision api.DecisionExpectation
|
||||||
|
if err := json.Unmarshal(data, &decision); err != nil {
|
||||||
|
return api.DecisionExpectation{}, fmt.Errorf("unmarshal decision result: %w", err)
|
||||||
|
}
|
||||||
|
return decision, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) evalBool(ctx context.Context, query string, input map[string]any, includeTests bool) (bool, error) {
|
||||||
|
results, err := p.eval(ctx, query, input, includeTests)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, result := range results {
|
||||||
|
if len(result.Expressions) == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if passed, ok := result.Expressions[0].Value.(bool); ok && passed {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) eval(ctx context.Context, query string, input map[string]any, includeTests bool) (rego.ResultSet, error) {
|
||||||
|
options := []func(*rego.Rego){
|
||||||
|
rego.Query(query),
|
||||||
|
rego.Module(moduleFilename(p.Source, "rego"), p.RegoModule),
|
||||||
|
}
|
||||||
|
if includeTests && p.TestModule != "" {
|
||||||
|
options = append(options, rego.Module(moduleFilename(p.Source, "test.rego"), p.TestModule))
|
||||||
|
}
|
||||||
|
if input != nil {
|
||||||
|
options = append(options, rego.Input(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
prepared, err := rego.New(options...).PrepareForEval(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return prepared.Eval(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectationMatches(expected, actual api.DecisionExpectation) bool {
|
||||||
|
if expected.Effect != "" && expected.Effect != actual.Effect {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if expected.Reason != "" && expected.Reason != actual.Reason {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(expected.Obligations) > 0 && !reflect.DeepEqual(expected.Obligations, actual.Obligations) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(expected.ConformanceFindings) > 0 && !reflect.DeepEqual(expected.ConformanceFindings, actual.ConformanceFindings) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRegoInput(value any) (map[string]any, error) {
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal input: %w", err)
|
||||||
|
}
|
||||||
|
var out map[string]any
|
||||||
|
if err := json.Unmarshal(data, &out); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal input: %w", err)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validationPassed(result ValidationResult, enforceCaring bool) bool {
|
||||||
|
for _, diagnostic := range result.Diagnostics {
|
||||||
|
if diagnostic.Severity == "error" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, finding := range result.CaringFindings {
|
||||||
|
if finding.Severity == "error" || (enforceCaring && finding.Severity != "info") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, test := range result.Tests {
|
||||||
|
if !test.Passed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, fixture := range result.Fixtures {
|
||||||
|
if !fixture.Passed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRuleModule(packageName string, blocks []CodeBlock) (string, []Diagnostic) {
|
||||||
|
module, _, diagnostics := normalizeModule(packageName, "", blocks, "POLICY-REGO")
|
||||||
|
return module, diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeTestModule(packageName string, blocks []CodeBlock) (string, string, []Diagnostic) {
|
||||||
|
if len(blocks) == 0 {
|
||||||
|
return "", "", []Diagnostic{{
|
||||||
|
Code: "POLICY-REGO-TEST-MISSING",
|
||||||
|
Severity: "error",
|
||||||
|
Message: "policy package must include at least one rego test block",
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPackage := ""
|
||||||
|
if packageName != "" {
|
||||||
|
defaultPackage = packageName + "_test"
|
||||||
|
}
|
||||||
|
module, testPackage, diagnostics := normalizeModule(defaultPackage, "test", blocks, "POLICY-REGO-TEST")
|
||||||
|
return module, testPackage, diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeModule(defaultPackage, label string, blocks []CodeBlock, codePrefix string) (string, string, []Diagnostic) {
|
||||||
|
if len(blocks) == 0 {
|
||||||
|
return "", "", []Diagnostic{{
|
||||||
|
Code: codePrefix + "-MISSING",
|
||||||
|
Severity: "error",
|
||||||
|
Message: "policy package must include at least one " + labelOrDefault(label, "rule") + " block",
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
var packageName string
|
||||||
|
var parts []string
|
||||||
|
var diagnostics []Diagnostic
|
||||||
|
for _, block := range blocks {
|
||||||
|
body, declared := stripPackageDeclaration(block.Body)
|
||||||
|
if declared != "" {
|
||||||
|
if packageName == "" {
|
||||||
|
packageName = declared
|
||||||
|
} else if declared != packageName {
|
||||||
|
diagnostics = append(diagnostics, Diagnostic{
|
||||||
|
Code: codePrefix + "-PACKAGE-MISMATCH",
|
||||||
|
Severity: "error",
|
||||||
|
Message: fmt.Sprintf("block at line %d declares package %q, expected %q", block.StartLine, declared, packageName),
|
||||||
|
Fields: []string{"package"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(body) != "" {
|
||||||
|
parts = append(parts, strings.TrimSpace(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultPackage != "" {
|
||||||
|
if packageName != "" && packageName != defaultPackage {
|
||||||
|
diagnostics = append(diagnostics, Diagnostic{
|
||||||
|
Code: codePrefix + "-PACKAGE-MISMATCH",
|
||||||
|
Severity: "error",
|
||||||
|
Message: fmt.Sprintf("declared package %q does not match frontmatter package %q", packageName, defaultPackage),
|
||||||
|
Fields: []string{"package"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
packageName = defaultPackage
|
||||||
|
}
|
||||||
|
if packageName == "" {
|
||||||
|
diagnostics = append(diagnostics, Diagnostic{
|
||||||
|
Code: codePrefix + "-PACKAGE-MISSING",
|
||||||
|
Severity: "error",
|
||||||
|
Message: "policy package cannot build a Rego module without a package name",
|
||||||
|
Fields: []string{"package"},
|
||||||
|
})
|
||||||
|
return "", "", diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
module := "package " + packageName + "\n\n" + strings.Join(parts, "\n\n")
|
||||||
|
return strings.TrimSpace(module) + "\n", packageName, diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
func labelOrDefault(label, fallback string) string {
|
||||||
|
if label == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripPackageDeclaration(body string) (string, string) {
|
||||||
|
lines := strings.Split(body, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, "package ") {
|
||||||
|
declared := strings.TrimSpace(strings.TrimPrefix(trimmed, "package "))
|
||||||
|
lines = append(lines[:i], lines[i+1:]...)
|
||||||
|
return strings.Join(lines, "\n"), declared
|
||||||
|
}
|
||||||
|
return body, ""
|
||||||
|
}
|
||||||
|
return body, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFixtureYAML(body string) ([]api.PolicyFixture, error) {
|
||||||
|
var node yaml.Node
|
||||||
|
if err := yaml.Unmarshal([]byte(body), &node); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(node.Content) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
root := node.Content[0]
|
||||||
|
if root.Kind == yaml.SequenceNode {
|
||||||
|
var fixtures []api.PolicyFixture
|
||||||
|
if err := root.Decode(&fixtures); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fixtures, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var fixture api.PolicyFixture
|
||||||
|
if err := root.Decode(&fixture); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []api.PolicyFixture{fixture}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type markdownExtraction struct {
|
||||||
|
prose string
|
||||||
|
ruleBlocks []CodeBlock
|
||||||
|
testBlocks []CodeBlock
|
||||||
|
fixtureBlocks []CodeBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractMarkdown(body string) (markdownExtraction, error) {
|
||||||
|
var out markdownExtraction
|
||||||
|
var prose strings.Builder
|
||||||
|
var block strings.Builder
|
||||||
|
var current *CodeBlock
|
||||||
|
|
||||||
|
lines := strings.Split(body, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
lineNo := i + 1
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if current == nil {
|
||||||
|
if strings.HasPrefix(trimmed, "```") {
|
||||||
|
info := strings.TrimSpace(strings.TrimPrefix(trimmed, "```"))
|
||||||
|
current = &CodeBlock{
|
||||||
|
Info: info,
|
||||||
|
StartLine: lineNo,
|
||||||
|
}
|
||||||
|
current.Language, current.Tags = parseFenceInfo(info)
|
||||||
|
block.Reset()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prose.WriteString(line)
|
||||||
|
if i < len(lines)-1 {
|
||||||
|
prose.WriteByte('\n')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "```") {
|
||||||
|
current.Body = strings.TrimRight(block.String(), "\n")
|
||||||
|
switch {
|
||||||
|
case current.Language == "rego" && hasTag(current.Tags, "test"):
|
||||||
|
out.testBlocks = append(out.testBlocks, *current)
|
||||||
|
case current.Language == "rego":
|
||||||
|
out.ruleBlocks = append(out.ruleBlocks, *current)
|
||||||
|
case (current.Language == "yaml" || current.Language == "yml") && hasTag(current.Tags, "fixture"):
|
||||||
|
out.fixtureBlocks = append(out.fixtureBlocks, *current)
|
||||||
|
}
|
||||||
|
current = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
block.WriteString(line)
|
||||||
|
block.WriteByte('\n')
|
||||||
|
}
|
||||||
|
if current != nil {
|
||||||
|
return out, fmt.Errorf("unterminated fenced code block starting at line %d", current.StartLine)
|
||||||
|
}
|
||||||
|
out.prose = prose.String()
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFenceInfo(info string) (string, []string) {
|
||||||
|
fields := strings.Fields(info)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
language := strings.ToLower(fields[0])
|
||||||
|
tags := make([]string, 0, len(fields)-1)
|
||||||
|
for _, field := range fields[1:] {
|
||||||
|
tags = append(tags, strings.ToLower(field))
|
||||||
|
}
|
||||||
|
return language, tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasTag(tags []string, want string) bool {
|
||||||
|
for _, tag := range tags {
|
||||||
|
if tag == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitFrontmatter(document string) (string, string, error) {
|
||||||
|
document = strings.TrimPrefix(document, "\ufeff")
|
||||||
|
lines := strings.SplitAfter(document, "\n")
|
||||||
|
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
||||||
|
return "", "", fmt.Errorf("policy package must start with YAML frontmatter")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(lines); i++ {
|
||||||
|
if strings.TrimSpace(lines[i]) == "---" {
|
||||||
|
return strings.Join(lines[1:i], ""), strings.Join(lines[i+1:], ""), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("policy package frontmatter is not closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func moduleFilename(source, suffix string) string {
|
||||||
|
if source == "" {
|
||||||
|
return "policy." + suffix
|
||||||
|
}
|
||||||
|
return source + "." + suffix
|
||||||
|
}
|
||||||
@@ -1,15 +1,96 @@
|
|||||||
package policy_test
|
package policy_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/netkingdom/flex-auth/internal/policy"
|
||||||
"github.com/netkingdom/flex-auth/pkg/api"
|
"github.com/netkingdom/flex-auth/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestLoadPolicyPackageMarkdownValidates(t *testing.T) {
|
||||||
|
pkg, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "caring", "policy_package.md"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadAndValidateFile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pkg.Valid {
|
||||||
|
t.Fatalf("pkg.Valid = false\n%s", formatValidation(pkg.Validation))
|
||||||
|
}
|
||||||
|
if pkg.Metadata.Caring.Profile != api.CaringProfileCaring040RC2 {
|
||||||
|
t.Fatalf("metadata.Caring.Profile = %q; want %q", pkg.Metadata.Caring.Profile, api.CaringProfileCaring040RC2)
|
||||||
|
}
|
||||||
|
if pkg.Metadata.Namespace != "markitect:document" {
|
||||||
|
t.Errorf("metadata.Namespace = %q; want markitect:document", pkg.Metadata.Namespace)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(pkg.RegoModule, "package flexauth.markitect.documents") {
|
||||||
|
t.Errorf("RegoModule prefix = %q; want flexauth.markitect.documents package", pkg.RegoModule[:min(len(pkg.RegoModule), 80)])
|
||||||
|
}
|
||||||
|
if len(pkg.RuleBlocks) != 1 || len(pkg.TestBlocks) != 1 || len(pkg.Fixtures) != 2 {
|
||||||
|
t.Fatalf("blocks/fixtures = rules:%d tests:%d fixtures:%d; want 1/1/2", len(pkg.RuleBlocks), len(pkg.TestBlocks), len(pkg.Fixtures))
|
||||||
|
}
|
||||||
|
if len(pkg.Validation.Tests) != 2 {
|
||||||
|
t.Fatalf("Validation.Tests len = %d; want 2", len(pkg.Validation.Tests))
|
||||||
|
}
|
||||||
|
for _, test := range pkg.Validation.Tests {
|
||||||
|
if !test.Passed {
|
||||||
|
t.Fatalf("test %s failed: %s", test.Name, test.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, fixture := range pkg.Validation.Fixtures {
|
||||||
|
if !fixture.Passed {
|
||||||
|
t.Fatalf("fixture %s failed: %s\nactual: %+v", fixture.ID, fixture.Error, fixture.Actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaringFindingsAreAdvisoryUntilEnforced(t *testing.T) {
|
||||||
|
doc := inlinePolicy(false, "allow")
|
||||||
|
pkg, err := policy.Load([]byte(doc), "inline-policy.md")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := pkg.Validate(context.Background())
|
||||||
|
if !result.Valid {
|
||||||
|
t.Fatalf("result.Valid = false without CARING enforcement\n%s", formatValidation(result))
|
||||||
|
}
|
||||||
|
if len(result.CaringFindings) == 0 {
|
||||||
|
t.Fatal("expected advisory CARING findings for missing metadata dimensions")
|
||||||
|
}
|
||||||
|
|
||||||
|
enforced := strings.Replace(doc, "enforce: false", "enforce: true", 1)
|
||||||
|
pkg, err = policy.Load([]byte(enforced), "inline-policy.md")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load enforced: %v", err)
|
||||||
|
}
|
||||||
|
result = pkg.Validate(context.Background())
|
||||||
|
if result.Valid {
|
||||||
|
t.Fatalf("result.Valid = true with CARING enforcement; want invalid\n%s", formatValidation(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFixtureMismatchInvalidatesPackage(t *testing.T) {
|
||||||
|
pkg, err := policy.Load([]byte(inlinePolicy(false, "deny")), "inline-policy.md")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := pkg.Validate(context.Background())
|
||||||
|
if result.Valid {
|
||||||
|
t.Fatalf("result.Valid = true; want fixture mismatch to invalidate package\n%s", formatValidation(result))
|
||||||
|
}
|
||||||
|
if len(result.Fixtures) != 1 || result.Fixtures[0].Passed {
|
||||||
|
t.Fatalf("fixture result = %+v; want one failed fixture", result.Fixtures)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPolicyPackageMetadataParses(t *testing.T) {
|
func TestPolicyPackageMetadataParses(t *testing.T) {
|
||||||
var metadata api.PolicyPackageMetadata
|
var metadata api.PolicyPackageMetadata
|
||||||
loadYAML(t, filepath.Join("..", "..", "examples", "caring", "policy_package.yaml"), &metadata)
|
loadYAML(t, filepath.Join("..", "..", "examples", "caring", "policy_package.yaml"), &metadata)
|
||||||
@@ -37,6 +118,63 @@ func TestPolicyFixtureParses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inlinePolicy(enforce bool, expectedEffect string) string {
|
||||||
|
enforceValue := "false"
|
||||||
|
if enforce {
|
||||||
|
enforceValue = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
fence := "```"
|
||||||
|
doc := strings.ReplaceAll(`---
|
||||||
|
id: inline.allow
|
||||||
|
version: v1
|
||||||
|
package: flexauth.inline
|
||||||
|
caring:
|
||||||
|
profile: caring-0.4.0-rc2
|
||||||
|
enforce: ENFORCE
|
||||||
|
---
|
||||||
|
|
||||||
|
# Inline Policy
|
||||||
|
|
||||||
|
`+fence+`rego
|
||||||
|
default decision := {"effect": "allow", "reason": "ok"}
|
||||||
|
`+fence+`
|
||||||
|
|
||||||
|
`+fence+`rego test
|
||||||
|
package flexauth.inline_test
|
||||||
|
|
||||||
|
import future.keywords.if
|
||||||
|
import data.flexauth.inline
|
||||||
|
|
||||||
|
test_allow if {
|
||||||
|
inline.decision.effect == "allow"
|
||||||
|
}
|
||||||
|
`+fence+`
|
||||||
|
|
||||||
|
`+fence+`yaml fixture
|
||||||
|
id: fixture:inline
|
||||||
|
request:
|
||||||
|
subject:
|
||||||
|
id: user:alice
|
||||||
|
action: read
|
||||||
|
resource:
|
||||||
|
id: document:inline
|
||||||
|
expect:
|
||||||
|
effect: EXPECTED
|
||||||
|
reason: ok
|
||||||
|
`+fence+`
|
||||||
|
`, "ENFORCE", enforceValue)
|
||||||
|
return strings.ReplaceAll(doc, "EXPECTED", expectedEffect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatValidation(result policy.ValidationResult) string {
|
||||||
|
data, err := json.MarshalIndent(result, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
func loadYAML(t *testing.T, path string, out any) {
|
func loadYAML(t *testing.T, path string, out any) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -102,9 +102,13 @@ type RelationshipFact struct {
|
|||||||
type PolicyPackageMetadata struct {
|
type PolicyPackageMetadata struct {
|
||||||
ID string `json:"id" yaml:"id"`
|
ID string `json:"id" yaml:"id"`
|
||||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||||
|
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
|
||||||
Version string `json:"version" yaml:"version"`
|
Version string `json:"version" yaml:"version"`
|
||||||
Status string `json:"status,omitempty" yaml:"status,omitempty"`
|
Status string `json:"status,omitempty" yaml:"status,omitempty"`
|
||||||
Package string `json:"package" yaml:"package"`
|
Package string `json:"package" yaml:"package"`
|
||||||
|
Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"`
|
||||||
|
Owner string `json:"owner,omitempty" yaml:"owner,omitempty"`
|
||||||
|
Fixtures []string `json:"fixtures,omitempty" yaml:"fixtures,omitempty"`
|
||||||
Caring CaringPolicyMetadata `json:"caring" yaml:"caring"`
|
Caring CaringPolicyMetadata `json:"caring" yaml:"caring"`
|
||||||
Activation map[string]any `json:"activation,omitempty" yaml:"activation,omitempty"`
|
Activation map[string]any `json:"activation,omitempty" yaml:"activation,omitempty"`
|
||||||
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
|
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
|
||||||
@@ -113,6 +117,7 @@ type PolicyPackageMetadata struct {
|
|||||||
// CaringPolicyMetadata declares the CARING envelope a policy governs.
|
// CaringPolicyMetadata declares the CARING envelope a policy governs.
|
||||||
type CaringPolicyMetadata struct {
|
type CaringPolicyMetadata struct {
|
||||||
Profile string `json:"profile" yaml:"profile"`
|
Profile string `json:"profile" yaml:"profile"`
|
||||||
|
Enforce bool `json:"enforce,omitempty" yaml:"enforce,omitempty"`
|
||||||
CanonicalRoles []CanonicalRole `json:"canonical_roles,omitempty" yaml:"canonical_roles,omitempty"`
|
CanonicalRoles []CanonicalRole `json:"canonical_roles,omitempty" yaml:"canonical_roles,omitempty"`
|
||||||
OrganizationRelations []OrganizationRelation `json:"organization_relations,omitempty" yaml:"organization_relations,omitempty"`
|
OrganizationRelations []OrganizationRelation `json:"organization_relations,omitempty" yaml:"organization_relations,omitempty"`
|
||||||
Scopes []CaringScope `json:"scopes,omitempty" yaml:"scopes,omitempty"`
|
Scopes []CaringScope `json:"scopes,omitempty" yaml:"scopes,omitempty"`
|
||||||
|
|||||||
@@ -8,9 +8,21 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"id": {"type": "string", "minLength": 1},
|
"id": {"type": "string", "minLength": 1},
|
||||||
"name": {"type": "string", "minLength": 1},
|
"name": {"type": "string", "minLength": 1},
|
||||||
|
"namespace": {"type": "string", "minLength": 1},
|
||||||
"version": {"type": "string", "minLength": 1},
|
"version": {"type": "string", "minLength": 1},
|
||||||
"status": {"type": "string", "minLength": 1},
|
"status": {"type": "string", "minLength": 1},
|
||||||
"package": {"type": "string", "minLength": 1},
|
"package": {"type": "string", "minLength": 1},
|
||||||
|
"actions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "minLength": 1},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"owner": {"type": "string", "minLength": 1},
|
||||||
|
"fixtures": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "minLength": 1},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
"caring": {"$ref": "#/$defs/caring_policy_metadata"},
|
"caring": {"$ref": "#/$defs/caring_policy_metadata"},
|
||||||
"activation": {"type": "object", "additionalProperties": true},
|
"activation": {"type": "object", "additionalProperties": true},
|
||||||
"metadata": {"type": "object", "additionalProperties": true}
|
"metadata": {"type": "object", "additionalProperties": true}
|
||||||
@@ -22,6 +34,7 @@
|
|||||||
"required": ["profile"],
|
"required": ["profile"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"profile": {"const": "caring-0.4.0-rc2"},
|
"profile": {"const": "caring-0.4.0-rc2"},
|
||||||
|
"enforce": {"type": "boolean"},
|
||||||
"canonical_roles": {
|
"canonical_roles": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/canonical_role"},
|
"items": {"$ref": "https://flex-auth.netkingdom/schemas/caring_access_descriptor.schema.json#/$defs/canonical_role"},
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ tests and local development.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: FLEX-WP-0002-T003
|
id: FLEX-WP-0002-T003
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "09be0f25-e5ba-42b5-8b2f-36fd0ef2fe6b"
|
state_hub_task_id: "09be0f25-e5ba-42b5-8b2f-36fd0ef2fe6b"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user