Skip to content

Commit 8bc02b6

Browse files
committed
feat(cel-proto-parser): add policy and RLS template fixtures, fix converter
- Add policy.txt with 34 OPA-ish policy expressions (Kubernetes admission control patterns) - Add rls-templates.txt with 17 RLS policy template expressions (direct_owner, membership, etc.) - Fix converter to handle index access operator '[]' (was only handling 'index') - Fix converter to handle unary negation '!_' with single node args (not array) - Note: @marcbachmann/cel-js does not support bitwise AND (&) operator Round-trip test results: - basic.txt: 16/16 pass - policy.txt: 34/34 pass - rls-templates.txt: 17/17 pass
1 parent 4f3f7be commit 8bc02b6

3 files changed

Lines changed: 114 additions & 5 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Policy-Shaped CEL Expression Fixtures
2+
# OPA-ish patterns useful for Kubernetes admission control
3+
# Rich in syntax variety for deparser testing
4+
5+
# 1) Core allow/deny shapes
6+
request.user == "system:admin"
7+
request.user in ["alice", "bob", "carol"]
8+
request.verb in ["create", "update"] && request.resource == "pods"
9+
!(request.user.startsWith("system:"))
10+
request.namespace != "kube-system" && request.namespace != "kube-public"
11+
12+
# 2) Field presence / null-ish handling
13+
has(object.metadata.labels) && has(object.metadata.labels["app"])
14+
!has(object.spec.replicas) || object.spec.replicas <= 10
15+
has(object.spec.template) ? object.spec.template.spec.nodeName == "" : true
16+
has(object.spec.securityContext) && has(object.spec.securityContext.runAsNonRoot) && object.spec.securityContext.runAsNonRoot == true
17+
has(params.exemptUsers) && request.user in params.exemptUsers
18+
19+
# 3) Map + list literals, indexing, membership
20+
object.metadata.labels["team"] in ["infra", "platform"]
21+
object.metadata.annotations["owner"] == request.user
22+
size(object.spec.containers) > 0
23+
object.spec.containers[0].image.matches("^ghcr\\.io/constructive-io/.+")
24+
25+
# 4) String ops + regex escaping edge cases
26+
request.user.matches("^user:[a-z0-9_-]{3,32}$")
27+
object.metadata.name.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
28+
object.spec.containers.exists(c, c.name.startsWith("sidecar-") && c.image.contains("@sha256:"))
29+
object.spec.serviceAccountName != "" && !object.spec.serviceAccountName.startsWith("default")
30+
31+
# 5) Quantifiers/macros (great for AST/deparser)
32+
object.spec.containers.all(c, !c.securityContext.privileged)
33+
object.spec.containers.exists(c, has(c.resources.limits["cpu"]) && has(c.resources.limits["memory"]))
34+
object.spec.containers.filter(c, c.image.contains(":latest")).size() == 0
35+
object.spec.containers.map(c, c.name).exists(n, n == "app")
36+
37+
# 6) Nested logic + precedence stress tests
38+
request.verb == "update" && (request.user == "alice" || request.user == "bob") && !request.dryRun
39+
(request.user in params.admins) || (request.namespace == "dev" && request.user.matches("^user:dev_.*$"))
40+
!(request.verb == "delete" && request.resource == "namespaces")
41+
42+
# 7) Numeric + bounds + ternary
43+
has(object.spec.replicas) ? (object.spec.replicas >= 1 && object.spec.replicas <= 20) : true
44+
object.spec.containers.all(c, c.resources.requests["cpu"] <= c.resources.limits["cpu"])
45+
46+
# 8) "OPA-ish" patterns (rule-ish in one expression)
47+
request.user in params.admins || (request.verb in ["get","list","watch"] && request.namespace in params.readNamespaces)
48+
request.user == object.metadata.annotations["owner"] || request.user in params.delegates[object.metadata.annotations["owner"]]
49+
50+
# 9) Equality against structured literals (map/list)
51+
object.metadata.labels == {"app": "api", "tier": "backend"}
52+
object.spec.tolerations.exists(t, t == {"key":"dedicated","operator":"Equal","value":"gpu","effect":"NoSchedule"})
53+
54+
# 10) "Weird but valid" deparser edge cases
55+
"" == ""
56+
true ? false ? true : false : true
57+
((a + b) * c) / (d - 1) > 0
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# RLS Policy Template CEL Expressions
2+
# Maps to common policy templates: direct_owner, membership, permission bits, etc.
3+
# Uses: row (table row), auth (auth context), acl (ACL table view), params (compile-time params)
4+
5+
# 1) direct_owner (row.owner_id == user_id)
6+
row.owner_id == auth.user_id
7+
has(row.owner_id) && row.owner_id == auth.user_id
8+
9+
# 2) multi_owner / any_owner (row.sender_id OR row.receiver_id)
10+
auth.user_id in [row.sender_id, row.receiver_id]
11+
12+
# 3) membership_by_field (row.org_id exists in ACL for user)
13+
acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_type == "org" && m.entity_id == row.org_id)
14+
15+
# 4) membership_by_join (row -> join table -> memberships)
16+
acl.joins.exists(j, j.row_id == row.id && acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_type == "group" && m.entity_id == j.entity_id))
17+
18+
# 5) scoped_owner / scope_owner (membership exists AND matches scope)
19+
acl.memberships.exists(m, m.user_id == auth.user_id && m.scope == "project" && m.entity_id == row.project_id)
20+
acl.memberships.exists(m, m.user_id == auth.user_id && m.membership_type == params.membership_type && m.entity_id == row.entity_id)
21+
22+
# 6) hasAccess / permission-bit checks (bitmask)
23+
# NOTE: @marcbachmann/cel-js does not support bitwise AND (&) operator
24+
# These would be: acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_id == row.org_id && (m.perms & params.required_perm) != 0)
25+
26+
# 7) Role-based overrides (auth.roles)
27+
("admin" in auth.roles) || (row.owner_id == auth.user_id)
28+
auth.user.startsWith("system:") || row.owner_id == auth.user_id
29+
30+
# 8) acl_exists (no entity match; "member of ANY org")
31+
acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_type == "org")
32+
33+
# 9) relatedAccess / relationship ownership (row references another object)
34+
acl.parents.exists(p, p.id == row.parent_id && (p.owner_id == auth.user_id || acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_id == p.org_id)))
35+
36+
# 10) Array membership / group array checks
37+
has(row.allowed_user_ids) && auth.user_id in row.allowed_user_ids
38+
row.group_ids.exists(gid, acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_type == "group" && m.entity_id == gid))
39+
40+
# 11) "Owner OR membership" compound policy (very common)
41+
row.owner_id == auth.user_id || acl.memberships.exists(m, m.user_id == auth.user_id && m.entity_id == row.org_id)
42+
43+
# 12) Insert/update "WITH CHECK" style (new row constraints)
44+
row.owner_id == auth.user_id
45+
newRow.owner_id == auth.user_id
46+
request.verb == "update" ? (newRow.owner_id == oldRow.owner_id) : true

