-
Notifications
You must be signed in to change notification settings - Fork 10
feat: add preflight-checks endpoint for autofix deploy permission validation #2119
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
Open
FentPams
wants to merge
9
commits into
main
Choose a base branch
from
permission-checks
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
3b7eb09
feat: add preflight-checks endpoint for autofix deploy permission val…
FentPams 3ec0fa7
chore: merge main and resolve conflicts; rename preflight-checks to a…
FentPams 12cafb1
refactor: rename PreflightChecksController to AutofixChecksController
FentPams c73ac20
refactor: rename internal preflight references to autofix in controller
FentPams f8b065b
Merge branch 'main' into permission-checks
FentPams e0a8249
Merge branch 'main' into permission-checks
FentPams 4fd7051
chore: merge main and resolve conflicts
FentPams 0be4d03
Merge branch 'permission-checks' of github.com:adobe/spacecat-api-ser…
FentPams 10b9c6e
fix: remove duplicate route key in test causing CI failure
FentPams File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| /* | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| import { isNonEmptyObject, isNonEmptyArray } from '@adobe/spacecat-shared-utils'; | ||
| import { | ||
| badRequest, forbidden, internalServerError, notFound, ok, | ||
| } from '@adobe/spacecat-shared-http-utils'; | ||
| import AccessControlUtil from '../support/access-control-util.js'; | ||
| import checkHandlerRegistry from '../support/preflight-checks/registry.js'; | ||
|
|
||
| /** | ||
| * Preflight Checks Controller — runs server-side permission and capability | ||
| * checks for a site before autofix deploy. | ||
| * | ||
| * POST /sites/:siteId/preflight-checks | ||
| * | ||
| * Request body: | ||
| * { "checks": [{ "type": "content-api-access" }] } | ||
| * | ||
| * Response: | ||
| * { "siteId": "...", "checks": [{ "type", "status", "message" }] } | ||
| * | ||
| * @param {Object} ctx - Application context (dataAccess, log, etc.) | ||
| * @returns {Object} Controller with runChecks method | ||
| */ | ||
| function PreflightChecksController(ctx) { | ||
| if (!isNonEmptyObject(ctx?.dataAccess)) { | ||
| throw new Error('Valid data access configuration required'); | ||
| } | ||
|
|
||
| const { dataAccess, log } = ctx; | ||
| const { Site } = dataAccess; | ||
| const accessControlUtil = AccessControlUtil.fromContext(ctx); | ||
|
|
||
| /** | ||
| * Runs the requested preflight checks for a site. | ||
| * @param {Object} context - Request context with params.siteId and data.checks | ||
| * @returns {Promise<Object>} HTTP response | ||
| */ | ||
| const runChecks = async (context) => { | ||
| const { siteId } = context.params || {}; | ||
| const { checks } = context.data || {}; | ||
|
|
||
| if (!isNonEmptyArray(checks)) { | ||
| return badRequest('Request body must include a non-empty "checks" array'); | ||
| } | ||
|
|
||
| // Validate all requested check types before doing any work | ||
| const unknownTypes = checks | ||
| .map((c) => c?.type) | ||
| .filter((type) => !checkHandlerRegistry[type]); | ||
|
|
||
| if (unknownTypes.length > 0) { | ||
| return badRequest(`Unknown check type(s): ${unknownTypes.join(', ')}`); | ||
| } | ||
|
|
||
| try { | ||
| const site = await Site.findById(siteId); | ||
|
|
||
| if (!site) { | ||
| return notFound(`Site with ID ${siteId} not found`); | ||
| } | ||
|
|
||
| if (!await accessControlUtil.hasAccess(site)) { | ||
| return forbidden('User does not have access to this site'); | ||
| } | ||
|
|
||
| const results = await Promise.all( | ||
| checks.map(async (check) => { | ||
| const handler = checkHandlerRegistry[check.type]; | ||
| try { | ||
| return await handler(site, context, log); | ||
| } catch (error) { | ||
| log.error(`Preflight check "${check.type}" threw unexpectedly: ${error.message}`); | ||
| return { | ||
| type: check.type, | ||
| status: 'ERROR', | ||
| message: 'Check failed unexpectedly', | ||
| }; | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| return ok({ siteId, checks: results }); | ||
| } catch (error) { | ||
| log.error(`Preflight checks failed for site ${siteId}: ${error.message}`); | ||
| return internalServerError('Failed to run preflight checks'); | ||
| } | ||
| }; | ||
|
|
||
| return { runChecks }; | ||
| } | ||
|
|
||
| export default PreflightChecksController; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
src/support/preflight-checks/handlers/content-api-access.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| /* | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| import { tracingFetch as fetch } from '@adobe/spacecat-shared-utils'; | ||
|
|
||
| const CHECK_TYPE = 'content-api-access'; | ||
|
|
||
| /** | ||
| * Content API probe path — matches the UI-side probe | ||
| * (contentApiAccessCheck.ts in experience-success-studio-ui). | ||
| * Uses the experimental endpoint that AEM CS instances expose; | ||
| * returns 404 when Content API is not deployed (Rotary Release < 23963). | ||
| */ | ||
| const CONTENT_API_PROBE_PATH = '/adobe/experimental/expires-20251231/pages?limit=1'; | ||
FentPams marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Probes the AEM Author Content API to verify it is reachable and the caller | ||
| * has sufficient permissions. | ||
| * | ||
| * Endpoint: {authorURL}/adobe/experimental/expires-20251231/pages?limit=1 | ||
| * | ||
| * Granular failure detection: | ||
| * - Edge Delivery site → skipped (different deploy mechanism) | ||
| * - Network error / timeout → author instance unreachable | ||
| * - 404 → Content API not deployed | ||
| * - 401 / 403 → insufficient permissions | ||
| * - 2xx → Content API is accessible | ||
| * | ||
| * @param {Object} site - Site entity | ||
| * @param {Object} context - Request context (pathInfo.headers.authorization) | ||
| * @param {Object} log - Logger | ||
| * @returns {Promise<{type: string, status: string, message: string}>} | ||
| */ | ||
| export default async function contentApiAccessHandler(site, context, log) { | ||
| // Edge Delivery sites use a different deploy mechanism — skip | ||
| if (site.getDeliveryType() === 'aem_edge') { | ||
FentPams marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return { | ||
| type: CHECK_TYPE, | ||
| status: 'PASSED', | ||
| message: 'Edge Delivery site — Content API check not required', | ||
| }; | ||
| } | ||
|
|
||
| const deliveryConfig = site.getDeliveryConfig(); | ||
| const authorURL = deliveryConfig?.authorURL; | ||
|
|
||
| if (!authorURL) { | ||
| return { | ||
| type: CHECK_TYPE, | ||
| status: 'FAILED', | ||
| message: 'Site has no authorURL configured', | ||
| }; | ||
| } | ||
|
|
||
| const authorization = context.pathInfo?.headers?.authorization; | ||
| if (!authorization) { | ||
| return { | ||
| type: CHECK_TYPE, | ||
| status: 'FAILED', | ||
| message: 'Missing authorization header', | ||
| }; | ||
| } | ||
|
|
||
| const probeUrl = `${authorURL}${CONTENT_API_PROBE_PATH}`; | ||
|
|
||
| try { | ||
| const response = await fetch(probeUrl, { | ||
| method: 'GET', | ||
| headers: { Authorization: authorization }, | ||
| }); | ||
|
|
||
| if (response.ok) { | ||
| return { | ||
| type: CHECK_TYPE, | ||
| status: 'PASSED', | ||
| message: 'Content API is accessible', | ||
| }; | ||
| } | ||
|
|
||
| if (response.status === 404) { | ||
| return { | ||
| type: CHECK_TYPE, | ||
| status: 'FAILED', | ||
| message: 'Content API is not available on this AEM instance', | ||
| }; | ||
| } | ||
|
|
||
| if (response.status === 401 || response.status === 403) { | ||
| return { | ||
| type: CHECK_TYPE, | ||
| status: 'FAILED', | ||
| message: 'Insufficient permissions for Content API', | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| type: CHECK_TYPE, | ||
| status: 'FAILED', | ||
| message: `Content API returned unexpected status ${response.status}`, | ||
| }; | ||
| } catch (error) { | ||
| log.error(`Content API probe failed for ${authorURL}: ${error.message}`); | ||
| return { | ||
| type: CHECK_TYPE, | ||
| status: 'FAILED', | ||
| message: 'Author instance is not reachable', | ||
| }; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /* | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| import contentApiAccessHandler from './handlers/content-api-access.js'; | ||
|
|
||
| /** | ||
| * Registry of preflight check handlers. | ||
| * | ||
| * Each handler is an async function with the signature: | ||
| * (site, context, log) => Promise<{ type: string, status: string, message: string }> | ||
| * | ||
| * To add a new handler: | ||
| * 1. Create a file in ./handlers/ following the content-api-access.js pattern | ||
| * 2. Import it here and add an entry to the registry map | ||
| * | ||
| * The key is the check type string that callers pass in the request body. | ||
| */ | ||
| const checkHandlerRegistry = { | ||
| 'content-api-access': contentApiAccessHandler, | ||
| }; | ||
|
|
||
| export default checkHandlerRegistry; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.