-
Notifications
You must be signed in to change notification settings - Fork 21
MANTA-5521 Add optional scope parameter to CreateAccessKey, UpdateAccessKey, and DeleteAccessKey CloudAPI endpoints. #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c896c78
cb75389
0ef966c
fad80bd
8f7fcf4
f754961
0c22b07
880d1aa
a18b2f0
13a468a
de31e7b
0ce714f
23357a8
2dd70d0
4ebd180
6f3e2a3
41a3169
8498be3
3ebbd85
965c478
1867efb
f0f68bb
c4b61e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ | |
|
|
||
| /* | ||
| * Copyright 2020 Joyent, Inc. | ||
| * Copyright 2025 Edgecast Cloud LLC. | ||
| * Copyright 2026 Edgecast Cloud LLC. | ||
| */ | ||
|
|
||
|
|
||
|
|
@@ -17,6 +17,7 @@ var assert = require('assert-plus'); | |
| var restify = require('restify'); | ||
|
|
||
| var resources = require('../resources'); | ||
| var scopeSchema = require('mahi').scopeSchema; | ||
|
|
||
|
|
||
| var sprintf = util.format; | ||
|
|
@@ -31,6 +32,30 @@ var InternalError = restify.InternalError; | |
| */ | ||
| var MAX_KEYS = 100; | ||
|
|
||
| /** | ||
| * @brief Validate a scope parameter from the request | ||
| * | ||
| * Delegates to the canonical scope schema module in node-mahi. Accepts only the | ||
| * canonical envelope: {"version":1,"permissions":[{bucket,level},...]} | ||
| * | ||
| * @param {Object} input - Scope envelope object | ||
| * @return {Object} {valid: bool, scope: string|null, | ||
| * error: string|null} | ||
| */ | ||
| function validateScope(input) { | ||
| return scopeSchema.validateScope(input); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * @brief Translate UFDS access key to API response format | ||
| * | ||
| * Maps internal UFDS attributes to the external API representation. Includes | ||
| * the accesskeyscope field as a parsed JSON object when present. | ||
| * | ||
| * @param {Object} accesskey - UFDS access key entry | ||
| * @return {Object} Translated access key for response | ||
| */ | ||
| function translateAccessKey(accesskey) { | ||
| if (!accesskey) { | ||
| return {}; | ||
|
|
@@ -65,6 +90,20 @@ function translateAccessKey(accesskey) { | |
| translated.expiration = null; | ||
| } | ||
|
|
||
| /* | ||
| * Return bucket scope as a parsed JSON object so callers can inspect | ||
| * permissions directly. When absent the key is unrestricted. | ||
| */ | ||
| if (accesskey.accesskeyscope) { | ||
| try { | ||
| translated.scope = JSON.parse(accesskey.accesskeyscope); | ||
| } catch (_e) { | ||
| translated.scope = null; | ||
| } | ||
| } else { | ||
| translated.scope = null; | ||
| } | ||
|
|
||
| return translated; | ||
| } | ||
|
|
||
|
|
@@ -107,6 +146,22 @@ function create(req, res, next) { | |
| return; | ||
| } | ||
|
|
||
| /* | ||
| * Per-bucket access key scoping. Scope must be the canonical envelope: | ||
| * {"version":1,"permissions":[{bucket,level},...]} | ||
| * | ||
| * Absent/null scope means unrestricted. | ||
| */ | ||
| if (req.params.scope !== undefined && | ||
| req.params.scope !== null) { | ||
| var result = validateScope(req.params.scope); | ||
| if (!result.valid) { | ||
| next(new InvalidArgumentError(result.error)); | ||
| return; | ||
| } | ||
| params.accesskeyscope = result.scope; | ||
| } | ||
|
|
||
| try { | ||
| vasync.waterfall([ | ||
|
|
||
|
|
@@ -155,6 +210,16 @@ function create(req, res, next) { | |
|
|
||
| var accesskeysecret = accesskey.accesskeysecret; | ||
|
|
||
| /* | ||
| * UFDS is authoritative. On a Redis miss, the | ||
| * mahi sigv4 read-through resolves the new key | ||
| * directly from UFDS on every authcache | ||
| * instance — that is what makes the new key | ||
| * usable on the first signed request without | ||
| * waiting for the replicator. We do not write | ||
| * to mahi from here. | ||
| */ | ||
|
|
||
| accesskey = translateAccessKey(accesskey); | ||
|
|
||
| // Only return the accesskeysecret on creation | ||
|
|
@@ -317,9 +382,24 @@ function del(req, res, next) { | |
| return; | ||
| } | ||
|
|
||
| /* | ||
| * UFDS is authoritative. Each authcache instance has | ||
| * its own mahi-replicator that polls UFDS and applies | ||
| * the deletion to local Redis on its own schedule. On | ||
| * a Redis miss the sigv4 read-through resolves the | ||
| * current state from UFDS directly. We do not call | ||
| * mahi from here: per-instance cache mutation from | ||
| * cloudapi was a misleading optimisation — it only | ||
| * ever reached the single mahi instance cloudapi | ||
| * could resolve by name, never the Manta authcache | ||
| * pool that serves the S3 data plane. The replicator | ||
| * is the consistency mechanism for every Redis | ||
| * holding this key. | ||
| */ | ||
| log.debug('DELETE %s -> ok', req.path()); | ||
| res.send(204); | ||
| next(); | ||
| return; | ||
| }); | ||
| } catch (e) { | ||
| log.error({err: e}, 'delete accesskey exception'); | ||
|
|
@@ -354,6 +434,26 @@ function update(req, res, next) { | |
| params.description = req.params.description; | ||
| } | ||
|
|
||
| /* | ||
| * Update bucket scope. An empty string or null removes the scope (makes | ||
| * the key unrestricted again). A non-empty value replaces the scope. | ||
| */ | ||
| if (req.params.scope !== undefined) { | ||
| if (req.params.scope === '' || | ||
| req.params.scope === null) { | ||
| /* Remove scope: set null so UFDS deletes attr */ | ||
| params.accesskeyscope = null; | ||
| } else { | ||
| var scopeResult = validateScope(req.params.scope); | ||
| if (!scopeResult.valid) { | ||
| next(new InvalidArgumentError( | ||
| scopeResult.error)); | ||
| return; | ||
| } | ||
| params.accesskeyscope = scopeResult.scope; | ||
| } | ||
| } | ||
|
|
||
| // Make it clear that credential type and expiration cannot be changed. | ||
| if (req.params.credentialtype) { | ||
| next(new ForbiddenError('credentialtype cannot be set via CloudAPI')); | ||
|
|
@@ -374,35 +474,45 @@ function update(req, res, next) { | |
| return; | ||
| } | ||
|
|
||
| /* | ||
| * UFDS is authoritative. Each authcache instance has | ||
| * its own mahi-replicator that polls UFDS and | ||
| * applies the update to local Redis on its own | ||
| * schedule. We do not call mahi from here — see the | ||
| * matching comment in del() above. | ||
| */ | ||
| accesskey = translateAccessKey(accesskey); | ||
|
|
||
| if (account) { | ||
| res.header('Location', | ||
| sprintf('/%s/users/%s/accesskeys/%s', | ||
| login, | ||
| sprintf('/%s/users/%s/accesskeys/%s', login, | ||
| user, | ||
| encodeURIComponent(accesskey.accesskeyid))); | ||
| encodeURIComponent( | ||
| accesskey.accesskeyid))); | ||
| } else { | ||
| res.header('Location', | ||
| sprintf('/%s/accesskeys/%s', | ||
| login, | ||
| encodeURIComponent(accesskey.accesskeyid))); | ||
| sprintf('/%s/accesskeys/%s', login, | ||
| encodeURIComponent( | ||
| accesskey.accesskeyid))); | ||
| } | ||
|
|
||
| if (req.headers['role-tag'] || req.activeRoles) { | ||
| // The resource we want to save is the individual one we've | ||
| // just created, not the collection URI: | ||
| /* | ||
| * The resource we want to save is the | ||
| * individual one we've just updated, not the | ||
| * collection URI. | ||
| */ | ||
| req.resourcename = req.resourcename + '/' + | ||
| accesskey.accesskeyid; | ||
| req.resource = { | ||
| req.resource = { | ||
| name: req.resourcename, | ||
| account: req.account.uuid, | ||
| roles: [] | ||
| }; | ||
| } | ||
|
|
||
| log.debug('POST %s => %j', req.path(), accesskey); | ||
| res.send(201, accesskey); | ||
| res.send(200, accesskey); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oof, thanks for fixing that. Could you add this change as an entry to the version changelog here: https://github.com/TritonDataCenter/sdc-cloudapi/blob/master/docs/index.md#versions Along with an entry for the new scope prop? Also separately, could you add details about the new |
||
| next(); | ||
| return; | ||
| }); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.