Skip to content
This repository was archived by the owner on Jun 6, 2024. It is now read-only.

feat: add custom key for cel expression support #961

Merged
merged 11 commits into from
Jul 20, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions examples/CEL/policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
apiVersion: v1
policies:
- name: CEL_policy
isDefault: true
rules:
- identifier: CUSTOM_DEPLOYMENT_BILLING_LABEL_EXISTS
messageOnFailure: "workloads labels should contain billing label"
- identifier: CUSTOM_SECRET_ENVIRONMENT_LABEL_EXISTS
messageOnFailure: "secret labels should contain environment label"


customRules:
- identifier: CUSTOM_WORKLOADS_BILLING_LABEL_EXISTS
name: Ensure Workloads has billing label [CUSTOM RULE]
defaultMessageOnFailure: workloads labels should contain billing label
schema:
# constraint schema
if:
properties:
kind:
type: string
enum:
- Deployment
- Pod
then:
CELDefinition:
- expression: "object.kind != 'Deployment' || (has(object.metadata.labels) && has(object.metadata.labels.billing))"
message: "deployment labels should contain billing label"
- expression: "object.kind != 'Pod' || (has(object.metadata.labels) && has(object.metadata.labels.billing))"
message: "pod labels should contain billing label"
- identifier: CUSTOM_SECRET_ENVIRONMENT_LABEL_EXISTS
name: Ensure Secret has environment label [CUSTOM RULE]
defaultMessageOnFailure: secret labels should contain environment label
schema:
# constraint schema
if:
properties:
kind:
type: string
enum:
- Secret
then:
CELDefinition:
- expression: "has(object.metadata.labels) && has(object.metadata.labels.environment)"

17 changes: 13 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -29,13 +29,17 @@ require (
sigs.k8s.io/yaml v1.3.0
)

require github.com/open-policy-agent/opa v0.49.2
require (
github.com/google/cel-go v0.16.0
github.com/open-policy-agent/opa v0.49.2
)

