Skip to content

Commit 41de0be

Browse files
ravvermaclaude
andauthored
feat(mac-giver-client): add MacGiver FACS permission client (#1711)
Introduces the new `@adobe/spacecat-shared-mac-giver-client` package — a thin client over the MacGiver `/api/facs/permissions/check` endpoint, consumed by `spacecat-auth-service` at login to mint the `facs_permissions` JWT claim. ## API - `checkListOfPermission({ userId, imsOrgId, permissions })` — evaluate an explicit list; returns the allowed subset. - `checkAllPermission({ userId, imsOrgId, namespaces })` — evaluate every permission in the given namespaces; returns the allowed subset. - `macGiverClientWrapper` attaches the client to `context`. ## Notable correctness details - Request body uses canonical FACS shapes: `subject { type: 'user' }` and `object { type: 'org' }` (not `'organization'`, which fails relation resolution → 500); `permissions` is sent only when non-empty. - Reads the bearer from `getServiceAccessToken().access_token` (throws if missing); does **not** send `X-User-Token` (FACS uses it for logging only). - Non-2xx / transport failure **throws** (tagged `warn`) so callers fail-safe; evaluated-but-empty returns `[]`. ## Tests 100% coverage (lines/branches/functions). Part of the MAC / FACS state-layer rollout. Pairs with a follow-up `http-utils` PR (AuthInfo FACS methods + constants + IMS-handler RBAC block) to unblock the auth-service PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bd46d3f commit 41de0be

14 files changed

Lines changed: 958 additions & 1 deletion

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ spacecat-shared/
7171
| **Utilities** | `utils` | Validation, S3/SQS helpers, schemas, date functions |
7272
| **Secrets** | `vault-secrets` | HashiCorp Vault integration (AppRole auth, KV secrets) |
7373
| **HTML** | `html-analyzer` | HTML analysis and extraction |
74-
| **API clients** | `athena-client`, `brand-client`, `cloud-manager-client`, `content-client`, `drs-client`, `google-client`, `gpt-client`, `ims-client`, `launchdarkly-client`, `mysticat-shared-seo-client`, `project-engine-client`, `rum-api-client`, `scrape-client`, `slack-client`, `splunk-client`, `tier-client`, `tokowaka-client`, `user-manager-client` | External service integrations |
74+
| **API clients** | `athena-client`, `brand-client`, `cloud-manager-client`, `content-client`, `drs-client`, `google-client`, `gpt-client`, `ims-client`, `launchdarkly-client`, `mac-giver-client`, `mysticat-shared-seo-client`, `project-engine-client`, `rum-api-client`, `scrape-client`, `slack-client`, `splunk-client`, `tier-client`, `tokowaka-client`, `user-manager-client` | External service integrations |
7575
| **Example** | `example` | Template for creating new packages |
7676

7777
### Package-Level CLAUDE.md Files

package-lock.json

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"reporterEnabled": "spec,xunit",
3+
"xunitReporterOptions": {
4+
"output": "junit/test-results.xml"
5+
}
6+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"reporter": [
3+
"lcov",
4+
"text"
5+
],
6+
"check-coverage": true,
7+
"lines": 100,
8+
"branches": 97,
9+
"statements": 100,
10+
"all": true,
11+
"include": [
12+
"src/**/*.js"
13+
]
14+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module.exports = {
2+
extends: "semantic-release-monorepo",
3+
plugins: [
4+
["@semantic-release/commit-analyzer", {
5+
"preset": "conventionalcommits",
6+
}],
7+
["@semantic-release/release-notes-generator", {
8+
"preset": "conventionalcommits",
9+
}],
10+
["@semantic-release/changelog", {
11+
"changelogFile": "CHANGELOG.md",
12+
}],
13+
...(process.env.SR_NO_NPM_AUTH === 'true' ? [] : ["@semantic-release/npm"]),
14+
["@semantic-release/git", {
15+
"assets": ["package.json", "CHANGELOG.md"],
16+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
17+
}],
18+
["@semantic-release/github", {}],
19+
],
20+
branches: ['main'],
21+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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. |
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)