toolpass-client 0.11.3 → 0.13.0
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/README.md +3 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +24 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,10 @@ Without a wrapper:
|
|
|
48
48
|
```ts
|
|
49
49
|
const d = await tp.check("dana@example.com", "jira-main", "DELETE_ISSUES", "issue:PAY-123");
|
|
50
50
|
d.decision, d.reason; // "deny", "denied: ..."
|
|
51
|
+
d.identity; // the account the email resolved to (a GitHub login), "" if none
|
|
51
52
|
await tp.require(...); // rejects with PermissionDenied unless allow
|
|
53
|
+
// The fifth argument: the user's groups, or { groups, fresh, token } (token: the user's own
|
|
54
|
+
// ID token, for a connection that reads the groups from it: oidc_issuer).
|
|
52
55
|
```
|
|
53
56
|
|
|
54
57
|
Framework examples: [examples/agent-ts](https://github.com/roee-hersh/toolpass/tree/main/examples/agent-ts).
|
package/dist/index.d.ts
CHANGED
|
@@ -38,13 +38,15 @@ export type DecisionValue = typeof ALLOW | typeof DENY | typeof UNKNOWN;
|
|
|
38
38
|
* `decision` is `allow`, `deny` or `unknown`. `reason` is `"<code>: <text>"`
|
|
39
39
|
* as toolpass sent it, or `"client_error: ..."` when the request never
|
|
40
40
|
* produced a usable answer. `status` is the HTTP status, 0 when no response
|
|
41
|
-
* arrived.
|
|
41
|
+
* arrived. `identity` is the account the user's email resolved to in the
|
|
42
|
+
* system (a GitHub login), "" when none was resolved.
|
|
42
43
|
*/
|
|
43
44
|
export declare class Decision {
|
|
44
45
|
readonly decision: DecisionValue;
|
|
45
46
|
readonly reason: string;
|
|
46
47
|
readonly status: number;
|
|
47
|
-
|
|
48
|
+
readonly identity: string;
|
|
49
|
+
constructor(decision: DecisionValue, reason: string, status?: number, identity?: string);
|
|
48
50
|
/** True only for a positive `allow`. `unknown` is not allowed. */
|
|
49
51
|
get allowed(): boolean;
|
|
50
52
|
/** The machine-readable code in front of the colon, e.g. `user_not_found`. */
|
|
@@ -89,8 +91,10 @@ export declare class Toolpass {
|
|
|
89
91
|
/**
|
|
90
92
|
* Ask toolpass. Never rejects on transport: every failure becomes an
|
|
91
93
|
* `unknown` decision. The fifth argument is the user's groups, or an
|
|
92
|
-
* options object: `{ groups, fresh }`. Groups left out or null are
|
|
93
|
-
* omitted from the request.
|
|
94
|
+
* options object: `{ groups, fresh, token }`. Groups left out or null are
|
|
95
|
+
* omitted from the request. `token` is the user's own login token (an
|
|
96
|
+
* OIDC ID token), for a connection that reads the groups from it
|
|
97
|
+
* (`oidc_issuer`).
|
|
94
98
|
*
|
|
95
99
|
* `fresh: true` asks for an answer straight from the upstream system,
|
|
96
100
|
* skipping toolpass's caches. Use it for destructive actions. It narrows
|
|
@@ -107,6 +111,8 @@ export declare class Toolpass {
|
|
|
107
111
|
export interface CheckOptions {
|
|
108
112
|
/** The user's groups; left out or null, none are sent. */
|
|
109
113
|
groups?: readonly string[] | null;
|
|
114
|
+
/** The user's own login token (an OIDC ID token); left out or null, none is sent. */
|
|
115
|
+
token?: string | null;
|
|
110
116
|
/** Skip toolpass's caches and ask the upstream system now. */
|
|
111
117
|
fresh?: boolean;
|
|
112
118
|
}
|
package/dist/index.js
CHANGED
|
@@ -38,16 +38,19 @@ export const UNKNOWN = "unknown";
|
|
|
38
38
|
* `decision` is `allow`, `deny` or `unknown`. `reason` is `"<code>: <text>"`
|
|
39
39
|
* as toolpass sent it, or `"client_error: ..."` when the request never
|
|
40
40
|
* produced a usable answer. `status` is the HTTP status, 0 when no response
|
|
41
|
-
* arrived.
|
|
41
|
+
* arrived. `identity` is the account the user's email resolved to in the
|
|
42
|
+
* system (a GitHub login), "" when none was resolved.
|
|
42
43
|
*/
|
|
43
44
|
export class Decision {
|
|
44
45
|
decision;
|
|
45
46
|
reason;
|
|
46
47
|
status;
|
|
47
|
-
|
|
48
|
+
identity;
|
|
49
|
+
constructor(decision, reason, status = 200, identity = "") {
|
|
48
50
|
this.decision = decision;
|
|
49
51
|
this.reason = reason;
|
|
50
52
|
this.status = status;
|
|
53
|
+
this.identity = identity;
|
|
51
54
|
Object.freeze(this);
|
|
52
55
|
}
|
|
53
56
|
/** True only for a positive `allow`. `unknown` is not allowed. */
|
|
@@ -105,8 +108,10 @@ export class Toolpass {
|
|
|
105
108
|
/**
|
|
106
109
|
* Ask toolpass. Never rejects on transport: every failure becomes an
|
|
107
110
|
* `unknown` decision. The fifth argument is the user's groups, or an
|
|
108
|
-
* options object: `{ groups, fresh }`. Groups left out or null are
|
|
109
|
-
* omitted from the request.
|
|
111
|
+
* options object: `{ groups, fresh, token }`. Groups left out or null are
|
|
112
|
+
* omitted from the request. `token` is the user's own login token (an
|
|
113
|
+
* OIDC ID token), for a connection that reads the groups from it
|
|
114
|
+
* (`oidc_issuer`).
|
|
110
115
|
*
|
|
111
116
|
* `fresh: true` asks for an answer straight from the upstream system,
|
|
112
117
|
* skipping toolpass's caches. Use it for destructive actions. It narrows
|
|
@@ -114,7 +119,7 @@ export class Toolpass {
|
|
|
114
119
|
* two; it does not close it.
|
|
115
120
|
*/
|
|
116
121
|
async check(user, connection, action, resource, options) {
|
|
117
|
-
const { groups, fresh } = checkOptions(options);
|
|
122
|
+
const { groups, fresh, token } = checkOptions(options);
|
|
118
123
|
const body = { user, connection, action, resource };
|
|
119
124
|
if (groups != null) {
|
|
120
125
|
body.groups = groupList(groups);
|
|
@@ -122,6 +127,9 @@ export class Toolpass {
|
|
|
122
127
|
if (fresh) {
|
|
123
128
|
body.fresh = true;
|
|
124
129
|
}
|
|
130
|
+
if (token) {
|
|
131
|
+
body.token = token;
|
|
132
|
+
}
|
|
125
133
|
let response;
|
|
126
134
|
let raw;
|
|
127
135
|
try {
|
|
@@ -203,28 +211,31 @@ function isLoopback(hostname) {
|
|
|
203
211
|
}
|
|
204
212
|
function checkOptions(o) {
|
|
205
213
|
if (o == null) {
|
|
206
|
-
return { groups: null, fresh: false };
|
|
214
|
+
return { groups: null, fresh: false, token: "" };
|
|
207
215
|
}
|
|
208
216
|
if (Array.isArray(o)) {
|
|
209
|
-
return { groups: o, fresh: false };
|
|
217
|
+
return { groups: o, fresh: false, token: "" };
|
|
210
218
|
}
|
|
211
219
|
if (typeof o === "boolean") {
|
|
212
|
-
throw new TypeError("the fifth argument is the groups array or { groups, fresh }; pass { fresh: true } for a fresh check");
|
|
220
|
+
throw new TypeError("the fifth argument is the groups array or { groups, fresh, token }; pass { fresh: true } for a fresh check");
|
|
213
221
|
}
|
|
214
222
|
if (typeof o !== "object" || Symbol.iterator in o) {
|
|
215
223
|
// A Set or another iterable of groups is not an array of them.
|
|
216
224
|
throw new TypeError("groups must be an array of strings");
|
|
217
225
|
}
|
|
218
226
|
for (const key of Object.keys(o)) {
|
|
219
|
-
if (key !== "groups" && key !== "fresh") {
|
|
220
|
-
throw new TypeError(`unknown check option ${JSON.stringify(key)}; the options are groups and
|
|
227
|
+
if (key !== "groups" && key !== "fresh" && key !== "token") {
|
|
228
|
+
throw new TypeError(`unknown check option ${JSON.stringify(key)}; the options are groups, fresh and token`);
|
|
221
229
|
}
|
|
222
230
|
}
|
|
223
231
|
const opts = o;
|
|
224
232
|
if (opts.fresh !== undefined && typeof opts.fresh !== "boolean") {
|
|
225
233
|
throw new TypeError("fresh must be a boolean");
|
|
226
234
|
}
|
|
227
|
-
|
|
235
|
+
if (opts.token != null && typeof opts.token !== "string") {
|
|
236
|
+
throw new TypeError("token must be a string");
|
|
237
|
+
}
|
|
238
|
+
return { groups: opts.groups ?? null, fresh: opts.fresh === true, token: opts.token ?? "" };
|
|
228
239
|
}
|
|
229
240
|
function groupList(groups) {
|
|
230
241
|
if (typeof groups === "string" || !Array.isArray(groups) || !groups.every((g) => typeof g === "string")) {
|
|
@@ -251,7 +262,7 @@ function parse(status, raw) {
|
|
|
251
262
|
if (typeof data !== "object" || data === null || !("decision" in data)) {
|
|
252
263
|
return new Decision(UNKNOWN, `client_error: unexpected response (HTTP ${status})`, status);
|
|
253
264
|
}
|
|
254
|
-
const { decision, reason } = data;
|
|
265
|
+
const { decision, reason, identity } = data;
|
|
255
266
|
if (decision !== ALLOW && decision !== DENY && decision !== UNKNOWN) {
|
|
256
267
|
return new Decision(UNKNOWN, `client_error: unexpected decision ${JSON.stringify(decision)}`, status);
|
|
257
268
|
}
|
|
@@ -259,7 +270,7 @@ function parse(status, raw) {
|
|
|
259
270
|
// toolpass never allows with a non-200 status; do not trust a proxy that does.
|
|
260
271
|
return new Decision(UNKNOWN, `client_error: allow with HTTP ${status}`, status);
|
|
261
272
|
}
|
|
262
|
-
return new Decision(decision, reason === undefined ? "" : String(reason), status);
|
|
273
|
+
return new Decision(decision, reason === undefined ? "" : String(reason), status, typeof identity === "string" ? identity : "");
|
|
263
274
|
}
|
|
264
275
|
/**
|
|
265
276
|
* Resolve a user or groups source now.
|