@ory/argus 1.4.0 → 1.4.2
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/dist/build-info.json +4 -4
- package/dist/client.js +8 -1
- package/dist/event-reporter.d.ts +11 -1
- package/dist/event-reporter.js +21 -3
- package/dist/permissions.js +126 -88
- package/dist/subject.d.ts +10 -1
- package/dist/subject.js +16 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repo": "ory-agent-plugins",
|
|
3
|
-
"commit": "
|
|
4
|
-
"commitShort": "
|
|
3
|
+
"commit": "42429b6ed5f9b4201bb43bedb7977e19b53783ac",
|
|
4
|
+
"commitShort": "42429b6",
|
|
5
5
|
"branch": "main",
|
|
6
|
-
"commitDate": "2026-
|
|
6
|
+
"commitDate": "2026-10-01T17:08:44-07:00",
|
|
7
7
|
"dirty": false,
|
|
8
|
-
"builtAt": "2026-
|
|
8
|
+
"builtAt": "2026-10-02T00:12:26.601Z"
|
|
9
9
|
}
|
package/dist/client.js
CHANGED
|
@@ -44,6 +44,7 @@ const agent_auth_js_1 = require("./agent-auth.js");
|
|
|
44
44
|
const context_js_1 = require("./context.js");
|
|
45
45
|
const config_js_1 = require("./config.js");
|
|
46
46
|
const auth_store_js_1 = require("./auth-store.js");
|
|
47
|
+
const subject_js_1 = require("./subject.js");
|
|
47
48
|
const runtime_credential_js_1 = require("./runtime-credential.js");
|
|
48
49
|
const BROKER_PERMISSION_BATCH_SIZE = 10;
|
|
49
50
|
function permissionCheckFromRequest(request) {
|
|
@@ -261,7 +262,13 @@ class OryAgentClient {
|
|
|
261
262
|
|| this.config.userSubjectNamespace
|
|
262
263
|
|| "User";
|
|
263
264
|
if (subject.namespace === ownerNamespace) {
|
|
264
|
-
|
|
265
|
+
const userSubject = this.userPrincipal.subject;
|
|
266
|
+
if (subject.object === userSubject) {
|
|
267
|
+
return { kind: "owner", namespace: subject.namespace };
|
|
268
|
+
}
|
|
269
|
+
// A preprovisioned runtime with no local user: the broker derives the
|
|
270
|
+
// owner from the authenticated runtime, and no owner ID is sent.
|
|
271
|
+
if (userSubject === undefined && subject.object === subject_js_1.RUNTIME_OWNER_SUBJECT && this._runtimeContext) {
|
|
265
272
|
return { kind: "owner", namespace: subject.namespace };
|
|
266
273
|
}
|
|
267
274
|
throw httpError(403, "Permission owner does not match the authenticated user");
|
package/dist/event-reporter.d.ts
CHANGED
|
@@ -43,7 +43,12 @@ export declare class AgentSecurityEventReporter {
|
|
|
43
43
|
private scheduled;
|
|
44
44
|
private activeRequest;
|
|
45
45
|
private flushDeadline;
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Set when a flush deadline passes. The current delivery stops starting new
|
|
48
|
+
* requests so a short-lived hook process can exit on time; undelivered events
|
|
49
|
+
* stay queued (and in the durable outbox) for a later delivery.
|
|
50
|
+
*/
|
|
51
|
+
private flushExpired;
|
|
47
52
|
private retryTimer;
|
|
48
53
|
private retryAttempt;
|
|
49
54
|
private readonly retryBaseMs;
|
|
@@ -56,6 +61,11 @@ export declare class AgentSecurityEventReporter {
|
|
|
56
61
|
enqueue(entry: ActivityEntry, principalKey?: string): void;
|
|
57
62
|
/** Attempt all currently deliverable batches, bounded by the caller's deadline. */
|
|
58
63
|
flush(timeoutMs?: number): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* The request timeout shares the flush deadline and can abort a request before
|
|
66
|
+
* the flush timer fires, so check the deadline itself as well as the flag.
|
|
67
|
+
*/
|
|
68
|
+
private flushDeadlinePassed;
|
|
59
69
|
private schedule;
|
|
60
70
|
private startDelivery;
|
|
61
71
|
private scheduleRetry;
|
package/dist/event-reporter.js
CHANGED
|
@@ -99,7 +99,12 @@ class AgentSecurityEventReporter {
|
|
|
99
99
|
scheduled = false;
|
|
100
100
|
activeRequest;
|
|
101
101
|
flushDeadline;
|
|
102
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Set when a flush deadline passes. The current delivery stops starting new
|
|
104
|
+
* requests so a short-lived hook process can exit on time; undelivered events
|
|
105
|
+
* stay queued (and in the durable outbox) for a later delivery.
|
|
106
|
+
*/
|
|
107
|
+
flushExpired = false;
|
|
103
108
|
retryTimer;
|
|
104
109
|
retryAttempt = 0;
|
|
105
110
|
retryBaseMs;
|
|
@@ -173,7 +178,7 @@ class AgentSecurityEventReporter {
|
|
|
173
178
|
work,
|
|
174
179
|
new Promise((resolve) => {
|
|
175
180
|
timer = setTimeout(() => {
|
|
176
|
-
this.
|
|
181
|
+
this.flushExpired = true;
|
|
177
182
|
this.activeRequest?.abort();
|
|
178
183
|
resolve();
|
|
179
184
|
}, timeoutMs);
|
|
@@ -185,6 +190,13 @@ class AgentSecurityEventReporter {
|
|
|
185
190
|
if (this.flushDeadline === deadline)
|
|
186
191
|
this.flushDeadline = undefined;
|
|
187
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* The request timeout shares the flush deadline and can abort a request before
|
|
195
|
+
* the flush timer fires, so check the deadline itself as well as the flag.
|
|
196
|
+
*/
|
|
197
|
+
flushDeadlinePassed() {
|
|
198
|
+
return this.flushExpired || (this.flushDeadline !== undefined && Date.now() >= this.flushDeadline);
|
|
199
|
+
}
|
|
188
200
|
schedule() {
|
|
189
201
|
if (this.scheduled || this.delivery)
|
|
190
202
|
return;
|
|
@@ -207,7 +219,7 @@ class AgentSecurityEventReporter {
|
|
|
207
219
|
void work.finally(() => {
|
|
208
220
|
if (this.delivery === work)
|
|
209
221
|
this.delivery = undefined;
|
|
210
|
-
this.
|
|
222
|
+
this.flushExpired = false;
|
|
211
223
|
if (this.queue.length === 0)
|
|
212
224
|
this.retryAttempt = 0;
|
|
213
225
|
else if (shouldRetry)
|
|
@@ -236,6 +248,8 @@ class AgentSecurityEventReporter {
|
|
|
236
248
|
this.prune();
|
|
237
249
|
const retainedScopes = new Set();
|
|
238
250
|
while (this.queue.length > 0) {
|
|
251
|
+
if (this.flushDeadlinePassed())
|
|
252
|
+
return true;
|
|
239
253
|
const first = this.queue.find((pending) => !retainedScopes.has(scopeKey(pendingScope(pending))));
|
|
240
254
|
if (!first)
|
|
241
255
|
return shouldRetry;
|
|
@@ -280,6 +294,10 @@ class AgentSecurityEventReporter {
|
|
|
280
294
|
const remove = new Set();
|
|
281
295
|
let retry = false;
|
|
282
296
|
for (const event of batch) {
|
|
297
|
+
if (this.flushDeadlinePassed()) {
|
|
298
|
+
retry = true;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
283
301
|
const isolated = await this.deliver([event], scope);
|
|
284
302
|
for (const eventId of isolated.remove)
|
|
285
303
|
remove.add(eventId);
|
package/dist/permissions.js
CHANGED
|
@@ -394,49 +394,29 @@ function applyPrincipalBlock(client, ctx) {
|
|
|
394
394
|
* must not hinge on the parser.
|
|
395
395
|
*/
|
|
396
396
|
async function decomposeAndCheck(client, args) {
|
|
397
|
-
|
|
397
|
+
// The word checks do not depend on the top-level result until aggregation, so
|
|
398
|
+
// send them alongside the top-level check: one round trip instead of two.
|
|
399
|
+
const topPromise = checkAndDecide(client, args.check, {
|
|
398
400
|
activityAttributes: args.activityAttributes,
|
|
399
401
|
modeOverride: args.modeOverride,
|
|
400
402
|
});
|
|
403
|
+
let parsed = await parseShellWords(args);
|
|
404
|
+
const subject = { subjectId: args.check.subjectId, subjectSet: args.check.subjectSet };
|
|
405
|
+
const namespace = (0, tool_catalog_js_1.resolveShellCommandNamespace)();
|
|
406
|
+
// Mirrors the mode checkAndDecide resolves, so a command that will be denied
|
|
407
|
+
// as too-complex in enforce sends no word checks, exactly as before.
|
|
408
|
+
const expectedMode = args.modeOverride ?? (0, config_js_1.resolveConfig)().permissionMode;
|
|
409
|
+
const wordsPromise = parsed.parserUnavailable
|
|
410
|
+
|| parsed.words.length === 0
|
|
411
|
+
|| parsed.words.length > MAX_SHELL_WORDS
|
|
412
|
+
|| (parsed.tooComplex && expectedMode === "enforce")
|
|
413
|
+
? undefined
|
|
414
|
+
: fetchShellWordResults(client, args, namespace, parsed.words, subject);
|
|
415
|
+
const [top, wordFetch] = await Promise.all([topPromise, wordsPromise]);
|
|
401
416
|
// Top-level Bash denied (enforce) or unreachable → no point decomposing.
|
|
402
417
|
if (top.kind === "deny" || top.kind === "fail_open")
|
|
403
418
|
return top;
|
|
404
419
|
const mode = top.mode;
|
|
405
|
-
// The parser is written to be total, but this is a security boundary: if it
|
|
406
|
-
// ever throws, treat it as too-complex (fail-closed in enforce) rather than
|
|
407
|
-
// letting the exception bubble to the harness catch, which fails open.
|
|
408
|
-
let parsed;
|
|
409
|
-
try {
|
|
410
|
-
const commands = typeof args.shellCommand === "string"
|
|
411
|
-
? [args.shellCommand]
|
|
412
|
-
: args.shellCommand ?? [];
|
|
413
|
-
const words = [];
|
|
414
|
-
const seen = new Set();
|
|
415
|
-
let tooComplex = false;
|
|
416
|
-
let parserUnavailable = false;
|
|
417
|
-
for (const command of commands) {
|
|
418
|
-
const result = await (0, bash_parser_js_1.extractBashCommandWords)(command);
|
|
419
|
-
tooComplex ||= result.tooComplex;
|
|
420
|
-
parserUnavailable ||= result.parserUnavailable === true;
|
|
421
|
-
for (const word of result.words) {
|
|
422
|
-
if (!seen.has(word)) {
|
|
423
|
-
seen.add(word);
|
|
424
|
-
words.push(word);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
parsed = { words, tooComplex, parserUnavailable };
|
|
429
|
-
}
|
|
430
|
-
catch {
|
|
431
|
-
parsed = { words: [], tooComplex: true };
|
|
432
|
-
}
|
|
433
|
-
if (args.toolName.toLowerCase() === "powershell"
|
|
434
|
-
|| (args.harness === "cline" && args.toolName === "run_commands" && process.platform === "win32")) {
|
|
435
|
-
// The bundled grammar models POSIX shell syntax, not PowerShell. Retain its
|
|
436
|
-
// Never retain words parsed with the incompatible grammar: PowerShell
|
|
437
|
-
// literals could otherwise be persisted as if they were command names.
|
|
438
|
-
parsed = { words: [], tooComplex: true, parserUnavailable: false };
|
|
439
|
-
}
|
|
440
420
|
if (parsed.parserUnavailable) {
|
|
441
421
|
client.logger.warn("bash.parser_unavailable", {
|
|
442
422
|
harness: args.harness,
|
|
@@ -453,7 +433,6 @@ async function decomposeAndCheck(client, args) {
|
|
|
453
433
|
if (parsed.words.length > MAX_SHELL_WORDS) {
|
|
454
434
|
parsed = { words: [], tooComplex: true };
|
|
455
435
|
}
|
|
456
|
-
const subject = { subjectId: args.check.subjectId, subjectSet: args.check.subjectSet };
|
|
457
436
|
const baseAttrs = buildDecisionAttributes(mode, subject.subjectId, subject.subjectSet);
|
|
458
437
|
// Decomposition attributes attached to activity so the shell breakdown is
|
|
459
438
|
// observable in real time — binary/builtin *names* only. The RAW command is
|
|
@@ -501,64 +480,34 @@ async function decomposeAndCheck(client, args) {
|
|
|
501
480
|
modeOverride: args.modeOverride,
|
|
502
481
|
});
|
|
503
482
|
}
|
|
504
|
-
// Check each distinct command word. Chunk into batches no larger than Keto's
|
|
505
|
-
// `max_batch_check_size` (see DEFAULT_SHELL_BATCH_SIZE) so a word-heavy command
|
|
506
|
-
// never trips the server's batch limit into a fail-open fallback.
|
|
507
|
-
const namespace = (0, tool_catalog_js_1.resolveShellCommandNamespace)();
|
|
508
483
|
const words = parsed.words;
|
|
509
484
|
const deniedWords = [];
|
|
510
485
|
const affectedWords = [];
|
|
511
486
|
let sawObserve = false;
|
|
512
|
-
|
|
487
|
+
let matchedBlocks = new Map();
|
|
513
488
|
if (words.length > 0) {
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}));
|
|
525
|
-
try {
|
|
526
|
-
const [batch, blocked] = await Promise.all([
|
|
527
|
-
client.batchCheckPermissions(checks, {
|
|
528
|
-
activityAttributes: { ...args.activityAttributes, ...decompAttrs, shellDecomposition: true },
|
|
529
|
-
}),
|
|
530
|
-
findBlockedPrincipals(client, {
|
|
531
|
-
namespace,
|
|
532
|
-
objects: chunk,
|
|
533
|
-
principals: args.principals,
|
|
534
|
-
}),
|
|
535
|
-
]);
|
|
536
|
-
results.push(...batch.results);
|
|
537
|
-
for (const [word, principal] of blocked) {
|
|
538
|
-
if (!matchedBlocks.has(word))
|
|
539
|
-
matchedBlocks.set(word, principal);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
catch (err) {
|
|
543
|
-
const oryErr = err;
|
|
544
|
-
if (mode === "enforce" && oryErr && CHECK_UNTRUSTED_CODES.has(oryErr.code)) {
|
|
545
|
-
return {
|
|
546
|
-
kind: "deny",
|
|
547
|
-
result: { allowed: false, checkedAt: new Date().toISOString(), check: args.check },
|
|
548
|
-
mode: "enforce",
|
|
549
|
-
activityAttributes: { ...baseAttrs, checkRejected: oryErr.code },
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
// Infrastructure error — fail open to the top-level decision.
|
|
553
|
-
client.logger.warn("bash.batch_check_failed", {
|
|
554
|
-
harness: args.harness,
|
|
555
|
-
toolName: args.toolName,
|
|
556
|
-
code: oryErr?.code,
|
|
557
|
-
note: "shell sub-tool batch check failed; falling back to top-level decision",
|
|
558
|
-
});
|
|
559
|
-
return top;
|
|
489
|
+
const fetched = wordFetch ?? await fetchShellWordResults(client, args, namespace, words, subject);
|
|
490
|
+
if (!fetched.ok) {
|
|
491
|
+
const oryErr = fetched.error;
|
|
492
|
+
if (mode === "enforce" && oryErr && CHECK_UNTRUSTED_CODES.has(oryErr.code)) {
|
|
493
|
+
return {
|
|
494
|
+
kind: "deny",
|
|
495
|
+
result: { allowed: false, checkedAt: new Date().toISOString(), check: args.check },
|
|
496
|
+
mode: "enforce",
|
|
497
|
+
activityAttributes: { ...baseAttrs, checkRejected: oryErr.code },
|
|
498
|
+
};
|
|
560
499
|
}
|
|
500
|
+
// Infrastructure error — fail open to the top-level decision.
|
|
501
|
+
client.logger.warn("bash.batch_check_failed", {
|
|
502
|
+
harness: args.harness,
|
|
503
|
+
toolName: args.toolName,
|
|
504
|
+
code: oryErr?.code,
|
|
505
|
+
note: "shell sub-tool batch check failed; falling back to top-level decision",
|
|
506
|
+
});
|
|
507
|
+
return top;
|
|
561
508
|
}
|
|
509
|
+
const results = fetched.results;
|
|
510
|
+
matchedBlocks = fetched.matchedBlocks;
|
|
562
511
|
for (let i = 0; i < words.length; i++) {
|
|
563
512
|
const word = words[i];
|
|
564
513
|
const allowed = results[i]?.allowed ?? false;
|
|
@@ -653,6 +602,95 @@ async function decomposeAndCheck(client, args) {
|
|
|
653
602
|
activityAttributes: { ...top.activityAttributes, ...decompAttrs },
|
|
654
603
|
};
|
|
655
604
|
}
|
|
605
|
+
async function parseShellWords(args) {
|
|
606
|
+
// The parser is written to be total, but this is a security boundary: if it
|
|
607
|
+
// ever throws, treat it as too-complex (fail-closed in enforce) rather than
|
|
608
|
+
// letting the exception bubble to the harness catch, which fails open.
|
|
609
|
+
let parsed;
|
|
610
|
+
try {
|
|
611
|
+
const commands = typeof args.shellCommand === "string"
|
|
612
|
+
? [args.shellCommand]
|
|
613
|
+
: args.shellCommand ?? [];
|
|
614
|
+
const words = [];
|
|
615
|
+
const seen = new Set();
|
|
616
|
+
let tooComplex = false;
|
|
617
|
+
let parserUnavailable = false;
|
|
618
|
+
for (const command of commands) {
|
|
619
|
+
const result = await (0, bash_parser_js_1.extractBashCommandWords)(command);
|
|
620
|
+
tooComplex ||= result.tooComplex;
|
|
621
|
+
parserUnavailable ||= result.parserUnavailable === true;
|
|
622
|
+
for (const word of result.words) {
|
|
623
|
+
if (!seen.has(word)) {
|
|
624
|
+
seen.add(word);
|
|
625
|
+
words.push(word);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
parsed = { words, tooComplex, parserUnavailable };
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
parsed = { words: [], tooComplex: true };
|
|
633
|
+
}
|
|
634
|
+
if (args.toolName.toLowerCase() === "powershell"
|
|
635
|
+
|| (args.harness === "cline" && args.toolName === "run_commands" && process.platform === "win32")) {
|
|
636
|
+
// The bundled grammar models POSIX shell syntax, not PowerShell. Never
|
|
637
|
+
// retain words parsed with the incompatible grammar: PowerShell literals
|
|
638
|
+
// could otherwise be persisted as if they were command names.
|
|
639
|
+
parsed = { words: [], tooComplex: true, parserUnavailable: false };
|
|
640
|
+
}
|
|
641
|
+
return parsed;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Check each distinct command word. Chunk into batches no larger than Keto's
|
|
645
|
+
* `max_batch_check_size` (see DEFAULT_SHELL_BATCH_SIZE) so a word-heavy command
|
|
646
|
+
* never trips the server's batch limit into a fail-open fallback. Chunks are
|
|
647
|
+
* sent concurrently and aggregated in order, so the first failing chunk decides
|
|
648
|
+
* the outcome exactly as a sequential pass would. Never rejects.
|
|
649
|
+
*/
|
|
650
|
+
async function fetchShellWordResults(client, args, namespace, words, subject) {
|
|
651
|
+
const decompAttrs = { shellWords: words, shellCommandCount: words.length };
|
|
652
|
+
const batchSize = resolveShellBatchSize();
|
|
653
|
+
const chunks = [];
|
|
654
|
+
for (let start = 0; start < words.length; start += batchSize) {
|
|
655
|
+
chunks.push(words.slice(start, start + batchSize));
|
|
656
|
+
}
|
|
657
|
+
const outcomes = await Promise.all(chunks.map(async (chunk) => {
|
|
658
|
+
const checks = chunk.map((word) => ({
|
|
659
|
+
namespace,
|
|
660
|
+
object: word,
|
|
661
|
+
relation: opl_js_1.PERMIT_USE,
|
|
662
|
+
...subject,
|
|
663
|
+
}));
|
|
664
|
+
try {
|
|
665
|
+
const [batch, blocked] = await Promise.all([
|
|
666
|
+
client.batchCheckPermissions(checks, {
|
|
667
|
+
activityAttributes: { ...args.activityAttributes, ...decompAttrs, shellDecomposition: true },
|
|
668
|
+
}),
|
|
669
|
+
findBlockedPrincipals(client, {
|
|
670
|
+
namespace,
|
|
671
|
+
objects: chunk,
|
|
672
|
+
principals: args.principals,
|
|
673
|
+
}),
|
|
674
|
+
]);
|
|
675
|
+
return { ok: true, results: batch.results, blocked };
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
return { ok: false, error: err };
|
|
679
|
+
}
|
|
680
|
+
}));
|
|
681
|
+
const results = [];
|
|
682
|
+
const matchedBlocks = new Map();
|
|
683
|
+
for (const outcome of outcomes) {
|
|
684
|
+
if (!outcome.ok)
|
|
685
|
+
return outcome;
|
|
686
|
+
results.push(...outcome.results);
|
|
687
|
+
for (const [word, principal] of outcome.blocked) {
|
|
688
|
+
if (!matchedBlocks.has(word))
|
|
689
|
+
matchedBlocks.set(word, principal);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return { ok: true, results, matchedBlocks };
|
|
693
|
+
}
|
|
656
694
|
/**
|
|
657
695
|
* Run a permission check and resolve the configured mode against the
|
|
658
696
|
* result. Never throws — infrastructure errors surface as a typed
|
package/dist/subject.d.ts
CHANGED
|
@@ -23,6 +23,14 @@ import type { OryAgentClient } from "./client.js";
|
|
|
23
23
|
* provisioning all use `User`, so it is the out-of-the-box default.
|
|
24
24
|
*/
|
|
25
25
|
export declare const DEFAULT_USER_SUBJECT_NAMESPACE = "User";
|
|
26
|
+
/**
|
|
27
|
+
* Owner object for a preprovisioned runtime (a Talos, SPIFFE or mTLS agent
|
|
28
|
+
* deployment) that has no local user principal. The client never knows that
|
|
29
|
+
* owner's identity; the broker derives it from the authenticated runtime, so
|
|
30
|
+
* this object is only ever sent as `{ kind: "owner" }`. It is not a UUID, so it
|
|
31
|
+
* cannot collide with a real identity ID.
|
|
32
|
+
*/
|
|
33
|
+
export declare const RUNTIME_OWNER_SUBJECT = "runtime-owner";
|
|
26
34
|
/** Namespace the agent principal is addressed under (delegation subjects). */
|
|
27
35
|
export declare const AGENT_NAMESPACE = "Agent";
|
|
28
36
|
/** Namespace a sub-agent principal is addressed under. */
|
|
@@ -53,7 +61,8 @@ export declare function runWithUserSubject<T>(subject: string | undefined, fn: (
|
|
|
53
61
|
* Resolve the user subject for permission checks. Prefers a per-call
|
|
54
62
|
* override installed via {@link runWithUserSubject}, then the user
|
|
55
63
|
* login's `userPrincipal.subject`, falls back to `ORY_USER_SUBJECT_ID`, then the
|
|
56
|
-
* legacy `ORY_AGENT_SUBJECT_ID`, then the
|
|
64
|
+
* legacy `ORY_AGENT_SUBJECT_ID`, then the deployment owner of a preprovisioned
|
|
65
|
+
* runtime ({@link RUNTIME_OWNER_SUBJECT}), then the caller-supplied `fallback`
|
|
57
66
|
* (typically `session:<id>`).
|
|
58
67
|
*
|
|
59
68
|
* Always returns a SubjectSet. A resolved principal is addressed under the
|
package/dist/subject.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* toggles between SubjectSet and a legacy direct SubjectID.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.AGENT_SUBJECT_ID_ENV_VAR = exports.SUBJECT_AXIS_SEPARATOR = exports.SESSION_NAMESPACE = exports.SUBAGENT_NAMESPACE = exports.AGENT_NAMESPACE = exports.DEFAULT_USER_SUBJECT_NAMESPACE = void 0;
|
|
21
|
+
exports.AGENT_SUBJECT_ID_ENV_VAR = exports.SUBJECT_AXIS_SEPARATOR = exports.SESSION_NAMESPACE = exports.SUBAGENT_NAMESPACE = exports.AGENT_NAMESPACE = exports.RUNTIME_OWNER_SUBJECT = exports.DEFAULT_USER_SUBJECT_NAMESPACE = void 0;
|
|
22
22
|
exports.runWithUserSubject = runWithUserSubject;
|
|
23
23
|
exports.resolveUserSubject = resolveUserSubject;
|
|
24
24
|
exports.subjectLabel = subjectLabel;
|
|
@@ -35,6 +35,14 @@ const node_async_hooks_1 = require("node:async_hooks");
|
|
|
35
35
|
* provisioning all use `User`, so it is the out-of-the-box default.
|
|
36
36
|
*/
|
|
37
37
|
exports.DEFAULT_USER_SUBJECT_NAMESPACE = "User";
|
|
38
|
+
/**
|
|
39
|
+
* Owner object for a preprovisioned runtime (a Talos, SPIFFE or mTLS agent
|
|
40
|
+
* deployment) that has no local user principal. The client never knows that
|
|
41
|
+
* owner's identity; the broker derives it from the authenticated runtime, so
|
|
42
|
+
* this object is only ever sent as `{ kind: "owner" }`. It is not a UUID, so it
|
|
43
|
+
* cannot collide with a real identity ID.
|
|
44
|
+
*/
|
|
45
|
+
exports.RUNTIME_OWNER_SUBJECT = "runtime-owner";
|
|
38
46
|
/** Namespace the agent principal is addressed under (delegation subjects). */
|
|
39
47
|
exports.AGENT_NAMESPACE = "Agent";
|
|
40
48
|
/** Namespace a sub-agent principal is addressed under. */
|
|
@@ -72,7 +80,8 @@ function subjectSet(namespace, object) {
|
|
|
72
80
|
* Resolve the user subject for permission checks. Prefers a per-call
|
|
73
81
|
* override installed via {@link runWithUserSubject}, then the user
|
|
74
82
|
* login's `userPrincipal.subject`, falls back to `ORY_USER_SUBJECT_ID`, then the
|
|
75
|
-
* legacy `ORY_AGENT_SUBJECT_ID`, then the
|
|
83
|
+
* legacy `ORY_AGENT_SUBJECT_ID`, then the deployment owner of a preprovisioned
|
|
84
|
+
* runtime ({@link RUNTIME_OWNER_SUBJECT}), then the caller-supplied `fallback`
|
|
76
85
|
* (typically `session:<id>`).
|
|
77
86
|
*
|
|
78
87
|
* Always returns a SubjectSet. A resolved principal is addressed under the
|
|
@@ -94,6 +103,11 @@ function resolveUserSubject(client, fallback) {
|
|
|
94
103
|
|| exports.DEFAULT_USER_SUBJECT_NAMESPACE;
|
|
95
104
|
if (principal)
|
|
96
105
|
return subjectSet(ns, principal);
|
|
106
|
+
// A preprovisioned runtime acts for its deployment's owner, whom only the
|
|
107
|
+
// broker knows. Without this, a headless deployment resolves no subject and
|
|
108
|
+
// every native tool is denied in enforce mode (#338).
|
|
109
|
+
if (client.runtimeContext)
|
|
110
|
+
return subjectSet(ns, exports.RUNTIME_OWNER_SUBJECT);
|
|
97
111
|
// No principal — use the caller's fallback. A `session:<id>` fallback is
|
|
98
112
|
// addressed under the Session namespace so it is a well-formed SubjectSet that
|
|
99
113
|
// could match a Session-scoped grant, rather than being wrapped under `User`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/argus",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://ory.com",
|