@adobe/spacecat-shared-mac-giver-client 1.0.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 +77 -0
- package/package.json +43 -0
- package/src/index.js +19 -0
- package/src/mac-giver-client-wrapper.js +29 -0
- package/src/mac-giver-client.js +191 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Spacecat Shared - MacGiver Client
|
|
2
|
+
|
|
3
|
+
A thin client over the MacGiver FACS `/api/facs/permissions/check` endpoint for
|
|
4
|
+
evaluating a single user's permissions within an IMS organization. Consumed by
|
|
5
|
+
`spacecat-auth-service` at login to mint the `facs_permissions` JWT claim.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @adobe/spacecat-shared-mac-giver-client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Create from a Universal context
|
|
16
|
+
|
|
17
|
+
The wrapper attaches the client to `context.macGiverClient` (place it after the
|
|
18
|
+
IMS client wrapper, since it requires `context.imsClient`):
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
import { macGiverClientWrapper } from '@adobe/spacecat-shared-mac-giver-client';
|
|
22
|
+
|
|
23
|
+
export const main = wrap(run)
|
|
24
|
+
.with(macGiverClientWrapper);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or construct directly:
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
import { MacGiverClient } from '@adobe/spacecat-shared-mac-giver-client';
|
|
31
|
+
|
|
32
|
+
const client = MacGiverClient.createFrom(context);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Check an explicit list of permissions
|
|
36
|
+
|
|
37
|
+
Returns the subset of the requested permissions that are allowed:
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
const allowed = await client.checkListOfPermission({
|
|
41
|
+
userId: '17837D23...@AdobeID',
|
|
42
|
+
imsOrgId: 'AAAA...CCCC@AdobeOrg',
|
|
43
|
+
permissions: ['llmo/can_view', 'llmo/can_manage_users'],
|
|
44
|
+
});
|
|
45
|
+
// → ['llmo/can_view']
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Check all permissions in a namespace
|
|
49
|
+
|
|
50
|
+
Evaluates every permission defined in the given namespaces and returns the
|
|
51
|
+
allowed subset:
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
const allowed = await client.checkAllPermission({
|
|
55
|
+
userId: '17837D23...@AdobeID',
|
|
56
|
+
imsOrgId: 'AAAA...CCCC@AdobeOrg',
|
|
57
|
+
namespaces: ['llmo'],
|
|
58
|
+
});
|
|
59
|
+
// → ['llmo/can_view', 'llmo/can_manage_users']
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Behavior
|
|
63
|
+
|
|
64
|
+
- **Evaluated, none granted** — a `2xx` `SUCCESS` response with no `allowed: true`
|
|
65
|
+
entries (or a non-`SUCCESS` status) returns `[]`.
|
|
66
|
+
- **Could not evaluate** — a non-`2xx` response or transport failure **throws**
|
|
67
|
+
(with a `tag: 'macgiver'` warning), so callers can fail-safe (e.g. omit the
|
|
68
|
+
`facs_permissions` claim) rather than confusing an outage with "no permissions".
|
|
69
|
+
- The request is bounded by `AbortSignal.timeout` (default `5000` ms, tunable via
|
|
70
|
+
the `MACGIVER_TIMEOUT_MS` env var) since MacGiver sits on the login critical path.
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
| Env var | Default | Description |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `MACGIVER_BASE_URL` | `http://localhost:8080` | MacGiver service base URL. |
|
|
77
|
+
| `MACGIVER_TIMEOUT_MS` | `5000` | Per-request timeout in milliseconds. |
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adobe/spacecat-shared-mac-giver-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared modules of the Spacecat Services - MacGiver Client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.0.0 <25.0.0",
|
|
8
|
+
"npm": ">=10.9.0 <12.0.0"
|
|
9
|
+
},
|
|
10
|
+
"main": "src/index.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "c8 mocha",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint --fix .",
|
|
15
|
+
"clean": "rm -rf package-lock.json node_modules"
|
|
16
|
+
},
|
|
17
|
+
"mocha": {
|
|
18
|
+
"reporter": "mocha-multi-reporters",
|
|
19
|
+
"reporter-options": "configFile=.mocha-multi.json",
|
|
20
|
+
"spec": "test/**/*.test.js"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/adobe/spacecat-shared.git"
|
|
25
|
+
},
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/adobe/spacecat-shared/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/adobe/spacecat-shared/packages/spacecat-shared-mac-giver-client/#readme",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"chai": "6.2.2",
|
|
38
|
+
"chai-as-promised": "8.0.2",
|
|
39
|
+
"nock": "14.0.11",
|
|
40
|
+
"sinon": "21.0.3",
|
|
41
|
+
"sinon-chai": "4.0.1"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import MacGiverClient from './mac-giver-client.js';
|
|
14
|
+
import { macGiverClientWrapper } from './mac-giver-client-wrapper.js';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
MacGiverClient,
|
|
18
|
+
macGiverClientWrapper,
|
|
19
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import MacGiverClient from './mac-giver-client.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wrapper that creates a MacGiverClient and attaches it to context.macGiverClient.
|
|
17
|
+
* Must be placed after imsClientWrapper in the chain — it requires context.imsClient.
|
|
18
|
+
*
|
|
19
|
+
* @param {UniversalAction} fn
|
|
20
|
+
* @returns {function(object, UniversalContext): Promise<Response>}
|
|
21
|
+
*/
|
|
22
|
+
export function macGiverClientWrapper(fn) {
|
|
23
|
+
return async (request, context) => {
|
|
24
|
+
if (!context.macGiverClient) {
|
|
25
|
+
context.macGiverClient = MacGiverClient.createFrom(context);
|
|
26
|
+
}
|
|
27
|
+
return fn(request, context);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BASE_URL = 'http://localhost:8080';
|
|
14
|
+
const CHECK_PATH = '/api/facs/permissions/check';
|
|
15
|
+
// MacGiver sits on the login critical path; bound the request so a hung
|
|
16
|
+
// dependency cannot hang login indefinitely. AbortSignal.timeout is native in
|
|
17
|
+
// Node 22+.
|
|
18
|
+
const REQUEST_TIMEOUT_MS = 5000;
|
|
19
|
+
|
|
20
|
+
export default class MacGiverClient {
|
|
21
|
+
static createFrom(context) {
|
|
22
|
+
const { log = console } = context;
|
|
23
|
+
if (!context.imsClient) {
|
|
24
|
+
throw new Error('MacGiverClient.createFrom: context.imsClient is required');
|
|
25
|
+
}
|
|
26
|
+
const macGiverBaseUrl = context.env?.MACGIVER_BASE_URL || DEFAULT_BASE_URL;
|
|
27
|
+
const requestTimeoutMs = Number(context.env?.MACGIVER_TIMEOUT_MS) || REQUEST_TIMEOUT_MS;
|
|
28
|
+
return new MacGiverClient({
|
|
29
|
+
macGiverBaseUrl, imsClient: context.imsClient, log, requestTimeoutMs,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
constructor({
|
|
34
|
+
macGiverBaseUrl, imsClient, log, requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
35
|
+
}) {
|
|
36
|
+
this.macGiverBaseUrl = macGiverBaseUrl;
|
|
37
|
+
this.imsClient = imsClient;
|
|
38
|
+
this.log = log;
|
|
39
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Calls the MacGiver `/api/facs/permissions/check` endpoint for a single
|
|
44
|
+
* (user, org) pair and returns the subset of evaluated permissions where
|
|
45
|
+
* `allowed === true`.
|
|
46
|
+
*
|
|
47
|
+
* `permissions` and `namespaces` map directly onto the request body:
|
|
48
|
+
* - `permissions` — evaluate this explicit list of (possibly namespaced)
|
|
49
|
+
* permissions.
|
|
50
|
+
* - `namespaces` — evaluate every permission defined in these namespaces.
|
|
51
|
+
* At least one must be non-empty (MacGiver rejects a request where both are
|
|
52
|
+
* empty); the public methods below each supply exactly one.
|
|
53
|
+
*
|
|
54
|
+
* The `X-User-Token` header is intentionally omitted — FACS uses it only for
|
|
55
|
+
* logging, never for the evaluation itself, so the service token alone is
|
|
56
|
+
* sufficient for SpaceCat's use.
|
|
57
|
+
*
|
|
58
|
+
* Two outcomes are reported distinctly:
|
|
59
|
+
* - **Evaluated, none granted** — 2xx with `status: 'SUCCESS'` and no
|
|
60
|
+
* `allowed: true` entries (or a non-SUCCESS status). Returns `[]`.
|
|
61
|
+
* - **Could not evaluate** — non-2xx, or a transport-layer failure. **Throws**
|
|
62
|
+
* so callers (e.g. login.js) can fail-safe and omit the `facs_permissions`
|
|
63
|
+
* claim rather than confusing an outage with "no permissions". The non-2xx
|
|
64
|
+
* path is logged with a `tag: 'macgiver'` warning for independent alerting.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} args
|
|
67
|
+
* @param {string} args.userId - IMS user id (subject).
|
|
68
|
+
* @param {string} args.imsOrgId - IMS org id (object).
|
|
69
|
+
* @param {string[]} [args.permissions] - Explicit permissions to evaluate.
|
|
70
|
+
* @param {string[]} [args.namespaces] - Namespaces whose permissions to evaluate.
|
|
71
|
+
* @returns {Promise<string[]>} Allowed permission names.
|
|
72
|
+
*/
|
|
73
|
+
async #check({
|
|
74
|
+
userId, imsOrgId, permissions = [], namespaces = [],
|
|
75
|
+
}) {
|
|
76
|
+
const url = `${this.macGiverBaseUrl}${CHECK_PATH}`;
|
|
77
|
+
// FACS resolves permissions via `<object_type>#<permission>` relations.
|
|
78
|
+
// The canonical IMS-org subject type in FACS is `org`, not `organization`
|
|
79
|
+
// — sending `organization` here makes FACS fail to resolve the relation
|
|
80
|
+
// and mac-giver surfaces it as a 500.
|
|
81
|
+
const subject = { type: 'user', id: userId };
|
|
82
|
+
const object = { type: 'org', id: imsOrgId };
|
|
83
|
+
// Include only the non-empty list. MacGiver requires at least one of
|
|
84
|
+
// `permissions` / `namespaces`; the public methods each supply exactly one,
|
|
85
|
+
// so omitting the empty one keeps the body symmetric and future-proof
|
|
86
|
+
// against stricter MacGiver validation.
|
|
87
|
+
const requestBody = { subject, object };
|
|
88
|
+
if (permissions.length > 0) {
|
|
89
|
+
requestBody.permissions = permissions;
|
|
90
|
+
}
|
|
91
|
+
if (namespaces.length > 0) {
|
|
92
|
+
requestBody.namespaces = namespaces;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ImsClient.getServiceAccessToken() resolves to
|
|
96
|
+
// { access_token, expires_in, token_type } — the bearer is in `access_token`.
|
|
97
|
+
// Templating the whole object would send `Bearer [object Object]` and FACS
|
|
98
|
+
// would reject it with 401.
|
|
99
|
+
const { access_token: serviceToken } = await this.imsClient.getServiceAccessToken();
|
|
100
|
+
if (!serviceToken) {
|
|
101
|
+
throw new Error('MacGiver check: IMS service token is missing access_token');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const res = await fetch(url, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: {
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
Authorization: `Bearer ${serviceToken}`,
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify(requestBody),
|
|
111
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
// Distinguish "could not evaluate" from "evaluated, none". Login wraps
|
|
116
|
+
// this call in a try/catch that logs and omits facs_permissions, so a
|
|
117
|
+
// throw here is the right shape: the user still logs in, but without
|
|
118
|
+
// FACS perms, and ops gets a tagged signal that MacGiver is degraded.
|
|
119
|
+
this.log.warn(
|
|
120
|
+
{
|
|
121
|
+
tag: 'macgiver', status: res.status, userId, imsOrgId,
|
|
122
|
+
},
|
|
123
|
+
'MacGiver permission lookup returned non-2xx',
|
|
124
|
+
);
|
|
125
|
+
throw new Error(`MacGiver returned ${res.status}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const json = await res.json();
|
|
129
|
+
if (json.status !== 'SUCCESS' || !json.results) {
|
|
130
|
+
// A non-SUCCESS status is an unexpected MacGiver condition, not "the user
|
|
131
|
+
// has no permissions" — log it (errors-only) so operators can tell them
|
|
132
|
+
// apart. A SUCCESS-with-no-results is genuinely empty and stays quiet.
|
|
133
|
+
if (json.status !== 'SUCCESS') {
|
|
134
|
+
this.log.warn(
|
|
135
|
+
{
|
|
136
|
+
tag: 'macgiver', status: json.status, userId, imsOrgId,
|
|
137
|
+
},
|
|
138
|
+
'MacGiver returned a non-SUCCESS status',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Object.entries(json.results)
|
|
145
|
+
.filter(([, v]) => v?.allowed === true)
|
|
146
|
+
.map(([permission]) => permission);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Checks an explicit list of FACS permissions for a (user, org) pair in one
|
|
151
|
+
* round-trip. Returns the subset of the requested permissions where
|
|
152
|
+
* `allowed === true`. See `#check` for the evaluated-vs-unavailable contract.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} args
|
|
155
|
+
* @param {string} args.userId
|
|
156
|
+
* @param {string} args.imsOrgId
|
|
157
|
+
* @param {string[]} args.permissions - The permissions to evaluate.
|
|
158
|
+
* @returns {Promise<string[]>} Allowed subset of `permissions`.
|
|
159
|
+
*/
|
|
160
|
+
async checkListOfPermission({ userId, imsOrgId, permissions }) {
|
|
161
|
+
if (!userId || !imsOrgId) {
|
|
162
|
+
throw new Error('MacGiver checkListOfPermission: userId and imsOrgId are required');
|
|
163
|
+
}
|
|
164
|
+
if (!Array.isArray(permissions) || permissions.length === 0) {
|
|
165
|
+
throw new Error('MacGiver checkListOfPermission: permissions must be a non-empty array');
|
|
166
|
+
}
|
|
167
|
+
return this.#check({ userId, imsOrgId, permissions });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns every FACS permission the (user, org) pair holds across the given
|
|
172
|
+
* namespaces — MacGiver evaluates all permissions defined in each namespace
|
|
173
|
+
* and this returns the allowed subset. See `#check` for the
|
|
174
|
+
* evaluated-vs-unavailable contract.
|
|
175
|
+
*
|
|
176
|
+
* @param {object} args
|
|
177
|
+
* @param {string} args.userId
|
|
178
|
+
* @param {string} args.imsOrgId
|
|
179
|
+
* @param {string[]} args.namespaces - The namespaces whose permissions to evaluate.
|
|
180
|
+
* @returns {Promise<string[]>} All allowed permissions across `namespaces`.
|
|
181
|
+
*/
|
|
182
|
+
async checkAllPermission({ userId, imsOrgId, namespaces }) {
|
|
183
|
+
if (!userId || !imsOrgId) {
|
|
184
|
+
throw new Error('MacGiver checkAllPermission: userId and imsOrgId are required');
|
|
185
|
+
}
|
|
186
|
+
if (!Array.isArray(namespaces) || namespaces.length === 0) {
|
|
187
|
+
throw new Error('MacGiver checkAllPermission: namespaces must be a non-empty array');
|
|
188
|
+
}
|
|
189
|
+
return this.#check({ userId, imsOrgId, namespaces });
|
|
190
|
+
}
|
|
191
|
+
}
|