Skip to content

Commit 619e9e2

Browse files
chore: sync upstream main
2 parents e230e5a + f0355df commit 619e9e2

69 files changed

Lines changed: 9036 additions & 3322 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

migrations/0007_runtime_policy.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
create table runtime_policy (
2+
id integer primary key check (id = 1),
3+
value text not null,
4+
updated_at text not null
5+
);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
alter table runtime_tokens add column allowed_actions text not null default '[]';
2+
alter table runtime_tokens add column blocked_actions text not null default '[]';

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/action-policy.test.ts

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const action: ActionDefinition = {
1616

1717
describe("ActionPolicyService", () => {
1818
it("allows actions by default", () => {
19-
expect(new ActionPolicyService().evaluate(action)).toEqual({ allowed: true });
19+
expect(new ActionPolicyService().evaluate(action)).toEqual({ allowed: true, checks: [] });
2020
});
2121

2222
it("enforces exact and provider-wide allowlists", () => {
@@ -26,15 +26,18 @@ describe("ActionPolicyService", () => {
2626
});
2727
expect(new ActionPolicyService({ allowedActions: ["github.*"] }).evaluate(action)).toEqual({
2828
allowed: true,
29+
checks: [{ source: "deployment", outcome: "allow_match", rule: "github.*" }],
2930
});
3031
expect(new ActionPolicyService({ allowedActions: ["github.create_issue"] }).evaluate(action)).toEqual({
3132
allowed: true,
33+
checks: [{ source: "deployment", outcome: "allow_match", rule: "github.create_issue" }],
3234
});
3335
});
3436

3537
it("supports bare wildcard to match all actions", () => {
3638
expect(new ActionPolicyService({ allowedActions: ["*"] }).evaluate(action)).toEqual({
3739
allowed: true,
40+
checks: [{ source: "deployment", outcome: "allow_match", rule: "*" }],
3841
});
3942
expect(new ActionPolicyService({ blockedActions: ["*"] }).evaluate(action)).toMatchObject({
4043
allowed: false,
@@ -55,23 +58,37 @@ describe("ActionPolicyService", () => {
5558
});
5659

5760
it("allows proxies by default", () => {
58-
expect(new ActionPolicyService().evaluateProxy("github")).toEqual({ allowed: true });
61+
expect(new ActionPolicyService().evaluateProxy("github")).toEqual({ allowed: true, checks: [] });
5962
});
6063

6164
it("ignores action policy when evaluating proxies", () => {
6265
expect(new ActionPolicyService({ allowedActions: ["github.get_current_user"] }).evaluateProxy("github")).toEqual({
6366
allowed: true,
67+
checks: [],
6468
});
6569
expect(new ActionPolicyService({ blockedActions: ["github.delete_repository"] }).evaluateProxy("github")).toEqual({
6670
allowed: true,
71+
checks: [],
72+
});
73+
expect(new ActionPolicyService({ allowedActions: ["*"] }).evaluateProxy("github")).toEqual({
74+
allowed: true,
75+
checks: [],
76+
});
77+
expect(new ActionPolicyService({ blockedActions: ["*"] }).evaluateProxy("github")).toEqual({
78+
allowed: true,
79+
checks: [],
6780
});
68-
expect(new ActionPolicyService({ allowedActions: ["*"] }).evaluateProxy("github")).toEqual({ allowed: true });
69-
expect(new ActionPolicyService({ blockedActions: ["*"] }).evaluateProxy("github")).toEqual({ allowed: true });
7081
});
7182

7283
it("ignores proxy policy when evaluating actions", () => {
73-
expect(new ActionPolicyService({ blockedProxies: ["*"] }).evaluate(action)).toEqual({ allowed: true });
74-
expect(new ActionPolicyService({ allowedProxies: ["slack"] }).evaluate(action)).toEqual({ allowed: true });
84+
expect(new ActionPolicyService({ blockedProxies: ["*"] }).evaluate(action)).toEqual({
85+
allowed: true,
86+
checks: [],
87+
});
88+
expect(new ActionPolicyService({ allowedProxies: ["slack"] }).evaluate(action)).toEqual({
89+
allowed: true,
90+
checks: [],
91+
});
7592
});
7693

7794
it("disables every proxy with a blocked wildcard", () => {
@@ -88,9 +105,11 @@ describe("ActionPolicyService", () => {
88105
});
89106
expect(new ActionPolicyService({ allowedProxies: ["github"] }).evaluateProxy("github")).toEqual({
90107
allowed: true,
108+
checks: [{ source: "deployment", outcome: "allow_match", rule: "github" }],
91109
});
92110
expect(new ActionPolicyService({ allowedProxies: ["*"] }).evaluateProxy("github")).toEqual({
93111
allowed: true,
112+
checks: [{ source: "deployment", outcome: "allow_match", rule: "*" }],
94113
});
95114
});
96115

@@ -109,4 +128,91 @@ describe("ActionPolicyService", () => {
109128
it("parses comma-separated environment lists", () => {
110129
expect(parseActionPolicyList(" github.* , gmail.send_email ,, ")).toEqual(["github.*", "gmail.send_email"]);
111130
});
131+
132+
it("intersects deployment, runtime, and token action allowlists", () => {
133+
const snapshot = new ActionPolicyService({ allowedActions: ["github.*"] }).createSnapshot(
134+
{
135+
allowedActions: ["github.create_issue"],
136+
blockedActions: [],
137+
allowedProxies: [],
138+
blockedProxies: [],
139+
},
140+
{ allowedActions: ["github.*"], blockedActions: [] },
141+
);
142+
143+
expect(snapshot.evaluate(action)).toEqual({
144+
allowed: true,
145+
checks: [
146+
{ source: "deployment", outcome: "allow_match", rule: "github.*" },
147+
{ source: "runtime", outcome: "allow_match", rule: "github.create_issue" },
148+
{ source: "token", outcome: "allow_match", rule: "github.*" },
149+
],
150+
});
151+
});
152+
153+
it("reports the decisive layer when a lower allowlist rejects", () => {
154+
const snapshot = new ActionPolicyService({ allowedActions: ["github.*"] }).createSnapshot({
155+
allowedActions: ["gmail.*"],
156+
blockedActions: [],
157+
allowedProxies: [],
158+
blockedProxies: [],
159+
});
160+
161+
expect(snapshot.evaluate(action)).toMatchObject({
162+
allowed: false,
163+
code: "action_not_allowed",
164+
checks: [
165+
{ source: "deployment", outcome: "allow_match", rule: "github.*" },
166+
{ source: "runtime", outcome: "allow_miss" },
167+
],
168+
});
169+
});
170+
171+
it("applies Runtime and token block rules before every allowlist", () => {
172+
const service = new ActionPolicyService({ allowedActions: ["*"] });
173+
const runtimeBlocked = service.createSnapshot({
174+
allowedActions: ["github.*"],
175+
blockedActions: ["github.create_issue"],
176+
allowedProxies: [],
177+
blockedProxies: [],
178+
});
179+
expect(runtimeBlocked.evaluate(action)).toMatchObject({
180+
allowed: false,
181+
code: "action_blocked",
182+
checks: [{ source: "runtime", outcome: "block_match", rule: "github.create_issue" }],
183+
});
184+
185+
const tokenBlocked = service.createSnapshot(
186+
{
187+
allowedActions: ["github.*"],
188+
blockedActions: [],
189+
allowedProxies: [],
190+
blockedProxies: [],
191+
},
192+
{ allowedActions: ["github.*"], blockedActions: ["github.create_issue"] },
193+
);
194+
expect(tokenBlocked.evaluate(action)).toMatchObject({
195+
allowed: false,
196+
checks: [{ source: "token", outcome: "block_match", rule: "github.create_issue" }],
197+
});
198+
});
199+
200+
it("records only the first matching rule from each layer", () => {
201+
const decision = new ActionPolicyService({ allowedActions: ["github.*", "*"] })
202+
.createSnapshot({
203+
allowedActions: ["github.create_issue", "github.*"],
204+
blockedActions: [],
205+
allowedProxies: [],
206+
blockedProxies: [],
207+
})
208+
.evaluate(action);
209+
210+
expect(decision).toEqual({
211+
allowed: true,
212+
checks: [
213+
{ source: "deployment", outcome: "allow_match", rule: "github.*" },
214+
{ source: "runtime", outcome: "allow_match", rule: "github.create_issue" },
215+
],
216+
});
217+
});
112218
});

0 commit comments

Comments
 (0)