@orygn/opa-mcp 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/LICENSE +21 -0
- package/README.md +622 -2
- package/dist/config.d.ts +58 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +72 -0
- package/dist/config.js.map +1 -0
- package/dist/lib/errors.d.ts +18 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +25 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/logger.d.ts +10 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +57 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/opa-cli.d.ts +226 -0
- package/dist/lib/opa-cli.d.ts.map +1 -0
- package/dist/lib/opa-cli.js +283 -0
- package/dist/lib/opa-cli.js.map +1 -0
- package/dist/lib/opa-client.d.ts +47 -0
- package/dist/lib/opa-client.d.ts.map +1 -0
- package/dist/lib/opa-client.js +101 -0
- package/dist/lib/opa-client.js.map +1 -0
- package/dist/lib/output.d.ts +17 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +21 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/regal-cli.d.ts +79 -0
- package/dist/lib/regal-cli.d.ts.map +1 -0
- package/dist/lib/regal-cli.js +122 -0
- package/dist/lib/regal-cli.js.map +1 -0
- package/dist/lib/security.d.ts +16 -0
- package/dist/lib/security.d.ts.map +1 -0
- package/dist/lib/security.js +64 -0
- package/dist/lib/security.js.map +1 -0
- package/dist/lib/subprocess.d.ts +26 -0
- package/dist/lib/subprocess.d.ts.map +1 -0
- package/dist/lib/subprocess.js +72 -0
- package/dist/lib/subprocess.js.map +1 -0
- package/dist/lib/tool-helpers.d.ts +46 -0
- package/dist/lib/tool-helpers.d.ts.map +1 -0
- package/dist/lib/tool-helpers.js +73 -0
- package/dist/lib/tool-helpers.js.map +1 -0
- package/dist/prompts/index.d.ts +4 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +147 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/resources/index.d.ts +12 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +107 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/patterns.d.ts +9 -0
- package/dist/resources/patterns.d.ts.map +1 -0
- package/dist/resources/patterns.js +375 -0
- package/dist/resources/patterns.js.map +1 -0
- package/dist/resources/style-guide.d.ts +6 -0
- package/dist/resources/style-guide.d.ts.map +1 -0
- package/dist/resources/style-guide.js +187 -0
- package/dist/resources/style-guide.js.map +1 -0
- package/dist/server.d.ts +34 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +125 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/authoring/capabilities.d.ts +11 -0
- package/dist/tools/authoring/capabilities.d.ts.map +1 -0
- package/dist/tools/authoring/capabilities.js +57 -0
- package/dist/tools/authoring/capabilities.js.map +1 -0
- package/dist/tools/authoring/check.d.ts +18 -0
- package/dist/tools/authoring/check.d.ts.map +1 -0
- package/dist/tools/authoring/check.js +71 -0
- package/dist/tools/authoring/check.js.map +1 -0
- package/dist/tools/authoring/deps.d.ts +8 -0
- package/dist/tools/authoring/deps.d.ts.map +1 -0
- package/dist/tools/authoring/deps.js +51 -0
- package/dist/tools/authoring/deps.js.map +1 -0
- package/dist/tools/authoring/format.d.ts +8 -0
- package/dist/tools/authoring/format.d.ts.map +1 -0
- package/dist/tools/authoring/format.js +44 -0
- package/dist/tools/authoring/format.js.map +1 -0
- package/dist/tools/authoring/index.d.ts +11 -0
- package/dist/tools/authoring/index.d.ts.map +1 -0
- package/dist/tools/authoring/index.js +17 -0
- package/dist/tools/authoring/index.js.map +1 -0
- package/dist/tools/authoring/inspect.d.ts +10 -0
- package/dist/tools/authoring/inspect.d.ts.map +1 -0
- package/dist/tools/authoring/inspect.js +47 -0
- package/dist/tools/authoring/inspect.js.map +1 -0
- package/dist/tools/authoring/lint.d.ts +18 -0
- package/dist/tools/authoring/lint.d.ts.map +1 -0
- package/dist/tools/authoring/lint.js +111 -0
- package/dist/tools/authoring/lint.js.map +1 -0
- package/dist/tools/authoring/parse.d.ts +7 -0
- package/dist/tools/authoring/parse.d.ts.map +1 -0
- package/dist/tools/authoring/parse.js +43 -0
- package/dist/tools/authoring/parse.js.map +1 -0
- package/dist/tools/bundles/build.d.ts +8 -0
- package/dist/tools/bundles/build.d.ts.map +1 -0
- package/dist/tools/bundles/build.js +87 -0
- package/dist/tools/bundles/build.js.map +1 -0
- package/dist/tools/bundles/index.d.ts +10 -0
- package/dist/tools/bundles/index.d.ts.map +1 -0
- package/dist/tools/bundles/index.js +7 -0
- package/dist/tools/bundles/index.js.map +1 -0
- package/dist/tools/bundles/sign.d.ts +8 -0
- package/dist/tools/bundles/sign.d.ts.map +1 -0
- package/dist/tools/bundles/sign.js +47 -0
- package/dist/tools/bundles/sign.js.map +1 -0
- package/dist/tools/evaluation/_shared.d.ts +55 -0
- package/dist/tools/evaluation/_shared.d.ts.map +1 -0
- package/dist/tools/evaluation/_shared.js +102 -0
- package/dist/tools/evaluation/_shared.js.map +1 -0
- package/dist/tools/evaluation/bench.d.ts +9 -0
- package/dist/tools/evaluation/bench.d.ts.map +1 -0
- package/dist/tools/evaluation/bench.js +76 -0
- package/dist/tools/evaluation/bench.js.map +1 -0
- package/dist/tools/evaluation/compile.d.ts +11 -0
- package/dist/tools/evaluation/compile.d.ts.map +1 -0
- package/dist/tools/evaluation/compile.js +19 -0
- package/dist/tools/evaluation/compile.js.map +1 -0
- package/dist/tools/evaluation/eval.d.ts +10 -0
- package/dist/tools/evaluation/eval.d.ts.map +1 -0
- package/dist/tools/evaluation/eval.js +35 -0
- package/dist/tools/evaluation/eval.js.map +1 -0
- package/dist/tools/evaluation/index.d.ts +11 -0
- package/dist/tools/evaluation/index.d.ts.map +1 -0
- package/dist/tools/evaluation/index.js +11 -0
- package/dist/tools/evaluation/index.js.map +1 -0
- package/dist/tools/evaluation/test.d.ts +28 -0
- package/dist/tools/evaluation/test.d.ts.map +1 -0
- package/dist/tools/evaluation/test.js +74 -0
- package/dist/tools/evaluation/test.js.map +1 -0
- package/dist/tools/helpers/describe-policy.d.ts +26 -0
- package/dist/tools/helpers/describe-policy.d.ts.map +1 -0
- package/dist/tools/helpers/describe-policy.js +76 -0
- package/dist/tools/helpers/describe-policy.js.map +1 -0
- package/dist/tools/helpers/explain-decision.d.ts +41 -0
- package/dist/tools/helpers/explain-decision.d.ts.map +1 -0
- package/dist/tools/helpers/explain-decision.js +75 -0
- package/dist/tools/helpers/explain-decision.js.map +1 -0
- package/dist/tools/helpers/generate-test-skeleton.d.ts +8 -0
- package/dist/tools/helpers/generate-test-skeleton.d.ts.map +1 -0
- package/dist/tools/helpers/generate-test-skeleton.js +93 -0
- package/dist/tools/helpers/generate-test-skeleton.js.map +1 -0
- package/dist/tools/helpers/index.d.ts +12 -0
- package/dist/tools/helpers/index.d.ts.map +1 -0
- package/dist/tools/helpers/index.js +11 -0
- package/dist/tools/helpers/index.js.map +1 -0
- package/dist/tools/helpers/suggest-fix.d.ts +15 -0
- package/dist/tools/helpers/suggest-fix.d.ts.map +1 -0
- package/dist/tools/helpers/suggest-fix.js +119 -0
- package/dist/tools/helpers/suggest-fix.js.map +1 -0
- package/dist/tools/index.d.ts +31 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +13 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/server-management/_shared.d.ts +9 -0
- package/dist/tools/server-management/_shared.d.ts.map +1 -0
- package/dist/tools/server-management/_shared.js +41 -0
- package/dist/tools/server-management/_shared.js.map +1 -0
- package/dist/tools/server-management/data.d.ts +4 -0
- package/dist/tools/server-management/data.d.ts.map +1 -0
- package/dist/tools/server-management/data.js +90 -0
- package/dist/tools/server-management/data.js.map +1 -0
- package/dist/tools/server-management/decisions.d.ts +4 -0
- package/dist/tools/server-management/decisions.d.ts.map +1 -0
- package/dist/tools/server-management/decisions.js +87 -0
- package/dist/tools/server-management/decisions.js.map +1 -0
- package/dist/tools/server-management/index.d.ts +11 -0
- package/dist/tools/server-management/index.d.ts.map +1 -0
- package/dist/tools/server-management/index.js +11 -0
- package/dist/tools/server-management/index.js.map +1 -0
- package/dist/tools/server-management/policies.d.ts +4 -0
- package/dist/tools/server-management/policies.d.ts.map +1 -0
- package/dist/tools/server-management/policies.js +94 -0
- package/dist/tools/server-management/policies.js.map +1 -0
- package/dist/tools/server-management/status.d.ts +4 -0
- package/dist/tools/server-management/status.d.ts.map +1 -0
- package/dist/tools/server-management/status.js +82 -0
- package/dist/tools/server-management/status.js.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +86 -4
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { OpaCli } from '../lib/opa-cli.js';
|
|
2
|
+
import { tryParseJson } from '../lib/tool-helpers.js';
|
|
3
|
+
import { STYLE_GUIDE } from './style-guide.js';
|
|
4
|
+
import { PATTERNS } from './patterns.js';
|
|
5
|
+
function categorizeBuiltins(caps) {
|
|
6
|
+
const grouped = {};
|
|
7
|
+
for (const builtin of caps.builtins ?? []) {
|
|
8
|
+
if (!builtin.name)
|
|
9
|
+
continue;
|
|
10
|
+
const categoryList = builtin.categories ?? [];
|
|
11
|
+
if (categoryList.length === 0) {
|
|
12
|
+
grouped['uncategorized'] ??= [];
|
|
13
|
+
grouped['uncategorized'].push(builtin.name);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
for (const category of categoryList) {
|
|
17
|
+
grouped[category] ??= [];
|
|
18
|
+
grouped[category].push(builtin.name);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const list of Object.values(grouped))
|
|
22
|
+
list.sort();
|
|
23
|
+
return grouped;
|
|
24
|
+
}
|
|
25
|
+
const SECURITY_SENSITIVE = new Set([
|
|
26
|
+
'http.send',
|
|
27
|
+
'opa.runtime',
|
|
28
|
+
'crypto.x509.parse_certificates',
|
|
29
|
+
'crypto.x509.parse_certificate_request',
|
|
30
|
+
'crypto.x509.parse_keypair',
|
|
31
|
+
'crypto.x509.parse_rsa_private_key',
|
|
32
|
+
'crypto.parse_private_keys',
|
|
33
|
+
]);
|
|
34
|
+
async function buildBuiltinsResource(opa) {
|
|
35
|
+
const result = await opa.capabilities({ current: true });
|
|
36
|
+
if (result.exitCode !== 0) {
|
|
37
|
+
return JSON.stringify({
|
|
38
|
+
error: 'opa capabilities --current failed',
|
|
39
|
+
stderr: result.stderr.trim(),
|
|
40
|
+
}, null, 2);
|
|
41
|
+
}
|
|
42
|
+
const caps = tryParseJson(result.stdout);
|
|
43
|
+
if (!caps) {
|
|
44
|
+
return JSON.stringify({ error: 'opa capabilities produced no parseable JSON' }, null, 2);
|
|
45
|
+
}
|
|
46
|
+
const grouped = categorizeBuiltins(caps);
|
|
47
|
+
const sensitive = (caps.builtins ?? [])
|
|
48
|
+
.map((b) => b.name)
|
|
49
|
+
.filter((n) => typeof n === 'string' && SECURITY_SENSITIVE.has(n))
|
|
50
|
+
.sort();
|
|
51
|
+
return JSON.stringify({
|
|
52
|
+
version_note: 'Derived at read time from `opa capabilities --current`. Reflects the OPA build linked into this MCP server, not necessarily the OPA the user has installed locally.',
|
|
53
|
+
builtin_count: caps.builtins?.length ?? 0,
|
|
54
|
+
categories: grouped,
|
|
55
|
+
future_keywords: caps.future_keywords ?? [],
|
|
56
|
+
features: caps.features ?? [],
|
|
57
|
+
wasm_abi_versions: caps.wasm_abi_versions ?? [],
|
|
58
|
+
security_sensitive_builtins: sensitive,
|
|
59
|
+
security_note: 'The functions in `security_sensitive_builtins` introduce side effects (HTTP fetches, crypto operations, runtime introspection). Use them sparingly in policy hot paths and confirm operator approval.',
|
|
60
|
+
}, null, 2);
|
|
61
|
+
}
|
|
62
|
+
export function registerResources(server, config) {
|
|
63
|
+
const opa = new OpaCli(config);
|
|
64
|
+
server.registerResource('opa-builtins', 'opa://builtins', {
|
|
65
|
+
title: 'OPA built-in functions',
|
|
66
|
+
description: 'The OPA built-in function catalog, categorized by namespace, with security-sensitive functions flagged. Derived at read time from `opa capabilities --current` so the list stays in sync with the actual OPA binary.',
|
|
67
|
+
mimeType: 'application/json',
|
|
68
|
+
}, async (uri) => {
|
|
69
|
+
const content = await buildBuiltinsResource(opa);
|
|
70
|
+
return {
|
|
71
|
+
contents: [
|
|
72
|
+
{
|
|
73
|
+
uri: uri.href,
|
|
74
|
+
mimeType: 'application/json',
|
|
75
|
+
text: content,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
server.registerResource('opa-style-guide', 'opa://style-guide', {
|
|
81
|
+
title: 'Rego style guide',
|
|
82
|
+
description: 'Condensed Rego style guide adapted from the Styra reference: rego.v1, package layout, naming, default-deny, comprehensions vs every, schema annotations.',
|
|
83
|
+
mimeType: 'text/markdown',
|
|
84
|
+
}, (uri) => Promise.resolve({
|
|
85
|
+
contents: [
|
|
86
|
+
{
|
|
87
|
+
uri: uri.href,
|
|
88
|
+
mimeType: 'text/markdown',
|
|
89
|
+
text: STYLE_GUIDE,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}));
|
|
93
|
+
server.registerResource('opa-patterns', 'opa://patterns', {
|
|
94
|
+
title: 'Rego pattern library',
|
|
95
|
+
description: 'Curated Rego patterns: RBAC, ABAC, Kubernetes admission, IaC gates, API authorization, rate limiting. Each pattern includes when to use it, a full working example, a test, and common pitfalls.',
|
|
96
|
+
mimeType: 'text/markdown',
|
|
97
|
+
}, (uri) => Promise.resolve({
|
|
98
|
+
contents: [
|
|
99
|
+
{
|
|
100
|
+
uri: uri.href,
|
|
101
|
+
mimeType: 'text/markdown',
|
|
102
|
+
text: PATTERNS,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated Rego pattern library. Each pattern includes:
|
|
3
|
+
* - When to use it
|
|
4
|
+
* - A working example
|
|
5
|
+
* - A test
|
|
6
|
+
* - Common pitfalls
|
|
7
|
+
*/
|
|
8
|
+
export declare const PATTERNS = "# Rego pattern library\n\nCommon Rego patterns with working examples, tests, and pitfalls. Each\npattern is self-contained \u2014 copy, adapt, and ship.\n\n---\n\n## 1. Role-based access control (RBAC)\n\n**When to use:** the simplest authorization model. Every user has one\nor more roles; each role grants a set of actions on a set of\nresources. Sufficient for ~80% of internal applications.\n\n```rego\npackage rbac\n\nimport rego.v1\n\ndefault allow := false\n\n# Permissions table \u2014 extend as roles evolve.\npermissions := {\n \"admin\": {\"read\", \"write\", \"delete\", \"manage_users\"},\n \"editor\": {\"read\", \"write\"},\n \"viewer\": {\"read\"},\n}\n\nallow if {\n some role in input.user.roles\n input.action in permissions[role]\n}\n\n# Why was it denied? \u2014 useful for audit logs.\ndeny_reasons contains reason if {\n not allow\n input.user\n reason := sprintf(\n \"user %q has roles %v, none grant %q\",\n [input.user.id, input.user.roles, input.action],\n )\n}\n\ndeny_reasons contains \"anonymous request\" if {\n not allow\n not input.user\n}\n```\n\n**Test:**\n\n```rego\npackage rbac_test\n\nimport rego.v1\nimport data.rbac\n\ntest_admin_can_delete if {\n rbac.allow with input as {\n \"user\": {\"id\": \"alice\", \"roles\": [\"admin\"]},\n \"action\": \"delete\",\n }\n}\n\ntest_viewer_cannot_delete if {\n not rbac.allow with input as {\n \"user\": {\"id\": \"bob\", \"roles\": [\"viewer\"]},\n \"action\": \"delete\",\n }\n}\n\ntest_anonymous_denied if {\n not rbac.allow with input as {\"action\": \"read\"}\n \"anonymous request\" in (rbac.deny_reasons with input as {\"action\": \"read\"})\n}\n```\n\n**Pitfalls:**\n- The `permissions` table grows unbounded. Move to data files\n (`data.permissions`) once you have more than ~20 roles.\n- Roles overlap with groups in real auth systems; map at the boundary\n rather than carrying both.\n\n---\n\n## 2. Attribute-based access control (ABAC)\n\n**When to use:** when \"who\" alone isn't enough \u2014 decisions also depend\non resource attributes (ownership, tenant, sensitivity) and context\n(time of day, source IP).\n\n```rego\npackage abac\n\nimport rego.v1\n\ndefault allow := false\n\n# A user can read any resource they own.\nallow if {\n input.action == \"read\"\n input.resource.owner_id == input.user.id\n}\n\n# A user can read shared resources at their organization.\nallow if {\n input.action == \"read\"\n input.resource.shared\n input.resource.org_id == input.user.org_id\n}\n\n# Admins can do anything within their organization.\nallow if {\n \"admin\" in input.user.roles\n input.resource.org_id == input.user.org_id\n}\n\n# Don't show \"secret\" resources to anyone outside the owner's\n# organization, even admins.\nallow := false if {\n input.resource.classification == \"secret\"\n input.resource.org_id != input.user.org_id\n}\n```\n\n**Test:**\n\n```rego\npackage abac_test\n\nimport rego.v1\nimport data.abac\n\ntest_owner_reads_own if {\n abac.allow with input as {\n \"action\": \"read\",\n \"user\": {\"id\": \"u1\", \"org_id\": \"o1\"},\n \"resource\": {\"owner_id\": \"u1\", \"org_id\": \"o1\"},\n }\n}\n\ntest_admin_blocked_from_secret_in_other_org if {\n not abac.allow with input as {\n \"action\": \"read\",\n \"user\": {\"id\": \"u1\", \"org_id\": \"o1\", \"roles\": [\"admin\"]},\n \"resource\": {\n \"owner_id\": \"u2\",\n \"org_id\": \"o2\",\n \"classification\": \"secret\",\n },\n }\n}\n```\n\n**Pitfalls:**\n- Multiple `allow` rules combine with logical OR. Use explicit\n `allow := false if ...` to *override* an allow.\n- Don't compute attributes inside the policy; compute them at the\n boundary and pass via `input`.\n\n---\n\n## 3. Kubernetes admission control\n\n**When to use:** validate or mutate Kubernetes resources at admission\ntime. Run as a Gatekeeper, OPA-as-a-webhook, or Kyverno-equivalent\npolicy layer.\n\n```rego\npackage k8s.admission\n\nimport rego.v1\n\n# Reject pods without resource limits.\ndeny contains msg if {\n input.request.kind.kind == \"Pod\"\n container := input.request.object.spec.containers[_]\n not container.resources.limits.memory\n msg := sprintf(\n \"Pod %q container %q is missing resources.limits.memory\",\n [input.request.object.metadata.name, container.name],\n )\n}\n\n# Reject privileged containers in production.\ndeny contains msg if {\n input.request.kind.kind == \"Pod\"\n input.request.namespace != \"kube-system\"\n container := input.request.object.spec.containers[_]\n container.securityContext.privileged == true\n msg := sprintf(\n \"privileged containers are not allowed: %q in %q\",\n [container.name, input.request.object.metadata.name],\n )\n}\n```\n\n**Pitfalls:**\n- Use `input.request.object` for the resource being admitted; the\n envelope shape comes from Kubernetes, not your control.\n- For mutations, return a JSON Patch via the `patch` field. Test\n patches with the actual admission webhook in dry-run mode before\n enforcing.\n- Iteration order is undefined; never rely on `containers[0]` to mean\n anything specific.\n\n---\n\n## 4. Infrastructure-as-Code gates (Terraform)\n\n**When to use:** validate Terraform plans before apply. Catch overly\npermissive IAM, public S3 buckets, missing encryption.\n\n```rego\npackage terraform\n\nimport rego.v1\n\n# Reject S3 buckets without server-side encryption.\ndeny contains msg if {\n resource := input.resource_changes[_]\n resource.type == \"aws_s3_bucket\"\n after := resource.change.after\n not after.server_side_encryption_configuration\n msg := sprintf(\n \"S3 bucket %q has no server-side encryption configured\",\n [resource.address],\n )\n}\n\n# Reject IAM policies with action \"*\" on resource \"*\".\ndeny contains msg if {\n resource := input.resource_changes[_]\n resource.type == \"aws_iam_policy\"\n policy := json.unmarshal(resource.change.after.policy)\n statement := policy.Statement[_]\n statement.Effect == \"Allow\"\n \"*\" in statement.Action\n statement.Resource == \"*\"\n msg := sprintf(\"IAM policy %q grants Allow * on *\", [resource.address])\n}\n```\n\n**Pitfalls:**\n- Terraform plan JSON is verbose and version-specific. Pin the\n `terraform plan -json` schema you target.\n- `resource_changes[_].change.after` may be `null` for destroys \u2014\n guard against it.\n- For wide-radius changes, use `opa exec --decision` against a plan\n file in CI, not the live API.\n\n---\n\n## 5. API authorization (HTTP request gating)\n\n**When to use:** at the API gateway / reverse proxy layer, validate\neach request against the caller's identity and the requested\nendpoint.\n\n```rego\npackage api.authz\n\nimport rego.v1\n\ndefault allow := false\n\n# Public endpoints \u2014 no auth required.\npublic_endpoints := {\n {\"method\": \"GET\", \"path\": [\"health\"]},\n {\"method\": \"GET\", \"path\": [\"version\"]},\n}\n\nallow if some _ in public_endpoints; matches_endpoint(_)\n\n# Authenticated reads on resources the user has access to.\nallow if {\n input.method == \"GET\"\n input.user\n user_can_read(input.user, input.path)\n}\n\n# Authenticated writes only with specific scopes.\nallow if {\n input.method in {\"POST\", \"PUT\", \"PATCH\", \"DELETE\"}\n input.user\n \"write\" in input.user.scopes\n user_can_write(input.user, input.path)\n}\n\nmatches_endpoint(spec) if {\n spec.method == input.method\n spec.path == input.path\n}\n\nuser_can_read(user, path) if {\n path[0] == \"users\"\n user.id == path[1]\n}\n\nuser_can_write(user, path) if {\n path[0] == \"users\"\n user.id == path[1]\n}\n```\n\n**Pitfalls:**\n- `input.path` is typically an array (`[\"users\", \"alice\"]`), not a\n string. Build it consistently at the gateway.\n- Path-prefix matching is easy; full pattern matching is not. For\n parameterized routes, decode at the gateway and pass structured\n fields.\n\n---\n\n## 6. Rate limiting (with sliding window data)\n\n**When to use:** allow N requests per principal per window. Lightweight\nlimit enforcement; for high throughput, push to a dedicated rate\nlimiter.\n\n```rego\npackage rate\n\nimport rego.v1\n\n# Configuration: 100 requests per principal per 60-second window.\nlimit := 100\nwindow_seconds := 60\n\n# input.now is a unix timestamp in nanoseconds.\n# data.requests[principal] is an array of nanosecond timestamps.\n\ncurrent_window := requests if {\n requests := [t | some t in data.requests[input.principal]; t > input.now - window_seconds * 1000000000]\n}\n\ncount := count(current_window)\n\nallow if count < limit\n\ndeny_reason := sprintf(\n \"rate limit exceeded: %d requests in last %d seconds (limit %d)\",\n [count, window_seconds, limit],\n) if not allow\n```\n\n**Pitfalls:**\n- `data.requests` grows unbounded unless the writer prunes outside the\n window. Schedule prune on every write.\n- This pattern is *advisory* \u2014 under load, two concurrent decisions\n can both see `count == limit - 1` and both allow. For strict\n limits, use a Lua/Redis token bucket at the gateway and have OPA\n validate the token, not count requests.\n\n---\n\n## Where these patterns came from\n\nEach is distilled from production policy code. The full Rego files,\ntests, and policy data fixtures live in this server's GitHub\nrepository under `tests/fixtures/policies/`.\n\nFor more patterns, see:\n\n- OPA Playground: https://play.openpolicyagent.org/\n- Awesome OPA: https://github.com/anderseknert/awesome-opa\n- Styra DAS pattern library: https://docs.styra.com/das/policies\n";
|
|
9
|
+
//# sourceMappingURL=patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.d.ts","sourceRoot":"","sources":["../../src/resources/patterns.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,glTA8WpB,CAAC"}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated Rego pattern library. Each pattern includes:
|
|
3
|
+
* - When to use it
|
|
4
|
+
* - A working example
|
|
5
|
+
* - A test
|
|
6
|
+
* - Common pitfalls
|
|
7
|
+
*/
|
|
8
|
+
export const PATTERNS = `# Rego pattern library
|
|
9
|
+
|
|
10
|
+
Common Rego patterns with working examples, tests, and pitfalls. Each
|
|
11
|
+
pattern is self-contained — copy, adapt, and ship.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Role-based access control (RBAC)
|
|
16
|
+
|
|
17
|
+
**When to use:** the simplest authorization model. Every user has one
|
|
18
|
+
or more roles; each role grants a set of actions on a set of
|
|
19
|
+
resources. Sufficient for ~80% of internal applications.
|
|
20
|
+
|
|
21
|
+
\`\`\`rego
|
|
22
|
+
package rbac
|
|
23
|
+
|
|
24
|
+
import rego.v1
|
|
25
|
+
|
|
26
|
+
default allow := false
|
|
27
|
+
|
|
28
|
+
# Permissions table — extend as roles evolve.
|
|
29
|
+
permissions := {
|
|
30
|
+
"admin": {"read", "write", "delete", "manage_users"},
|
|
31
|
+
"editor": {"read", "write"},
|
|
32
|
+
"viewer": {"read"},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
allow if {
|
|
36
|
+
some role in input.user.roles
|
|
37
|
+
input.action in permissions[role]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Why was it denied? — useful for audit logs.
|
|
41
|
+
deny_reasons contains reason if {
|
|
42
|
+
not allow
|
|
43
|
+
input.user
|
|
44
|
+
reason := sprintf(
|
|
45
|
+
"user %q has roles %v, none grant %q",
|
|
46
|
+
[input.user.id, input.user.roles, input.action],
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
deny_reasons contains "anonymous request" if {
|
|
51
|
+
not allow
|
|
52
|
+
not input.user
|
|
53
|
+
}
|
|
54
|
+
\`\`\`
|
|
55
|
+
|
|
56
|
+
**Test:**
|
|
57
|
+
|
|
58
|
+
\`\`\`rego
|
|
59
|
+
package rbac_test
|
|
60
|
+
|
|
61
|
+
import rego.v1
|
|
62
|
+
import data.rbac
|
|
63
|
+
|
|
64
|
+
test_admin_can_delete if {
|
|
65
|
+
rbac.allow with input as {
|
|
66
|
+
"user": {"id": "alice", "roles": ["admin"]},
|
|
67
|
+
"action": "delete",
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test_viewer_cannot_delete if {
|
|
72
|
+
not rbac.allow with input as {
|
|
73
|
+
"user": {"id": "bob", "roles": ["viewer"]},
|
|
74
|
+
"action": "delete",
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test_anonymous_denied if {
|
|
79
|
+
not rbac.allow with input as {"action": "read"}
|
|
80
|
+
"anonymous request" in (rbac.deny_reasons with input as {"action": "read"})
|
|
81
|
+
}
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
**Pitfalls:**
|
|
85
|
+
- The \`permissions\` table grows unbounded. Move to data files
|
|
86
|
+
(\`data.permissions\`) once you have more than ~20 roles.
|
|
87
|
+
- Roles overlap with groups in real auth systems; map at the boundary
|
|
88
|
+
rather than carrying both.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 2. Attribute-based access control (ABAC)
|
|
93
|
+
|
|
94
|
+
**When to use:** when "who" alone isn't enough — decisions also depend
|
|
95
|
+
on resource attributes (ownership, tenant, sensitivity) and context
|
|
96
|
+
(time of day, source IP).
|
|
97
|
+
|
|
98
|
+
\`\`\`rego
|
|
99
|
+
package abac
|
|
100
|
+
|
|
101
|
+
import rego.v1
|
|
102
|
+
|
|
103
|
+
default allow := false
|
|
104
|
+
|
|
105
|
+
# A user can read any resource they own.
|
|
106
|
+
allow if {
|
|
107
|
+
input.action == "read"
|
|
108
|
+
input.resource.owner_id == input.user.id
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# A user can read shared resources at their organization.
|
|
112
|
+
allow if {
|
|
113
|
+
input.action == "read"
|
|
114
|
+
input.resource.shared
|
|
115
|
+
input.resource.org_id == input.user.org_id
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Admins can do anything within their organization.
|
|
119
|
+
allow if {
|
|
120
|
+
"admin" in input.user.roles
|
|
121
|
+
input.resource.org_id == input.user.org_id
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Don't show "secret" resources to anyone outside the owner's
|
|
125
|
+
# organization, even admins.
|
|
126
|
+
allow := false if {
|
|
127
|
+
input.resource.classification == "secret"
|
|
128
|
+
input.resource.org_id != input.user.org_id
|
|
129
|
+
}
|
|
130
|
+
\`\`\`
|
|
131
|
+
|
|
132
|
+
**Test:**
|
|
133
|
+
|
|
134
|
+
\`\`\`rego
|
|
135
|
+
package abac_test
|
|
136
|
+
|
|
137
|
+
import rego.v1
|
|
138
|
+
import data.abac
|
|
139
|
+
|
|
140
|
+
test_owner_reads_own if {
|
|
141
|
+
abac.allow with input as {
|
|
142
|
+
"action": "read",
|
|
143
|
+
"user": {"id": "u1", "org_id": "o1"},
|
|
144
|
+
"resource": {"owner_id": "u1", "org_id": "o1"},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
test_admin_blocked_from_secret_in_other_org if {
|
|
149
|
+
not abac.allow with input as {
|
|
150
|
+
"action": "read",
|
|
151
|
+
"user": {"id": "u1", "org_id": "o1", "roles": ["admin"]},
|
|
152
|
+
"resource": {
|
|
153
|
+
"owner_id": "u2",
|
|
154
|
+
"org_id": "o2",
|
|
155
|
+
"classification": "secret",
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
\`\`\`
|
|
160
|
+
|
|
161
|
+
**Pitfalls:**
|
|
162
|
+
- Multiple \`allow\` rules combine with logical OR. Use explicit
|
|
163
|
+
\`allow := false if ...\` to *override* an allow.
|
|
164
|
+
- Don't compute attributes inside the policy; compute them at the
|
|
165
|
+
boundary and pass via \`input\`.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 3. Kubernetes admission control
|
|
170
|
+
|
|
171
|
+
**When to use:** validate or mutate Kubernetes resources at admission
|
|
172
|
+
time. Run as a Gatekeeper, OPA-as-a-webhook, or Kyverno-equivalent
|
|
173
|
+
policy layer.
|
|
174
|
+
|
|
175
|
+
\`\`\`rego
|
|
176
|
+
package k8s.admission
|
|
177
|
+
|
|
178
|
+
import rego.v1
|
|
179
|
+
|
|
180
|
+
# Reject pods without resource limits.
|
|
181
|
+
deny contains msg if {
|
|
182
|
+
input.request.kind.kind == "Pod"
|
|
183
|
+
container := input.request.object.spec.containers[_]
|
|
184
|
+
not container.resources.limits.memory
|
|
185
|
+
msg := sprintf(
|
|
186
|
+
"Pod %q container %q is missing resources.limits.memory",
|
|
187
|
+
[input.request.object.metadata.name, container.name],
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Reject privileged containers in production.
|
|
192
|
+
deny contains msg if {
|
|
193
|
+
input.request.kind.kind == "Pod"
|
|
194
|
+
input.request.namespace != "kube-system"
|
|
195
|
+
container := input.request.object.spec.containers[_]
|
|
196
|
+
container.securityContext.privileged == true
|
|
197
|
+
msg := sprintf(
|
|
198
|
+
"privileged containers are not allowed: %q in %q",
|
|
199
|
+
[container.name, input.request.object.metadata.name],
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
**Pitfalls:**
|
|
205
|
+
- Use \`input.request.object\` for the resource being admitted; the
|
|
206
|
+
envelope shape comes from Kubernetes, not your control.
|
|
207
|
+
- For mutations, return a JSON Patch via the \`patch\` field. Test
|
|
208
|
+
patches with the actual admission webhook in dry-run mode before
|
|
209
|
+
enforcing.
|
|
210
|
+
- Iteration order is undefined; never rely on \`containers[0]\` to mean
|
|
211
|
+
anything specific.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 4. Infrastructure-as-Code gates (Terraform)
|
|
216
|
+
|
|
217
|
+
**When to use:** validate Terraform plans before apply. Catch overly
|
|
218
|
+
permissive IAM, public S3 buckets, missing encryption.
|
|
219
|
+
|
|
220
|
+
\`\`\`rego
|
|
221
|
+
package terraform
|
|
222
|
+
|
|
223
|
+
import rego.v1
|
|
224
|
+
|
|
225
|
+
# Reject S3 buckets without server-side encryption.
|
|
226
|
+
deny contains msg if {
|
|
227
|
+
resource := input.resource_changes[_]
|
|
228
|
+
resource.type == "aws_s3_bucket"
|
|
229
|
+
after := resource.change.after
|
|
230
|
+
not after.server_side_encryption_configuration
|
|
231
|
+
msg := sprintf(
|
|
232
|
+
"S3 bucket %q has no server-side encryption configured",
|
|
233
|
+
[resource.address],
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# Reject IAM policies with action "*" on resource "*".
|
|
238
|
+
deny contains msg if {
|
|
239
|
+
resource := input.resource_changes[_]
|
|
240
|
+
resource.type == "aws_iam_policy"
|
|
241
|
+
policy := json.unmarshal(resource.change.after.policy)
|
|
242
|
+
statement := policy.Statement[_]
|
|
243
|
+
statement.Effect == "Allow"
|
|
244
|
+
"*" in statement.Action
|
|
245
|
+
statement.Resource == "*"
|
|
246
|
+
msg := sprintf("IAM policy %q grants Allow * on *", [resource.address])
|
|
247
|
+
}
|
|
248
|
+
\`\`\`
|
|
249
|
+
|
|
250
|
+
**Pitfalls:**
|
|
251
|
+
- Terraform plan JSON is verbose and version-specific. Pin the
|
|
252
|
+
\`terraform plan -json\` schema you target.
|
|
253
|
+
- \`resource_changes[_].change.after\` may be \`null\` for destroys —
|
|
254
|
+
guard against it.
|
|
255
|
+
- For wide-radius changes, use \`opa exec --decision\` against a plan
|
|
256
|
+
file in CI, not the live API.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 5. API authorization (HTTP request gating)
|
|
261
|
+
|
|
262
|
+
**When to use:** at the API gateway / reverse proxy layer, validate
|
|
263
|
+
each request against the caller's identity and the requested
|
|
264
|
+
endpoint.
|
|
265
|
+
|
|
266
|
+
\`\`\`rego
|
|
267
|
+
package api.authz
|
|
268
|
+
|
|
269
|
+
import rego.v1
|
|
270
|
+
|
|
271
|
+
default allow := false
|
|
272
|
+
|
|
273
|
+
# Public endpoints — no auth required.
|
|
274
|
+
public_endpoints := {
|
|
275
|
+
{"method": "GET", "path": ["health"]},
|
|
276
|
+
{"method": "GET", "path": ["version"]},
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
allow if some _ in public_endpoints; matches_endpoint(_)
|
|
280
|
+
|
|
281
|
+
# Authenticated reads on resources the user has access to.
|
|
282
|
+
allow if {
|
|
283
|
+
input.method == "GET"
|
|
284
|
+
input.user
|
|
285
|
+
user_can_read(input.user, input.path)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# Authenticated writes only with specific scopes.
|
|
289
|
+
allow if {
|
|
290
|
+
input.method in {"POST", "PUT", "PATCH", "DELETE"}
|
|
291
|
+
input.user
|
|
292
|
+
"write" in input.user.scopes
|
|
293
|
+
user_can_write(input.user, input.path)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
matches_endpoint(spec) if {
|
|
297
|
+
spec.method == input.method
|
|
298
|
+
spec.path == input.path
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
user_can_read(user, path) if {
|
|
302
|
+
path[0] == "users"
|
|
303
|
+
user.id == path[1]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
user_can_write(user, path) if {
|
|
307
|
+
path[0] == "users"
|
|
308
|
+
user.id == path[1]
|
|
309
|
+
}
|
|
310
|
+
\`\`\`
|
|
311
|
+
|
|
312
|
+
**Pitfalls:**
|
|
313
|
+
- \`input.path\` is typically an array (\`["users", "alice"]\`), not a
|
|
314
|
+
string. Build it consistently at the gateway.
|
|
315
|
+
- Path-prefix matching is easy; full pattern matching is not. For
|
|
316
|
+
parameterized routes, decode at the gateway and pass structured
|
|
317
|
+
fields.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 6. Rate limiting (with sliding window data)
|
|
322
|
+
|
|
323
|
+
**When to use:** allow N requests per principal per window. Lightweight
|
|
324
|
+
limit enforcement; for high throughput, push to a dedicated rate
|
|
325
|
+
limiter.
|
|
326
|
+
|
|
327
|
+
\`\`\`rego
|
|
328
|
+
package rate
|
|
329
|
+
|
|
330
|
+
import rego.v1
|
|
331
|
+
|
|
332
|
+
# Configuration: 100 requests per principal per 60-second window.
|
|
333
|
+
limit := 100
|
|
334
|
+
window_seconds := 60
|
|
335
|
+
|
|
336
|
+
# input.now is a unix timestamp in nanoseconds.
|
|
337
|
+
# data.requests[principal] is an array of nanosecond timestamps.
|
|
338
|
+
|
|
339
|
+
current_window := requests if {
|
|
340
|
+
requests := [t | some t in data.requests[input.principal]; t > input.now - window_seconds * 1000000000]
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
count := count(current_window)
|
|
344
|
+
|
|
345
|
+
allow if count < limit
|
|
346
|
+
|
|
347
|
+
deny_reason := sprintf(
|
|
348
|
+
"rate limit exceeded: %d requests in last %d seconds (limit %d)",
|
|
349
|
+
[count, window_seconds, limit],
|
|
350
|
+
) if not allow
|
|
351
|
+
\`\`\`
|
|
352
|
+
|
|
353
|
+
**Pitfalls:**
|
|
354
|
+
- \`data.requests\` grows unbounded unless the writer prunes outside the
|
|
355
|
+
window. Schedule prune on every write.
|
|
356
|
+
- This pattern is *advisory* — under load, two concurrent decisions
|
|
357
|
+
can both see \`count == limit - 1\` and both allow. For strict
|
|
358
|
+
limits, use a Lua/Redis token bucket at the gateway and have OPA
|
|
359
|
+
validate the token, not count requests.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Where these patterns came from
|
|
364
|
+
|
|
365
|
+
Each is distilled from production policy code. The full Rego files,
|
|
366
|
+
tests, and policy data fixtures live in this server's GitHub
|
|
367
|
+
repository under \`tests/fixtures/policies/\`.
|
|
368
|
+
|
|
369
|
+
For more patterns, see:
|
|
370
|
+
|
|
371
|
+
- OPA Playground: https://play.openpolicyagent.org/
|
|
372
|
+
- Awesome OPA: https://github.com/anderseknert/awesome-opa
|
|
373
|
+
- Styra DAS pattern library: https://docs.styra.com/das/policies
|
|
374
|
+
`;
|
|
375
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/resources/patterns.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8WvB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated Rego style guide. Adapted from the Styra reference plus the
|
|
3
|
+
* OPA team's own conventions, condensed for LLM consumption.
|
|
4
|
+
*/
|
|
5
|
+
export declare const STYLE_GUIDE = "# Rego style guide\n\nA condensed reference for writing idiomatic, maintainable Rego. Adapted\nfrom the Styra style guide and the OPA project's conventions.\n\n## Always import `rego.v1`\n\nEvery `.rego` file should start with the v1 import. It enables the\nmodern syntax (`if`, `contains`, `every`) and disables the older\nimplicit-membership operator that's a frequent source of bugs.\n\n```rego\npackage authz\n\nimport rego.v1\n```\n\nOPA 1.0 (released January 2025) treats this as the default, but\nincluding it explicitly makes intent clear and keeps the file portable\nto older OPA installations.\n\n## Package layout mirrors directory layout\n\nMatch `package foo.bar` with the file path `foo/bar/main.rego` (or\n`foo/bar/<anything>.rego`). Regal flags mismatches as\n`directory-package-mismatch`.\n\nFor a multi-file package, group by responsibility:\n\n```\nauthz/\n main.rego # principal decision (allow / deny)\n rbac.rego # role-based access checks\n abac.rego # attribute-based access checks\n helpers.rego # shared helpers, no decisions\n main_test.rego # tests for principal decisions\n rbac_test.rego # tests for rbac\n```\n\nTests live in `*_test.rego` siblings of the source they test, in the\nsame package or a `<package>_test` package.\n\n## Naming\n\n- Rule names: `snake_case`.\n- Boolean rules: read like predicates \u2014 `allow`, `is_admin`,\n `should_log`. Avoid `is_not_blocked` (double negative).\n- Set/object rules: read like nouns \u2014 `grants`, `roles`, `reasons`.\n- Helper rules: `_` prefix is *not* a convention; just give them names\n that explain what they return.\n\nNames that conflict with reserved words (`type`, `time`, `input`) won't\nparse. Names matching builtins (`http`, `json`) shadow them.\n\n## Default deny\n\nEvery principal decision should have a default fallback so the policy\nreturns a value even when no rule matches.\n\n```rego\ndefault allow := false\n\nallow if {\n input.user.role == \"admin\"\n}\n```\n\nThe same applies to set-valued reasons:\n\n```rego\ndeny contains reason if {\n not allow\n reason := \"not authorized\"\n}\n```\n\n## Comprehensions vs `every`\n\nWhen you need a value derived from a collection, use a comprehension:\n\n```rego\nadmin_users := {u | some u in input.users; u.role == \"admin\"}\n```\n\nWhen you need to assert that a property holds for every element, use\n`every` (introduced in OPA 0.41):\n\n```rego\nallow if {\n every claim in input.token.claims {\n claim.verified == true\n }\n}\n```\n\n`every` is clearer than the older `not <comprehension>` idiom and\nshort-circuits on the first failure.\n\n## Annotations\n\nPublic rules deserve a metadata block so consumers know what they do.\n\n```rego\n# METADATA\n# title: Authorization decision for HTTP requests\n# description: |\n# Returns true when the requesting principal has a role with a\n# permission entry for the requested action on the requested\n# resource. Anonymous requests always deny.\n# entrypoint: true\nallow if {\n some role in input.principal.roles\n permission_grants[role][input.action]\n}\n```\n\nAnnotations show up in `opa inspect`, the registry, and editor\nhovers. They are also extracted by `rego_describe_policy`.\n\n## Schema annotations\n\nFor policies that are sensitive to input shape, attach a JSON Schema:\n\n```rego\n# METADATA\n# schemas:\n# - input: schema.input\nallow if {\n input.user.id != \"\"\n}\n```\n\nCombined with `opa check --schema`, this turns input-shape mismatches\ninto compile errors instead of runtime undefineds.\n\n## Anti-patterns\n\n- **`http.send` in the decision path.** Each call adds round-trip\n latency. If you must, scope it to a small, cacheable read.\n- **Deep `with` chains.** `x with input as ... with data as ...` more\n than two layers deep is a smell \u2014 the test is doing too much. Split.\n- **`print` and `trace`.** Useful in development; remove before\n shipping. Regal flags both as `print-or-trace-call`.\n- **Mixing `if` and the legacy implicit form in the same file.** Pick\n one. With `rego.v1` imported you cannot use the implicit form.\n\n## Tests\n\n`opa test` runs every rule whose name starts with `test_`. A test\neither evaluates to true (pass) or fails to evaluate (fail). The\ncommon shape:\n\n```rego\npackage authz_test\n\nimport rego.v1\nimport data.authz\n\ntest_admin_can_delete if {\n authz.allow with input as {\n \"user\": {\"role\": \"admin\"},\n \"action\": \"delete\",\n }\n}\n\ntest_viewer_cannot_delete if {\n not authz.allow with input as {\n \"user\": {\"role\": \"viewer\"},\n \"action\": \"delete\",\n }\n}\n```\n\nRun with `opa test -v ./...` for verbose output. Add `--coverage` to\nverify which lines were exercised.\n\n## References\n\n- Official Rego style guide: https://docs.styra.com/regal/rego-style-guide\n- OPA documentation: https://www.openpolicyagent.org/docs/latest/\n- Regal linter rules: https://docs.styra.com/regal/rules\n";
|
|
6
|
+
//# sourceMappingURL=style-guide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style-guide.d.ts","sourceRoot":"","sources":["../../src/resources/style-guide.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW,+6JAqLvB,CAAC"}
|