require (
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/a8m/envsubst v1.3.0 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/alecthomas/participle/v2 v2.0.0-beta.5 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/elliotchance/orderedmap v1.4.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.9.11 // indirect
@@ -46,10 +50,15 @@ require (
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
github.com/yashtewari/glob-intersection v0.1.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
)

@@ -78,8 +87,8 @@ require (
github.com/tklauser/numcpus v0.3.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/ini.v1 v1.51.0 // indirect
)

30 changes: 23 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -53,6 +53,8 @@ github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0q
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
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=
@@ -168,10 +170,13 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/cel-go v0.16.0 h1:DG9YQ8nFCFXAs/FDDwBxmL1tpKNrdlGUM9U3537bX/Y=
github.com/google/cel-go v0.16.0/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -182,6 +187,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
@@ -380,6 +386,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -454,6 +462,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -512,8 +522,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -583,8 +593,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -593,8 +603,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -718,6 +728,10 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -744,7 +758,9 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
132 changes: 132 additions & 0 deletions pkg/jsonSchemaValidator/extensions/customKeyCELDefinition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// This file defines a custom key to implement the logic for cel rule:

package jsonSchemaValidator

import (
"encoding/json"
"fmt"

"github.com/google/cel-go/cel"
"github.com/santhosh-tekuri/jsonschema/v5"
)

const CELDefinitionCustomKey = "CELDefinition"

type CustomKeyCELDefinitionCompiler struct{}

type CustomKeyCELDefinitionSchema []interface{}

var CustomKeyCELRule = jsonschema.MustCompileString("customKeyCELDefinition.json", `{
"properties" : {
"CELDefinition": {
"type": "array"
}
}
}`)

func (CustomKeyCELDefinitionCompiler) Compile(ctx jsonschema.CompilerContext, m map[string]interface{}) (jsonschema.ExtSchema, error) {
if customKeyCELRule, ok := m[CELDefinitionCustomKey]; ok {
customKeyCELRuleObj, validObject := customKeyCELRule.([]interface{})
if !validObject {
return nil, fmt.Errorf("CELDefinition must be an array")
}

CELDefinitionSchema, err := convertCustomKeyCELDefinitionSchemaToCELDefinitionSchema(customKeyCELRuleObj)
if err != nil {
return nil, err
}

if len(CELDefinitionSchema.CELExpressions) == 0 {
return nil, fmt.Errorf("CELDefinition can't be empty")
}

return CustomKeyCELDefinitionSchema(customKeyCELRuleObj), nil
}
return nil, nil
}

func (customKeyCELDefinitionSchema CustomKeyCELDefinitionSchema) Validate(ctx jsonschema.ValidationContext, dataValue interface{}) error {
CELDefinitionSchema, err := convertCustomKeyCELDefinitionSchemaToCELDefinitionSchema(customKeyCELDefinitionSchema)
if err != nil {
return ctx.Error(CustomKeyValidationErrorKeyPath, err.Error())
}
// wrap dataValue (the resource that should be validated) inside a struct with parent object key
resourceWithParentKey := make(map[string]interface{})
resourceWithParentKey["object"] = dataValue

// prepare CEL env inputs - in our case the only input is the resource that should be validated
inputs, err := getCELEnvInputs(resourceWithParentKey)
if err != nil {
return ctx.Error(CustomKeyValidationErrorKeyPath, err.Error())
}

env, err := cel.NewEnv(inputs...)
if err != nil {
return ctx.Error(CustomKeyValidationErrorKeyPath, err.Error())
}

for _, celExpression := range CELDefinitionSchema.CELExpressions {
ast, issues := env.Compile(celExpression.Expression)
if issues != nil && issues.Err() != nil {
return ctx.Error(CustomKeyValidationErrorKeyPath, "cel expression compile error: %s", issues.Err())
}

prg, err := env.Program(ast)
if err != nil {
return ctx.Error(CustomKeyValidationErrorKeyPath, "cel program construction error: %s", err)
}

res1, _, err := prg.Eval(resourceWithParentKey)
if err != nil {
return ctx.Error(CustomKeyValidationErrorKeyPath, "cel evaluation error: %s", err)
}

if res1.Type().TypeName() != "bool" {
return ctx.Error(CustomKeyValidationErrorKeyPath, "cel expression needs to return a boolean")
}

celReturnValue, ok := res1.Value().(bool)
if !ok {
return ctx.Error(CustomKeyValidationErrorKeyPath, "cel expression needs to return a boolean")
}
if !celReturnValue {
return ctx.Error(CELDefinitionCustomKey, "values in data value %v do not match", dataValue)
}
}

return nil
}

type CELExpression struct {
Expression string `json:"expression"`
}

type CELDefinition struct {
CELExpressions []CELExpression
}

func convertCustomKeyCELDefinitionSchemaToCELDefinitionSchema(CELDefinitionSchema CustomKeyCELDefinitionSchema) (*CELDefinition, error) {
var CELDefinition CELDefinition
for _, CELExpressionFromSchema := range CELDefinitionSchema {
var CELExpression CELExpression
b, err := json.Marshal(CELExpressionFromSchema)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why Marshal and then Unmarshal? if you just need a type conversion then use Go's type conversion. Am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok let's see, what is your suggestion?

if err != nil {
return nil, fmt.Errorf("CELExpression failed to marshal to json, %s", err.Error())
}
err = json.Unmarshal(b, &CELExpression)
if err != nil {
return nil, fmt.Errorf("CELExpression must be an object of type CELExpression %s", err.Error())
}
CELDefinition.CELExpressions = append(CELDefinition.CELExpressions, CELExpression)
}

return &CELDefinition, nil
}

func getCELEnvInputs(dataValue map[string]interface{}) ([]cel.EnvOption, error) {
inputVars := make([]cel.EnvOption, 0, len(dataValue))
for input := range dataValue {
inputVars = append(inputVars, cel.Variable(input, cel.DynType))
}
return inputVars, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"CELDefinition": [
{
"expression": "hassss(object.metadata.labels) && has(object.metadata.labels.billing)",
"message": "deployment labels should contain billing label"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"CELDefinition": [
{
"expression": 1,
"message": "deployment labels should contain billing label"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"CELDefinition": [
{
"expression": "has(object.metadata.labels) && has(object.metadata.labels.billing)",
"message": "deployment labels should contain billing label"
}
]
}
1 change: 1 addition & 0 deletions pkg/jsonSchemaValidator/validator.go
Original file line number Diff line number Diff line change
@@ -73,6 +73,7 @@ func (jsv *JSONSchemaValidator) Validate(schemaContent string, yamlContent []byt
compiler.RegisterExtension("customKeyRule89", extensions.CustomKeyRule89, extensions.CustomKeyRule89Compiler{})
compiler.RegisterExtension("customKeyRule101", extensions.CustomKeyRule101, extensions.CustomKeyRule101Compiler{})
compiler.RegisterExtension("customKeyRegoRule", extensions.CustomKeyRegoRule, extensions.CustomKeyRegoDefinitionCompiler{})
compiler.RegisterExtension("customKeyCELRule", extensions.CustomKeyCELRule, extensions.CustomKeyCELDefinitionCompiler{})

// compiler.Compile() is an expensive operation. We cache the compiled schema in rulesSchemasCache to avoid re-compiling the same schema.
schemaAny, ok := jsv.rulesSchemasCache.Load(schemaContent)
110 changes: 99 additions & 11 deletions pkg/jsonSchemaValidator/validator_test.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package jsonSchemaValidator
import (
_ "embed"
"encoding/json"
"fmt"
"strings"
"testing"

@@ -49,14 +50,20 @@ func TestValidateResourceMinMaxCustomKeysPass(t *testing.T) {
//go:embed test_fixtures/invalid-rego-definition.json
var invalidRegoDefinitionJson string

//go:embed test_fixtures/invalid-cel-definition.json
var invalidCELDefinitionJson string

//go:embed test_fixtures/valid-rego-definition.json
var validRegoDefinitionJson string

//go:embed test_fixtures/rego-rule-fail.yaml
var regoRuleFail string
//go:embed test_fixtures/valid-cel-definition.json
var validCELDefinitionJson string

//go:embed test_fixtures/rule-fail.yaml
var ruleFail string

//go:embed test_fixtures/rego-rule-pass.yaml
var regoRulePass string
//go:embed test_fixtures/rule-pass.yaml
var rulePass string

func TestRegoDefinitionCustomKey(t *testing.T) {
t.Run("invalidSchema", func(t *testing.T) {
@@ -83,7 +90,7 @@ func TestRegoDefinitionCustomKey(t *testing.T) {
t.Fatal(err)
}
t.Run("validInstance", func(t *testing.T) {
jsonYamlContent, err := getInterfaceFromYamlContext(regoRulePass)
jsonYamlContent, err := getInterfaceFromYamlContext(rulePass)
if err != nil {
t.Fatal(err)
}
@@ -93,7 +100,7 @@ func TestRegoDefinitionCustomKey(t *testing.T) {
}
})
t.Run("invalidInstance", func(t *testing.T) {
jsonYamlContent, err := getInterfaceFromYamlContext(regoRuleFail)
jsonYamlContent, err := getInterfaceFromYamlContext(ruleFail)
if err != nil {
t.Fatal(err)
}
@@ -112,7 +119,7 @@ func TestRegoDefinitionCustomKey(t *testing.T) {
func TestValidateRegoDefinitionCustomKeyPass(t *testing.T) {
passResourceYamlFileContent, customRuleSchemaYamlFileContent :=
getResourceAndSchemaYamlContentsAsString(
regoYamlFilesPath+"/rego-rule-pass.yaml",
regoYamlFilesPath+"/rule-pass.yaml",
regoYamlFilesPath+"/valid-rego-definition.json",
)

@@ -140,7 +147,7 @@ func TestValidateRegoDefinitionCustomKeyPassDueToResourceNotInConstraint(t *test
func TestValidateRegoDefinitionCustomKeyFail(t *testing.T) {
failResourceYamlFileContent, customRuleSchemaYamlFileContent :=
getResourceAndSchemaYamlContentsAsString(
regoYamlFilesPath+"/rego-rule-fail.yaml",
regoYamlFilesPath+"/rule-fail.yaml",
regoYamlFilesPath+"/valid-rego-definition.json",
)

@@ -155,7 +162,7 @@ func TestValidateRegoDefinitionCustomKeyFail(t *testing.T) {
func TestValidateRegoDefinitionCustomKeyFailDueToRegoCompile(t *testing.T) {
failResourceYamlFileContent, customRuleSchemaYamlFileContent :=
getResourceAndSchemaYamlContentsAsString(
regoYamlFilesPath+"/rego-rule-pass.yaml",
regoYamlFilesPath+"/rule-pass.yaml",
regoYamlFilesPath+"/invalid-rego-definition-code.json",
)

@@ -167,6 +174,87 @@ func TestValidateRegoDefinitionCustomKeyFailDueToRegoCompile(t *testing.T) {
assert.Contains(t, errorsResult[0].Error, "can't compile rego code, rego code must have a package")
}

func TestCELDefinitionCustomKey(t *testing.T) {
t.Run("invalidSchema", func(t *testing.T) {
c := jsonschema.NewCompiler()
c.RegisterExtension(extensions.CELDefinitionCustomKey, extensions.CustomKeyCELRule, extensions.CustomKeyCELDefinitionCompiler{})
err := c.AddResource("test.json", strings.NewReader(invalidCELDefinitionJson))
if err != nil {
t.Fatal(err)
}
_, err = c.Compile("test.json")
if err == nil {
t.Fatal("error expected")
}
assert.Contains(t, err.Error(), "CELExpression must be an object of type CELExpression json: cannot unmarshal number into Go struct field CELExpression.expression of type string")
})
t.Run("validSchema", func(t *testing.T) {
c := jsonschema.NewCompiler()
c.RegisterExtension(extensions.CELDefinitionCustomKey, extensions.CustomKeyCELRule, extensions.CustomKeyCELDefinitionCompiler{})
if err := c.AddResource("test.json", strings.NewReader(validCELDefinitionJson)); err != nil {
t.Fatal(err)
}
schema, err := c.Compile("test.json")
if err != nil {
t.Fatal(err)
}
t.Run("validInstance", func(t *testing.T) {
jsonYamlContent, err := getInterfaceFromYamlContext(rulePass)
if err != nil {
t.Fatal(err)
}

if err := schema.Validate(jsonYamlContent); err != nil {
t.Fatal(err)
}
})
t.Run("invalidInstance", func(t *testing.T) {
jsonYamlContent, err := getInterfaceFromYamlContext(ruleFail)
if err != nil {
t.Fatal(err)
}
if err := schema.Validate(jsonYamlContent); err == nil {
t.Fatal("validation must fail")
} else {
fmt.Println(err.(*jsonschema.ValidationError).GoString())
t.Logf("%#v", err)
if !strings.Contains(err.(*jsonschema.ValidationError).GoString(), "doesn't validate") {
t.Fatal("validation error expected to contain CELDefinition message")
}
}
})
})
}

func TestValidateCELDefinitionCustomKeyPass(t *testing.T) {
passResourceYamlFileContent, customRuleSchemaYamlFileContent :=
getResourceAndSchemaYamlContentsAsString(
regoYamlFilesPath+"/rule-pass.yaml",
regoYamlFilesPath+"/valid-cel-definition.json",
)

jsonSchemaValidator := New()

errorsResult, _ := jsonSchemaValidator.ValidateYamlSchema(customRuleSchemaYamlFileContent, passResourceYamlFileContent)

assert.Empty(t, errorsResult)
}

func TestValidateCELDefinitionCustomKeyFailDueToCELCompile(t *testing.T) {
failResourceYamlFileContent, customRuleSchemaYamlFileContent :=
getResourceAndSchemaYamlContentsAsString(
regoYamlFilesPath+"/rule-pass.yaml",
regoYamlFilesPath+"/invalid-cel-definition-expression.json",
)

jsonSchemaValidator := New()

errorsResult, _ := jsonSchemaValidator.ValidateYamlSchema(customRuleSchemaYamlFileContent, failResourceYamlFileContent)

assert.GreaterOrEqual(t, len(errorsResult), 1)
assert.Contains(t, errorsResult[0].Error, "cel expression compile error")
}

func getResourceAndSchemaYamlContentsAsString(resourceToValidatePath string, schemaPath string) (string, string) {
fileReader := fileReader.CreateFileReader(nil)

@@ -185,8 +273,8 @@ func getResourceAndSchemaYamlContentsAsString(resourceToValidatePath string, sch

func getInterfaceFromYamlContext(yamlContent string) (interface{}, error) {
var jsonYamlContent interface{}
regoRuleFailsYamlBytes, _ := yaml.YAMLToJSON([]byte(yamlContent))
err := json.Unmarshal(regoRuleFailsYamlBytes, &jsonYamlContent)
ruleFailsYamlBytes, _ := yaml.YAMLToJSON([]byte(yamlContent))
err := json.Unmarshal(ruleFailsYamlBytes, &jsonYamlContent)
if err != nil {
return nil, err
}