packages/cel-proto-parser/src/converter/index.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ export function convertToProtoExpr(node: MarcAstNode, idCounter = { value: 1 }):
147147
};
148148
}
149149

150-
case 'index': {
150+
case 'index':
151+
case '[]': {
151152
// Index access: obj[key]
152153
const args = node.args as [MarcAstNode, MarcAstNode];
153154
return {
@@ -187,14 +188,19 @@ export function convertToProtoExpr(node: MarcAstNode, idCounter = { value: 1 }):
187188

188189
// Unary operators
189190
case '!':
190-
case 'neg': {
191-
const args = node.args as [MarcAstNode];
192-
const funcName = node.op === '!' ? '!_' : '-_';
191+
case '!_':
192+
case 'neg':
193+
case '-_': {
194+
// Args can be either a single node or an array with one element
195+
const argNode = Array.isArray(node.args)
196+
? (node.args as MarcAstNode[])[0]
197+
: (node.args as MarcAstNode);
198+
const funcName = node.op === '!' || node.op === '!_' ? '!_' : '-_';
193199
return {
194200
id,
195201
callExpr: {
196202
function: funcName,
197-
args: args.map((a) => convertToProtoExpr(a, idCounter))
203+
args: [convertToProtoExpr(argNode, idCounter)]
198204
}
199205
};
200206
}

0 commit comments

Comments
 (0)