├── .eslintignore ├── .npmrc ├── e2e ├── jest.config.js └── README.md ├── COPYRIGHT ├── .github ├── workflows │ ├── daily.yml │ ├── node.js.yml │ ├── smoke-tests.yml │ └── deploy-api.yml ├── dependabot.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── test ├── fixtures │ ├── list │ │ ├── response.filter-value-test-events.json │ │ ├── response.complex-filter.exclusion-filter.json │ │ ├── registry-with-dev-console.json │ │ ├── response.only-app-builder-templates.json │ │ ├── response.filter-value-none-runtime.json │ │ ├── response.not-approved.json │ │ ├── registry2.json │ │ ├── registry-bad-response.json │ │ ├── response.filter-value-any-events.json │ │ ├── response.complex-filter.json │ │ ├── response.simple-filter.json │ │ ├── response.with-dev-console-templates.json │ │ ├── response.filter-value-none-extensions.json │ │ ├── response.simple-filter.exclusion-filter.json │ │ ├── response.orderBy.multiple.json │ │ ├── registry.json │ │ ├── response.github.issues.json │ │ ├── response.exclusion-filter.only.json │ │ ├── response.filter-value-any.json │ │ ├── response.orderBy.names.desc.json │ │ ├── response.orderBy.names.asc.json │ │ ├── response.full.no-review-issues.json │ │ └── response.full.json │ └── smoke │ │ └── template.console.json ├── jest.setup.smoke.js ├── jest.setup.js ├── metrics.test.js ├── mongo.test.js ├── smoke.test.js ├── acrs.test.js ├── ims.test.js └── utils.test.js ├── .eslintrc ├── .gitignore ├── .env.example ├── scripts └── updateSwaggerVersion.js ├── CONTRIBUTING.md ├── db └── mongo.js ├── actions ├── metrics.js ├── acrs.js ├── ims.js ├── templateEntitlement.js ├── templates │ ├── delete │ │ └── index.js │ ├── get │ │ └── index.js │ └── put │ │ └── index.js ├── utils.js └── templateRegistry.js ├── README.md ├── package.json ├── CODE_OF_CONDUCT.md └── app.config.yaml /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | verbose: true, 4 | testRegex: './e2e/e2e.js' 5 | }; 6 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | © Copyright 2024 Adobe. All rights reserved. 2 | 3 | Adobe holds the copyright for all the files found in this repository. 4 | 5 | See the LICENSE file for licensing information. 6 | -------------------------------------------------------------------------------- /.github/workflows/daily.yml: -------------------------------------------------------------------------------- 1 | name: Daily - Do npm install and run all unit tests 2 | 3 | on: 4 | schedule: 5 | # run daily at midnight 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | build: 10 | uses: adobe/aio-reusable-workflows/.github/workflows/daily.yml@main 11 | secrets: inherit -------------------------------------------------------------------------------- /test/fixtures/list/response.filter-value-test-events.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?events=test" 7 | } 8 | }, 9 | "items": [ 10 | 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/list/response.complex-filter.exclusion-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?names=@author/app-builder-template-2&categories=!events,ui,action&apis=Events&adobeRecommended=true" 7 | } 8 | }, 9 | "items": [ 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- 1 | # E2E Tests 2 | 3 | ## Requirements 4 | 5 | To run the e2e test you'll need these env variables set: 6 | 0. Create `.env` in `e2e` directory and SET: 7 | 1. `TEMPLATE_REGISTRY_API_URL` 8 | 2. `AUTH_TOKEN` 9 | 3. `IMS_CLIENT_ID`=` 10 | 4. `IMS_CLIENT_SECRET`=` 11 | 5. `IMS_AUTH_CODE`=` 12 | 6. `IMS_SCOPES`=` 13 | 7. `IMS_URL`=` 14 | 8. `ACCESS_TOKEN` 15 | 16 | ## Run 17 | 18 | `npm run e2e` 19 | 20 | ## Test overview 21 | 22 | The tests cover: 23 | 24 | 1. Template Registry API CRUD Operations -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | uses: adobe/aio-reusable-workflows/.github/workflows/node.js.yml@main 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 12, 5 | "sourceType": "module" 6 | }, 7 | "extends": [ 8 | "@adobe/eslint-config-aio-lib-config" 9 | ], 10 | "plugins": ["jest"], 11 | "rules": { 12 | "indent": ["error", 2, { "SwitchCase": 1 }], 13 | "semi": ["error", "always"], 14 | "quotes": ["error", "single"], 15 | "no-trailing-spaces": ["error"], 16 | "node/no-unpublished-require": 0, 17 | "node/no-extraneous-require": 0 18 | }, 19 | "env": { 20 | "es2021": true, 21 | "node": true, 22 | "jest/globals": true 23 | } 24 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # package directories 3 | node_modules 4 | jspm_packages 5 | 6 | # lock file 7 | package-lock.json 8 | 9 | # build 10 | build 11 | dist 12 | .manifest-dist.yml 13 | 14 | # Config 15 | config.json 16 | .env 17 | 18 | # Adobe I/O console config 19 | console.json 20 | 21 | # Test output 22 | junit.xml 23 | 24 | # IDE & Temp 25 | .cache 26 | .idea 27 | .nyc_output 28 | .vscode 29 | coverage 30 | .aws.tmp.creds.json 31 | .wskdebug.props.tmp 32 | 33 | # Parcel 34 | .parcel-cache 35 | 36 | # OSX 37 | .DS_Store 38 | 39 | # yeoman 40 | .yo-repository 41 | 42 | # logs folder for aio-run-detached 43 | logs 44 | 45 | # yalc 46 | .yalc 47 | yalc.lock -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ### Expected Behaviour 5 | 6 | ### Actual Behaviour 7 | 8 | ### Reproduce Scenario (including but not limited to) 9 | 10 | #### Steps to Reproduce 11 | 12 | #### Platform and Version 13 | 14 | #### Sample Code that illustrates the problem 15 | 16 | #### Logs taken while reproducing problem 17 | -------------------------------------------------------------------------------- /test/jest.setup.smoke.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | jest.setTimeout(30000); 13 | 14 | global.console = { 15 | ...console, 16 | // comment to enable a specific log level 17 | log: jest.fn(), 18 | debug: jest.fn(), 19 | info: jest.fn(), 20 | warn: jest.fn(), 21 | error: jest.fn() 22 | }; 23 | -------------------------------------------------------------------------------- /test/fixtures/smoke/template.console.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-template-smoke-tests", 3 | "description": "Test template", 4 | "latestVersion": "0.0.1", 5 | "links": { 6 | "consoleProject": "https://developer-stage.adobe.com/console/projects/918/4566206088344750634/overview" 7 | }, 8 | "createdBy": "GitHub Actions", 9 | "codeSamples": [ 10 | { 11 | "language": "javascript", 12 | "link": "https://developer-stage.adobe.com/package.zip" 13 | } 14 | ], 15 | "adobeRecommended": false, 16 | "author": "Adobe, Inc. Test", 17 | "status": "Approved", 18 | "apis": [ 19 | { 20 | "code": "AssetComputeSDK", 21 | "credentialType": "oauth" 22 | } 23 | ], 24 | "credentials": [ 25 | { 26 | "type": "oauth", 27 | "flowType": "adobeid" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /test/jest.setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | jest.setTimeout(30000); 13 | 14 | const nock = require('nock'); 15 | nock.disableNetConnect(); 16 | 17 | afterEach(() => { 18 | nock.cleanAll(); 19 | }); 20 | 21 | global.console = { 22 | ...console, 23 | // comment to enable a specific log level 24 | log: jest.fn(), 25 | debug: jest.fn(), 26 | info: jest.fn(), 27 | warn: jest.fn(), 28 | error: jest.fn() 29 | }; 30 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ## Adobe I/O Runtime credentials 2 | AIO_runtime_auth= 3 | AIO_runtime_namespace= 4 | AIO_runtime_apihost= 5 | ## Adobe I/O Console service account credentials (JWT) Api Key 6 | SERVICE_API_KEY= 7 | 8 | TEMPLATE_REGISTRY_ORG= 9 | TEMPLATE_REGISTRY_REPOSITORY= 10 | ## Github Access Token to Template Registry Github repo 11 | ACCESS_TOKEN_GITHUB= 12 | 13 | IMS_URL= 14 | # The IMS client ID of the resource server, for example, 'ims-client-unique-name' 15 | IMS_CLIENT_ID= 16 | # The auth code for the IMS client 17 | IMS_AUTH_CODE= 18 | # The IMS client secret of the resource server 19 | IMS_CLIENT_SECRET= 20 | # The IMS scopes needed to interact with Console APIs 21 | IMS_SCOPES= 22 | # a comma separated list of IMS organizations, for example, 'adminOrg1@AdobeOrg,adminOrg2@AdobeOrg' 23 | ADMIN_IMS_ORGANIZATIONS= 24 | 25 | # Publicly-facing Template Registry API URL 26 | TEMPLATE_REGISTRY_API_URL= 27 | 28 | # Mongodb Credentials 29 | MONGODB_NAME= 30 | MONGODB_URI= 31 | 32 | # (Optional) URL for metrics service 33 | METRICS_URL= 34 | -------------------------------------------------------------------------------- /test/fixtures/list/registry-with-dev-console.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 4 | "name": "@author/app-builder-template-4", 5 | "status": "InVerification", 6 | "links": { 7 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 8 | "github": "https://github.com/author/app-builder-template-4" 9 | } 10 | }, 11 | { 12 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 13 | "name": "@author/app-builder-template-5", 14 | "status": "Rejected", 15 | "links": { 16 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 17 | "github": "https://github.com/author/app-builder-template-5" 18 | } 19 | }, 20 | { 21 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 22 | "name": "app-builder-template-6", 23 | "status": "Rejected", 24 | "links": { 25 | "consoleProject": "https://developer-stage.adobe.com/console/projects/1234" 26 | } 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /scripts/updateSwaggerVersion.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const fs = require('fs'); 13 | const path = require('path'); 14 | 15 | /** 16 | * 17 | */ 18 | async function main () { 19 | const newVersion = process.env.npm_package_version; 20 | const templateRegistryApiPath = path.resolve(__dirname, '../template-registry-api.json'); 21 | const templateRegistryApi = require(templateRegistryApiPath); 22 | templateRegistryApi.info.version = newVersion; 23 | fs.writeFileSync(templateRegistryApiPath, JSON.stringify(templateRegistryApi, null, 2)); 24 | } 25 | 26 | main() 27 | .catch(console.error); 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for choosing to contribute! 4 | 5 | The following are a set of guidelines to follow when contributing to this project. 6 | 7 | ## Code Of Conduct 8 | 9 | This project adheres to the Adobe [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 10 | 11 | ## Contributor License Agreement 12 | 13 | All third-party contributions to this project must be accompanied by a signed contributor license agreement. This gives Adobe permission to redistribute your contributions as part of the project. [Sign our CLA](http://opensource.adobe.com/cla.html). You only need to submit an Adobe CLA one time, so if you have submitted one previously, you are good to go! 14 | 15 | ## Code Reviews 16 | 17 | All submissions should come in the form of pull requests and need to be reviewed by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) for more information on sending pull requests. 18 | 19 | Lastly, please follow the [pull request template](./.github/PULL_REQUEST_TEMPLATE.md) when submitting a pull request! -------------------------------------------------------------------------------- /test/metrics.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const metrics = require('../actions/metrics'); 13 | const metricsLib = require('@adobe/aio-metrics-client'); 14 | jest.mock('@adobe/aio-metrics-client'); 15 | 16 | describe('metrics', () => { 17 | beforeEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | test('setMetricsUrl with metrics-url in environment', async () => { 22 | metrics.setMetricsUrl('https://test.com', 'fake-metric'); 23 | expect(metricsLib.setMetricsURL).toHaveBeenCalledTimes(1); 24 | }); 25 | 26 | test('setMetricsUrl no metrics-url in environment', async () => { 27 | metrics.setMetricsUrl('test', 'fake-metric'); 28 | expect(metricsLib.setMetricsURL).toHaveBeenCalledTimes(0); 29 | }); 30 | 31 | test('incErrorCounterMetrics', async () => { 32 | await metrics.incErrorCounterMetrics('test', 'test', 'test'); 33 | expect(metricsLib.incBatchCounterMultiLabel).toHaveBeenCalledTimes(1); 34 | expect(metricsLib.incBatchCounterMultiLabel).toHaveBeenCalledWith('error_count', 'test', { api: 'test', errorCategory: 'test' }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /db/mongo.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const { MongoClient, Collection } = require('mongodb'); 13 | 14 | let db = null; 15 | 16 | /** 17 | * Connects to MongoDB. 18 | * @param {object} params MongoDB connection parameters 19 | * @returns {Promise} 20 | */ 21 | async function connectToMongoDB (params) { 22 | try { 23 | const dbName = params.MONGODB_NAME; 24 | const url = params.MONGODB_URI; 25 | const client = new MongoClient(url); 26 | await client.connect(); 27 | console.log('Connected to MongoDB.'); 28 | db = client.db(dbName); 29 | } catch (error) { 30 | console.error('Error connecting to MongoDB:', error); 31 | throw new Error('Error connecting to MongoDB'); 32 | } 33 | } 34 | 35 | /** 36 | * Returns a collection to interact with from MongoDB. 37 | * @param {object} params MongoDB connection parameters 38 | * @param {string} collectionName Name of the collection 39 | * @returns {Promise} A collection 40 | */ 41 | async function mongoConnection (params, collectionName) { 42 | if (!db || db === null) { 43 | await connectToMongoDB(params); 44 | } 45 | return db.collection(collectionName); // returns a collection 46 | } 47 | 48 | module.exports = { mongoConnection }; 49 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Motivation and Context 15 | 16 | 17 | 18 | ## How Has This Been Tested? 19 | 20 | 21 | 22 | 23 | 24 | ## Screenshots (if appropriate): 25 | 26 | ## Types of changes 27 | 28 | 29 | 30 | - [ ] Bug fix (non-breaking change which fixes an issue) 31 | - [ ] New feature (non-breaking change which adds functionality) 32 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 33 | 34 | ## Checklist: 35 | 36 | 37 | 38 | 39 | - [ ] I have signed the [Adobe Open Source CLA](http://opensource.adobe.com/cla.html). 40 | - [ ] My code follows the code style of this project. 41 | - [ ] My change requires a change to the documentation. 42 | - [ ] I have updated the documentation accordingly. 43 | - [ ] I have read the **CONTRIBUTING** document. 44 | - [ ] I have added tests to cover my changes. 45 | - [ ] All new and existing tests passed. 46 | -------------------------------------------------------------------------------- /actions/metrics.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const { posix } = require('node:path'); 13 | const { setMetricsURL, incBatchCounterMultiLabel } = require('@adobe/aio-metrics-client'); 14 | const logger = require('@adobe/aio-lib-core-logging')('adp-template-registry-api'); 15 | 16 | /** 17 | * Increment error counter metrics 18 | * @param {string} requester Name of the requester, usually IMS clientId or userId, ex. 'crusher-stage' 19 | * @param {string} api Endpoint to track errors for, ex. 'GET /templates/{templateId}' 20 | * @param {string} errorCategory Error category, ex. '401' 21 | */ 22 | const incErrorCounterMetrics = async (requester, api, errorCategory) => { 23 | await incBatchCounterMultiLabel('error_count', requester, { api, errorCategory }); 24 | }; 25 | 26 | /** 27 | * Sets the metrics URL for metrics recording 28 | * 29 | * @param {object} metricsUrl Metrics URL 30 | * @param {string} metricsKey Metrics key to append to the URL 31 | */ 32 | const setMetricsUrl = (metricsUrl, metricsKey) => { 33 | try { 34 | const url = new URL(metricsUrl); 35 | url.pathname = posix.join(url.pathname, metricsKey); 36 | setMetricsURL(url.href); 37 | } catch (ex) { 38 | logger.info('Creating metrics url failed : ', metricsUrl); 39 | logger.error(ex); 40 | } 41 | }; 42 | 43 | module.exports = { 44 | setMetricsUrl, 45 | incErrorCounterMetrics 46 | }; 47 | -------------------------------------------------------------------------------- /test/fixtures/list/response.only-app-builder-templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "_links": { 12 | "review": { 13 | "description": "A link to the \"Template Review Request\" Github issue.", 14 | "href": "https://github.com/adobe/aio-templates/issues/100" 15 | }, 16 | "self": { 17 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-4" 18 | } 19 | }, 20 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 21 | "name": "@author/app-builder-template-4", 22 | "status": "InVerification", 23 | "links": { 24 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 25 | "github": "https://github.com/author/app-builder-template-4" 26 | } 27 | }, 28 | { 29 | "_links": { 30 | "review": { 31 | "description": "A link to the \"Template Review Request\" Github issue.", 32 | "href": "https://github.com/adobe/aio-templates/issues/100" 33 | }, 34 | "self": { 35 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-5" 36 | } 37 | }, 38 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 39 | "name": "@author/app-builder-template-5", 40 | "status": "Rejected", 41 | "links": { 42 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 43 | "github": "https://github.com/author/app-builder-template-5" 44 | } 45 | } 46 | ] 47 | } 48 | } -------------------------------------------------------------------------------- /test/fixtures/list/response.filter-value-none-runtime.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?runtime=" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "_links": { 12 | "self": { 13 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-4" 14 | }, 15 | "review": { 16 | "href": "https://github.com/adobe/aio-templates/issues/100", 17 | "description": "A link to the \"Template Review Request\" Github issue." 18 | } 19 | }, 20 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 21 | "name": "@author/app-builder-template-4", 22 | "status": "InVerification", 23 | "links": { 24 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 25 | "github": "https://github.com/author/app-builder-template-4" 26 | } 27 | }, 28 | { 29 | "_links": { 30 | "self": { 31 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-5" 32 | }, 33 | "review": { 34 | "href": "https://github.com/adobe/aio-templates/issues/100", 35 | "description": "A link to the \"Template Review Request\" Github issue." 36 | } 37 | }, 38 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 39 | "name": "@author/app-builder-template-5", 40 | "status": "Rejected", 41 | "links": { 42 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 43 | "github": "https://github.com/author/app-builder-template-5" 44 | } 45 | } 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/fixtures/list/response.not-approved.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?statuses=!Approved" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "_links": { 12 | "review": { 13 | "description": "A link to the \"Template Review Request\" Github issue.", 14 | "href": "https://github.com/adobe/aio-templates/issues/100" 15 | }, 16 | "self": { 17 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-4" 18 | } 19 | }, 20 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 21 | "name": "@author/app-builder-template-4", 22 | "status": "InVerification", 23 | "links": { 24 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 25 | "github": "https://github.com/author/app-builder-template-4" 26 | } 27 | }, 28 | { 29 | "_links": { 30 | "review": { 31 | "description": "A link to the \"Template Review Request\" Github issue.", 32 | "href": "https://github.com/adobe/aio-templates/issues/100" 33 | }, 34 | "self": { 35 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-5" 36 | } 37 | }, 38 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 39 | "name": "@author/app-builder-template-5", 40 | "status": "Rejected", 41 | "links": { 42 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 43 | "github": "https://github.com/author/app-builder-template-5" 44 | } 45 | } 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/fixtures/list/registry2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 4 | "author": "Adobe Inc.", 5 | "name": "@author/app-builder-template-1", 6 | "publishDate": "2022-05-01T03:50:39.658Z", 7 | "status": "Approved", 8 | "links": { 9 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1", 10 | "github": "https://github.com/author/app-builder-template-1" 11 | } 12 | }, 13 | { 14 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 15 | "author": "Adobe Inc.", 16 | "name": "@author/app-builder-template-4", 17 | "publishDate": "2020-05-01T03:50:39.658Z", 18 | "adobeRecommended": true, 19 | "status": "Approved", 20 | "links": { 21 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 22 | "github": "https://github.com/author/app-builder-template-4" 23 | } 24 | }, 25 | { 26 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 27 | "author": "Adobe Inc.", 28 | "name": "@author/app-builder-template-3", 29 | "publishDate": "2023-05-01T03:50:39.658Z", 30 | "adobeRecommended": true, 31 | "status": "Approved", 32 | "links": { 33 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3", 34 | "github": "https://github.com/author/app-builder-template-3" 35 | } 36 | }, 37 | { 38 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 39 | "name": "@author/app-builder-template-2", 40 | "status": "Rejected", 41 | "links": { 42 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2", 43 | "github": "https://github.com/author/app-builder-template-2" 44 | } 45 | }, 46 | { 47 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 48 | "name": "@author/app-builder-template-6", 49 | "status": "Rejected", 50 | "links": { 51 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-6", 52 | "github": "https://github.com/author/app-builder-template-6" 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /test/fixtures/list/registry-bad-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 4 | "author": "Adobe Inc.", 5 | "name": "@author/app-builder-template-1", 6 | "publishDate": "2022-05-01T03:50:39.658Z", 7 | "status": "Approved", 8 | "links": { 9 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1", 10 | "github": "https://github.com/author/app-builder-template-1" 11 | }, 12 | "fake": "param" 13 | }, 14 | { 15 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 16 | "author": "Adobe Inc.", 17 | "name": "@author/app-builder-template-4", 18 | "publishDate": "2020-05-01T03:50:39.658Z", 19 | "adobeRecommended": true, 20 | "status": "Approved", 21 | "links": { 22 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 23 | "github": "https://github.com/author/app-builder-template-4" 24 | } 25 | }, 26 | { 27 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 28 | "author": "Adobe Inc.", 29 | "name": "@author/app-builder-template-3", 30 | "publishDate": "2023-05-01T03:50:39.658Z", 31 | "adobeRecommended": true, 32 | "status": "Approved", 33 | "links": { 34 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3", 35 | "github": "https://github.com/author/app-builder-template-3" 36 | } 37 | }, 38 | { 39 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 40 | "name": "@author/app-builder-template-2", 41 | "status": "Rejected", 42 | "links": { 43 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2", 44 | "github": "https://github.com/author/app-builder-template-2" 45 | } 46 | }, 47 | { 48 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 49 | "name": "@author/app-builder-template-6", 50 | "status": "Rejected", 51 | "links": { 52 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-6", 53 | "github": "https://github.com/author/app-builder-template-6" 54 | } 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 14 | 15 | 16 | # Template Registry API 17 | 18 | ## OpenAPI Schema 19 | Template Registry API follows OpenAPI 2.0 Specification. 20 | 21 | See [Template Registry API Schema](https://opensource.adobe.com/adp-template-registry-api/) for more details on this RESTful API. 22 | 23 | # Getting started 24 | 25 | ## Installation 26 | 27 | ```bash 28 | $ git clone git@github.com:adobe/adp-template-registry-api.git 29 | $ cd adp-template-registry-api 30 | $ npm install 31 | ``` 32 | 33 | ## Populate .env 34 | 35 | Copy the root `.env.example` to a new `.env` file and fill out all the fields 36 | 37 | ## Deploy service and APIs 38 | 39 | ```bash 40 | $ aio app deploy 41 | $ aio runtime api create /v1 --config-file=template-registry-api.json 42 | ``` 43 | 44 | The output of the second command should provide you with the base URL for calling your service 45 | 46 | > Note: It can take up to five minutes for the API configuration to be fully setup and ready for use 47 | 48 | ## Run Unit Tests 49 | 50 | `npm test` 51 | 52 | ## Functional Testing 53 | 54 | To functionally test the API, developers can import the Template Registry collection [template-registry-collection.json](https://github.com/adobe/adp-template-registry-api/blob/main/template-registry-collection.json) into [Insomnia](https://insomnia.rest/) (or any API tooling forked from Insomnia). 55 | 56 | ## Contributing 57 | 58 | Contributions are welcomed! Read the [Contributing Guide](CONTRIBUTING.md) for more information. 59 | 60 | ## Licensing 61 | 62 | This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information. 63 | -------------------------------------------------------------------------------- /test/mongo.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const { MongoClient } = require('mongodb'); 13 | const { mongoConnection } = require('../db/mongo'); 14 | 15 | describe('mongoConnection', () => { 16 | let clientConnectSpy; 17 | let clientDbMock; 18 | 19 | const params = { 20 | MONGODB_URI: 'mongodb://localhost:27017', 21 | MONGODB_NAME: 'testDb' 22 | }; 23 | 24 | beforeAll(() => { 25 | clientConnectSpy = jest.spyOn(MongoClient.prototype, 'connect').mockResolvedValue(); 26 | clientDbMock = jest.spyOn(MongoClient.prototype, 'db').mockReturnValue({ 27 | collection: jest.fn().mockReturnValue({ 28 | insertOne: jest.fn(), 29 | find: jest.fn() 30 | }) 31 | }); 32 | }); 33 | 34 | afterEach(() => { 35 | jest.clearAllMocks(); 36 | }); 37 | 38 | afterAll(() => { 39 | clientConnectSpy.mockRestore(); 40 | clientDbMock.mockRestore(); 41 | jest.clearAllMocks(); 42 | }); 43 | 44 | it('should return connection error', async () => { 45 | await expect(mongoConnection({}, 'existingCollection')).rejects.toThrow('Error connecting to MongoDB'); 46 | }); 47 | 48 | it('should connect to MongoDB and return a collection', async () => { 49 | const collectionName = 'testCollection'; 50 | await mongoConnection(params, collectionName); 51 | 52 | expect(clientConnectSpy).toHaveBeenCalled(); 53 | expect(clientDbMock).toHaveBeenCalled(); 54 | expect(clientDbMock().collection).toHaveBeenCalledWith(collectionName); 55 | }); 56 | 57 | it('should return the existing collection if already connected', async () => { 58 | await mongoConnection(params, 'existingCollection'); 59 | await mongoConnection(params, 'existingCollection'); 60 | 61 | expect(clientConnectSpy).not.toHaveBeenCalled(); 62 | expect(clientDbMock).not.toHaveBeenCalled(); 63 | expect(clientDbMock().collection).toHaveBeenCalledWith('existingCollection'); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/fixtures/list/response.filter-value-any-events.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?events=*" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "_links": { 12 | "self": { 13 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-2" 14 | } 15 | }, 16 | "adobeRecommended": true, 17 | "author": "Adobe Inc.", 18 | "categories": [ 19 | "events" 20 | ], 21 | "description": "A template for testing purposes", 22 | "extensions": [ 23 | { 24 | "extensionPointId": "dx/asset-compute/worker/1" 25 | } 26 | ], 27 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 28 | "keywords": [ 29 | "aio", 30 | "adobeio", 31 | "app", 32 | "templates", 33 | "aio-app-builder-template" 34 | ], 35 | "latestVersion": "1.0.1", 36 | "links": { 37 | "github": "https://github.com/author/app-builder-template-2", 38 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2" 39 | }, 40 | "name": "@author/app-builder-template-2", 41 | "publishDate": "2022-05-01T03:50:39.658Z", 42 | "apis": [ 43 | { 44 | "code": "Events", 45 | "hooks": [ 46 | { 47 | "postdeploy": "some command" 48 | } 49 | ] 50 | }, 51 | { 52 | "code": "Mesh", 53 | "endpoints": [ 54 | { 55 | "my-action": "https://some-action.com/action" 56 | } 57 | ] 58 | } 59 | ], 60 | "status": "Approved", 61 | "runtime": true, 62 | "event": {} 63 | } 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template-registry-api", 3 | "version": "1.0.1", 4 | "dependencies": { 5 | "@adobe/aio-lib-console": "^5.1.0", 6 | "@adobe/aio-lib-ims": "^7.0.1", 7 | "@adobe/aio-metrics-client": "^0.4.0", 8 | "@adobe/aio-sdk": "^5.0.1", 9 | "@octokit/rest": "^20.0.2", 10 | "axios": "^0.27.2", 11 | "lodash.orderby": "^4.6.0", 12 | "mongodb": "^6.5.0", 13 | "openapi-enforcer": "^1.23.0" 14 | }, 15 | "devDependencies": { 16 | "@adobe/eslint-config-aio-lib-config": "^4.0.0", 17 | "@openwhisk/wskdebug": "^1.3.0", 18 | "@redocly/cli": "^1.10.6", 19 | "@types/jest": "^29.5.12", 20 | "eslint": "^8.57.0", 21 | "eslint-config-standard": "^17.1.0", 22 | "eslint-plugin-import": "^2.29.1", 23 | "eslint-plugin-jest": "^27.9.0", 24 | "eslint-plugin-jsdoc": "^48.2.3", 25 | "eslint-plugin-n": "^15.7.0", 26 | "eslint-plugin-node": "^11.1.0", 27 | "eslint-plugin-promise": "^6.1.1", 28 | "jest": "^29.7.0", 29 | "jest-junit": "^16.0.0", 30 | "nock": "^13.5.4", 31 | "node-fetch": "^2.6.7", 32 | "np": "^10.0.5" 33 | }, 34 | "scripts": { 35 | "test": "npm run lint:check && npm run unit-tests", 36 | "test:smoke": "jest --ci --testPathPattern=smoke --collectCoverage=false --runInBand --detectOpenHandles --setupFilesAfterEnv=./test/jest.setup.smoke.js", 37 | "unit-tests": "jest --ci --testPathIgnorePatterns=smoke", 38 | "lint:check": "eslint --ext .js .", 39 | "lint:fix": "eslint --ext .js --fix .", 40 | "e2e": "jest --detectOpenHandles --config ./e2e/jest.config.js", 41 | "api:spec:lint": "redocly lint template-registry-api.json", 42 | "api:spec:generate": "redocly build-docs template-registry-api.json --output index.html", 43 | "version": "node scripts/updateSwaggerVersion.js && npm run api:spec:generate", 44 | "release": "np --no-publish" 45 | }, 46 | "engines": { 47 | "node": "^22" 48 | }, 49 | "jest": { 50 | "rootDir": ".", 51 | "testEnvironment": "node", 52 | "verbose": true, 53 | "setupFilesAfterEnv": [ 54 | "./test/jest.setup.js" 55 | ], 56 | "testPathIgnorePatterns": [ 57 | "/node_modules/" 58 | ], 59 | "collectCoverage": true, 60 | "collectCoverageFrom": [], 61 | "reporters": [ 62 | "default", 63 | "jest-junit" 64 | ], 65 | "coverageThreshold": { 66 | "global": { 67 | "branches": 100, 68 | "lines": 100, 69 | "statements": 100, 70 | "functions": 100 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/fixtures/list/response.complex-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?names=@author/app-builder-template-2&categories=|events,ui,action&apis=Events&adobeRecommended=true" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 12 | "author": "Adobe Inc.", 13 | "name": "@author/app-builder-template-2", 14 | "description": "A template for testing purposes", 15 | "latestVersion": "1.0.1", 16 | "publishDate": "2022-05-01T03:50:39.658Z", 17 | "apis": [ 18 | { 19 | "code": "Events", 20 | "hooks": [ 21 | { 22 | "postdeploy": "some command" 23 | } 24 | ] 25 | }, 26 | { 27 | "code": "Mesh", 28 | "endpoints": [ 29 | { 30 | "my-action": "https://some-action.com/action" 31 | } 32 | ] 33 | } 34 | ], 35 | "adobeRecommended": true, 36 | "keywords": [ 37 | "aio", 38 | "adobeio", 39 | "app", 40 | "templates", 41 | "aio-app-builder-template" 42 | ], 43 | "status": "Approved", 44 | "links": { 45 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2", 46 | "github": "https://github.com/author/app-builder-template-2" 47 | }, 48 | "extensions": [ 49 | { 50 | "extensionPointId": "dx/asset-compute/worker/1" 51 | } 52 | ], 53 | "categories": [ 54 | "events" 55 | ], 56 | "runtime": true, 57 | "event": {}, 58 | "_links": { 59 | "self": { 60 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-2" 61 | } 62 | } 63 | } 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /actions/acrs.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | /** 13 | * Calls ACRS to fetch all requests for the user in the org. Collects all the app ids with a pending status 14 | * in a set and returns that in the response. In case of an error, the error is caught and logged and an empty 15 | * Set is returned 16 | * @param {string} userToken Token belonging to the logged in user 17 | * @param {string} orgId IMS org id for the org to be checked 18 | * @param {string} env Service environment - stage or prod 19 | * @param {string} apiKey API key of the service 20 | * @param {object} logger logger instance 21 | * @returns {Promise>} a Set of app ids which have a pending request 22 | */ 23 | async function fetchAppIdsWithPendingRequests (userToken, orgId, env, apiKey, logger) { 24 | try { 25 | const url = `https://acrs${env === 'stage' ? '-stage' : ''}.adobe.io/organization/${orgId}/app_auth_requests?userAccountId=self`; 26 | logger.debug(`Fetching pending requests from acrs url:${url}`); 27 | const headers = { 28 | authorization: 'Bearer ' + userToken, 29 | 'x-api-key': apiKey 30 | }; 31 | const response = await fetch(url, { headers }); 32 | 33 | if (!response.ok) { 34 | const responseText = await response.text(); 35 | logger.error(`Error response from acrs while fetching pending requests for org ${orgId}. Response: ${responseText}`); 36 | throw new Error(`Failed to fetch pending requests from ACRS for org ${orgId}.`); 37 | } 38 | 39 | const accessRequests = await response.json(); 40 | 41 | const pendingAppIds = new Set(); 42 | accessRequests.forEach(accessRequest => { 43 | if (accessRequest.status !== 'PENDING') { 44 | return; 45 | } 46 | 47 | accessRequest.applicationIds.forEach(applicationId => pendingAppIds.add(applicationId)); 48 | }); 49 | return pendingAppIds; 50 | } catch (error) { 51 | logger.error(`Error while fetching pending requests from acrs for org ${orgId}.`, error); 52 | throw new Error(`Failed to fetch pending requests from ACRS for org ${orgId}.`); 53 | } 54 | } 55 | 56 | module.exports = { 57 | fetchAppIdsWithPendingRequests 58 | }; 59 | -------------------------------------------------------------------------------- /test/fixtures/list/response.simple-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?categories=action,ui" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 12 | "author": "Adobe Inc.", 13 | "name": "@author/app-builder-template-1", 14 | "description": "A template for testing purposes", 15 | "latestVersion": "1.0.0", 16 | "publishDate": "2022-05-01T03:50:39.658Z", 17 | "apis": [ 18 | { 19 | "code": "AnalyticsSDK", 20 | "credentials": "OAuth" 21 | }, 22 | { 23 | "code": "CampaignStandard" 24 | }, 25 | { 26 | "code": "Runtime" 27 | }, 28 | { 29 | "code": "Events", 30 | "hooks": [ 31 | { 32 | "postdeploy": "some command" 33 | } 34 | ] 35 | }, 36 | { 37 | "code": "Mesh", 38 | "endpoints": [ 39 | { 40 | "my-action": "https://some-action.com/action" 41 | } 42 | ] 43 | } 44 | ], 45 | "adobeRecommended": false, 46 | "keywords": [ 47 | "aio", 48 | "adobeio", 49 | "app", 50 | "templates", 51 | "aio-app-builder-template" 52 | ], 53 | "status": "Approved", 54 | "links": { 55 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1", 56 | "github": "https://github.com/author/app-builder-template-1" 57 | }, 58 | "extensions": [ 59 | { 60 | "extensionPointId": "dx/excshell/1" 61 | } 62 | ], 63 | "categories": [ 64 | "action", 65 | "ui" 66 | ], 67 | "runtime": true, 68 | "_links": { 69 | "self": { 70 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-1" 71 | } 72 | } 73 | } 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/fixtures/list/response.with-dev-console-templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "_links": { 12 | "review": { 13 | "description": "A link to the \"Template Review Request\" Github issue.", 14 | "href": "https://github.com/adobe/aio-templates/issues/100" 15 | }, 16 | "self": { 17 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-4" 18 | } 19 | }, 20 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 21 | "name": "@author/app-builder-template-4", 22 | "status": "InVerification", 23 | "links": { 24 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 25 | "github": "https://github.com/author/app-builder-template-4" 26 | } 27 | }, 28 | { 29 | "_links": { 30 | "review": { 31 | "description": "A link to the \"Template Review Request\" Github issue.", 32 | "href": "https://github.com/adobe/aio-templates/issues/100" 33 | }, 34 | "self": { 35 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-5" 36 | } 37 | }, 38 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 39 | "name": "@author/app-builder-template-5", 40 | "status": "Rejected", 41 | "links": { 42 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 43 | "github": "https://github.com/author/app-builder-template-5" 44 | } 45 | }, 46 | { 47 | "_links": { 48 | "review": { 49 | "description": "A link to the \"Template Review Request\" Github issue.", 50 | "href": "https://github.com/adobe/aio-templates/issues/100" 51 | }, 52 | "self": { 53 | "href": "https://template-registry-api.tbd/apis/v1/templates/d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48" 54 | } 55 | }, 56 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 57 | "name": "app-builder-template-6", 58 | "status": "Rejected", 59 | "links": { 60 | "consoleProject": "https://developer-stage.adobe.com/console/projects/1234" 61 | } 62 | } 63 | ] 64 | } 65 | } -------------------------------------------------------------------------------- /.github/workflows/smoke-tests.yml: -------------------------------------------------------------------------------- 1 | name: Smoke Tests 2 | 3 | defaults: 4 | run: 5 | shell: bash 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | environment: 11 | description: 'Deployment environment' 12 | required: true 13 | type: choice 14 | options: 15 | - Stage 16 | - Production 17 | default: 'Stage' 18 | schedule: 19 | - cron: '*/5 * * * *' # Run prod every 5 minutes 20 | - cron: '*/60 * * * *' # Run stage every hour 21 | 22 | jobs: 23 | smoke: 24 | environment: ${{ inputs.environment || github.event.schedule == '*/5 * * * *' && 'Production' || 'Stage' }} 25 | name: Smoke test ${{ inputs.environment || github.event.schedule == '*/5 * * * *' && 'Production' || 'Stage' }} 26 | # we would like to avoid running this workflow in forked repos 27 | if: github.repository == 'adobe/adp-template-registry-api' 28 | runs-on: ${{ matrix.os }} 29 | strategy: 30 | max-parallel: 1 31 | matrix: 32 | node-version: ['20'] 33 | os: [ubuntu-latest] 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | - name: Use Node.js ${{ matrix.node-version }} 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: ${{ matrix.node-version }} 41 | - name: Clear npm cache 42 | run: npm cache clean --force 43 | - name: Install dependencies 44 | uses: nick-fields/retry@v3 45 | with: 46 | max_attempts: 3 47 | timeout_minutes: 5 48 | retry_wait_seconds: 30 49 | command: npm i --package-lock --package-lock-only && npm ci 50 | - name: Run smoke tests 51 | uses: nick-fields/retry@v3 52 | with: 53 | max_attempts: 3 54 | timeout_minutes: 5 55 | retry_wait_seconds: 60 56 | command: npm run test:smoke 57 | env: 58 | AIO_RUNTIME_APIHOST: ${{ secrets.AIO_RUNTIME_APIHOST }} 59 | IMS_CLIENT_ID: ${{ secrets.IMS_CLIENT_ID }} 60 | IMS_CLIENT_SECRET: ${{ secrets.IMS_CLIENT_SECRET }} 61 | IMS_AUTH_CODE: ${{ secrets.IMS_AUTH_CODE }} 62 | IMS_SCOPES: ${{ secrets.IMS_SCOPES }} 63 | TEMPLATE_REGISTRY_API_URL: ${{ secrets.TEMPLATE_REGISTRY_API_URL }} 64 | - id: slacknotification 65 | name: Slack Notification 66 | if: ${{ failure() }} 67 | uses: rtCamp/action-slack-notify@v2 68 | env: 69 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 70 | SLACK_TITLE: ${{ inputs.environment || github.event.schedule == '*/60 * * * *' && 'Stage ' || '' }}Template Registry Smoke Tests Failed 71 | SLACK_MESSAGE: 'Node Version: ${{ matrix.node-version }}\n Runbook: https://git.corp.adobe.com/CNA/runbooks/tree/main/runbooks/template-registry-smoke-tests-failure.md' 72 | SLACK_COLOR: ${{ job.status == 'success' && 'good' || job.status == 'cancelled' && '#808080' || 'danger' }} 73 | ENABLE_ESCAPES: true 74 | -------------------------------------------------------------------------------- /.github/workflows/deploy-api.yml: -------------------------------------------------------------------------------- 1 | name: Deploy API 2 | 3 | defaults: 4 | run: 5 | shell: bash 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | environment: 11 | description: 'Deployment environment' 12 | required: true 13 | type: choice 14 | options: 15 | - Stage 16 | - Production 17 | default: 'Stage' 18 | release: # Prod 19 | types: [released] 20 | push: # Stage 21 | branches: 22 | - main 23 | 24 | jobs: 25 | deploy: 26 | environment: ${{ inputs.environment || github.event_name == 'release' && 'Production' || github.event_name == 'push' && 'Stage' }} 27 | name: Deploy to ${{ inputs.environment || github.event_name == 'release' && 'Production' || github.event_name == 'push' && 'Stage' }} 28 | # we would like to avoid running this workflow in forked repos 29 | if: github.repository == 'adobe/adp-template-registry-api' 30 | runs-on: ${{ matrix.os }} 31 | strategy: 32 | max-parallel: 1 33 | matrix: 34 | node-version: ['20'] 35 | os: [ubuntu-latest] 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | - name: Use Node.js ${{ matrix.node-version }} 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | - name: Install dependencies 44 | run: npm i --package-lock --package-lock-only && npm ci 45 | - name: Setup Adobe AIO CLI 46 | uses: adobe/aio-cli-setup-action@1.3.0 47 | with: 48 | os: ${{ matrix.os }} 49 | version: 10.x.x 50 | - name: Generate Template Registry APIs 51 | env: 52 | AIO_runtime_auth: ${{ secrets.AIO_RUNTIME_AUTH }} 53 | AIO_runtime_namespace: ${{ secrets.AIO_RUNTIME_NAMESPACE }} 54 | AIO_runtime_apihost: ${{ secrets.AIO_RUNTIME_APIHOST }} 55 | run: aio runtime api create --config-file=template-registry-api.json 56 | - name: Deploy 57 | env: 58 | AIO_runtime_auth: ${{ secrets.AIO_RUNTIME_AUTH }} 59 | AIO_runtime_namespace: ${{ secrets.AIO_RUNTIME_NAMESPACE }} 60 | AIO_runtime_apihost: ${{ secrets.AIO_RUNTIME_APIHOST }} 61 | TEMPLATE_REGISTRY_ORG: ${{ secrets.TEMPLATE_REGISTRY_ORG }} 62 | TEMPLATE_REGISTRY_REPOSITORY: ${{ secrets.TEMPLATE_REGISTRY_REPOSITORY }} 63 | ACCESS_TOKEN_GITHUB: ${{ secrets.ACCESS_TOKEN_GITHUB }} 64 | IMS_URL: ${{ secrets.IMS_URL }} 65 | IMS_CLIENT_ID: ${{ secrets.IMS_CLIENT_ID }} 66 | IMS_CLIENT_SECRET: ${{ secrets.IMS_CLIENT_SECRET }} 67 | IMS_AUTH_CODE: ${{ secrets.IMS_AUTH_CODE }} 68 | IMS_SCOPES: ${{ secrets.IMS_SCOPES }} 69 | ADMIN_IMS_ORGANIZATIONS: ${{ secrets.ADMIN_IMS_ORGANIZATIONS }} 70 | TEMPLATE_REGISTRY_API_URL: ${{ secrets.TEMPLATE_REGISTRY_API_URL }} 71 | MONGODB_NAME: ${{ secrets.MONGODB_NAME }} 72 | MONGODB_URI: ${{ secrets.MONGODB_URI }} 73 | METRICS_URL: ${{ secrets.METRICS_URL }} 74 | run: aio app deploy 75 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /test/fixtures/list/response.filter-value-none-extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?extensions=" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "_links": { 12 | "self": { 13 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-3" 14 | } 15 | }, 16 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 17 | "author": "Adobe Inc.", 18 | "name": "@author/app-builder-template-3", 19 | "description": "A template for testing purposes", 20 | "latestVersion": "1.0.1", 21 | "publishDate": "2022-05-01T03:50:39.658Z", 22 | "apis": [ 23 | { 24 | "code": "CampaignStandard" 25 | } 26 | ], 27 | "adobeRecommended": true, 28 | "keywords": [ 29 | "aio", 30 | "adobeio", 31 | "app", 32 | "templates", 33 | "aio-app-builder-template" 34 | ], 35 | "status": "Approved", 36 | "links": { 37 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3", 38 | "github": "https://github.com/author/app-builder-template-3" 39 | }, 40 | "categories": [ 41 | "ui" 42 | ], 43 | "runtime": false 44 | }, 45 | { 46 | "_links": { 47 | "self": { 48 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-4" 49 | }, 50 | "review": { 51 | "href": "https://github.com/adobe/aio-templates/issues/100", 52 | "description": "A link to the \"Template Review Request\" Github issue." 53 | } 54 | }, 55 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 56 | "name": "@author/app-builder-template-4", 57 | "status": "InVerification", 58 | "links": { 59 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 60 | "github": "https://github.com/author/app-builder-template-4" 61 | } 62 | }, 63 | { 64 | "_links": { 65 | "self": { 66 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-5" 67 | }, 68 | "review": { 69 | "href": "https://github.com/adobe/aio-templates/issues/100", 70 | "description": "A link to the \"Template Review Request\" Github issue." 71 | } 72 | }, 73 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 74 | "name": "@author/app-builder-template-5", 75 | "status": "Rejected", 76 | "links": { 77 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 78 | "github": "https://github.com/author/app-builder-template-5" 79 | } 80 | } 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/fixtures/list/response.simple-filter.exclusion-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?categories=ui,!helper-template" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 12 | "author": "Adobe Inc.", 13 | "name": "@author/app-builder-template-1", 14 | "description": "A template for testing purposes", 15 | "latestVersion": "1.0.0", 16 | "publishDate": "2022-05-01T03:50:39.658Z", 17 | "apis": [ 18 | { 19 | "code": "AnalyticsSDK", 20 | "credentials": "OAuth" 21 | }, 22 | { 23 | "code": "CampaignStandard" 24 | }, 25 | { 26 | "code": "Runtime" 27 | }, 28 | { 29 | "code": "Events", 30 | "hooks": [ 31 | { 32 | "postdeploy": "some command" 33 | } 34 | ] 35 | }, 36 | { 37 | "code": "Mesh", 38 | "endpoints": [ 39 | { 40 | "my-action": "https://some-action.com/action" 41 | } 42 | ] 43 | } 44 | ], 45 | "adobeRecommended": false, 46 | "keywords": [ 47 | "aio", 48 | "adobeio", 49 | "app", 50 | "templates", 51 | "aio-app-builder-template" 52 | ], 53 | "status": "Approved", 54 | "links": { 55 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1", 56 | "github": "https://github.com/author/app-builder-template-1" 57 | }, 58 | "extensions": [ 59 | { 60 | "extensionPointId": "dx/excshell/1" 61 | } 62 | ], 63 | "categories": [ 64 | "action", 65 | "ui" 66 | ], 67 | "runtime": true, 68 | "_links": { 69 | "self": { 70 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-1" 71 | } 72 | } 73 | }, 74 | { 75 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 76 | "author": "Adobe Inc.", 77 | "name": "@author/app-builder-template-3", 78 | "description": "A template for testing purposes", 79 | "latestVersion": "1.0.1", 80 | "publishDate": "2022-05-01T03:50:39.658Z", 81 | "apis": [ 82 | { 83 | "code": "CampaignStandard" 84 | } 85 | ], 86 | "adobeRecommended": true, 87 | "keywords": [ 88 | "aio", 89 | "adobeio", 90 | "app", 91 | "templates", 92 | "aio-app-builder-template" 93 | ], 94 | "status": "Approved", 95 | "links": { 96 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3", 97 | "github": "https://github.com/author/app-builder-template-3" 98 | }, 99 | "categories": [ 100 | "ui" 101 | ], 102 | "runtime": false, 103 | "_links": { 104 | "self": { 105 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-3" 106 | } 107 | } 108 | } 109 | ] 110 | } 111 | } -------------------------------------------------------------------------------- /test/fixtures/list/response.orderBy.multiple.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?orderBy=statuses,adobeRecommended,publishDate desc" 7 | } 8 | }, 9 | "items":[ 10 | { 11 | "_links": { 12 | "self": { 13 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-3" 14 | } 15 | }, 16 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 17 | "author": "Adobe Inc.", 18 | "name": "@author/app-builder-template-3", 19 | "publishDate": "2023-05-01T03:50:39.658Z", 20 | "adobeRecommended": true, 21 | "status": "Approved", 22 | "links": { 23 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3", 24 | "github": "https://github.com/author/app-builder-template-3" 25 | } 26 | }, { 27 | "_links": { 28 | "self": { 29 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-4" 30 | } 31 | }, 32 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 33 | "author": "Adobe Inc.", 34 | "name": "@author/app-builder-template-4", 35 | "publishDate": "2020-05-01T03:50:39.658Z", 36 | "adobeRecommended": true, 37 | "status": "Approved", 38 | "links": { 39 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 40 | "github": "https://github.com/author/app-builder-template-4" 41 | } 42 | }, { 43 | "_links": { 44 | "self": { 45 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-1" 46 | } 47 | }, 48 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 49 | "author": "Adobe Inc.", 50 | "name": "@author/app-builder-template-1", 51 | "publishDate": "2022-05-01T03:50:39.658Z", 52 | "status": "Approved", 53 | "links": { 54 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1", 55 | "github": "https://github.com/author/app-builder-template-1" 56 | } 57 | }, { 58 | "_links": { 59 | "self": { 60 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-2" 61 | }, 62 | "review": { 63 | "description": "A link to the \"Template Review Request\" Github issue.", 64 | "href": "https://github.com/adobe/aio-templates/issues/100" 65 | } 66 | }, 67 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 68 | "name": "@author/app-builder-template-2", 69 | "status": "Rejected", 70 | "links": { 71 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2", 72 | "github": "https://github.com/author/app-builder-template-2" 73 | } 74 | }, { 75 | "_links": { 76 | "self": { 77 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-6" 78 | }, 79 | "review": { 80 | "description": "A link to the \"Template Review Request\" Github issue.", 81 | "href": "https://github.com/adobe/aio-templates/issues/100" 82 | } 83 | }, 84 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 85 | "name": "@author/app-builder-template-6", 86 | "status": "Rejected", 87 | "links": { 88 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-6", 89 | "github": "https://github.com/author/app-builder-template-6" 90 | } 91 | } 92 | ] 93 | } 94 | } -------------------------------------------------------------------------------- /test/smoke.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const fetch = require('node-fetch'); 13 | require('dotenv').config(); 14 | const { Ims } = require('@adobe/aio-lib-ims'); 15 | const testConsoleTemplate = require('./fixtures/smoke/template.console.json'); 16 | 17 | let accessToken = ''; 18 | 19 | describe('smoke tests', () => { 20 | beforeAll(async () => { 21 | const ims = new Ims(process.env.AIO_RUNTIME_APIHOST.includes('stage') ? 'stage' : 'prod'); 22 | const { payload } = await ims.getAccessToken( 23 | process.env.IMS_AUTH_CODE, 24 | process.env.IMS_CLIENT_ID, 25 | process.env.IMS_CLIENT_SECRET, 26 | process.env.IMS_SCOPES 27 | ); 28 | accessToken = payload.access_token; 29 | }); 30 | 31 | describe('console template', () => { 32 | let newTemplateId = ''; 33 | 34 | it('should add a new template', async () => { 35 | testConsoleTemplate.name = `${testConsoleTemplate.name}-${Math.random().toString(10).substring(2, 8)}`; // append random 6 numbers to the template name, avoid 409s on errors 36 | const response = await fetch(`${process.env.TEMPLATE_REGISTRY_API_URL}/templates`, { 37 | method: 'POST', 38 | headers: { 39 | 'Content-Type': 'application/json', 40 | Authorization: `Bearer ${accessToken}` 41 | }, 42 | body: JSON.stringify(testConsoleTemplate) 43 | }); 44 | expect(response.status).toBe(200); 45 | const template = await response.json(); 46 | newTemplateId = template.id; 47 | expect(template.name).toBe(testConsoleTemplate.name); 48 | }); 49 | 50 | it('should fetch the new template', async () => { 51 | const response = await fetch(`${process.env.TEMPLATE_REGISTRY_API_URL}/templates/${newTemplateId}`, { 52 | headers: { 53 | 'Content-Type': 'application/json', 54 | Authorization: `Bearer ${accessToken}` 55 | } 56 | }); 57 | expect(response.status).toBe(200); 58 | const template = await response.json(); 59 | expect(template.name).toBe(testConsoleTemplate.name); 60 | }); 61 | 62 | it('should fetch list of templates, should have the new template', async () => { 63 | const response = await fetch(`${process.env.TEMPLATE_REGISTRY_API_URL}/templates`, { 64 | headers: { 65 | 'Content-Type': 'application/json', 66 | Authorization: `Bearer ${accessToken}` 67 | } 68 | }); 69 | expect(response.status).toBe(200); 70 | const { items: templates } = await response.json(); 71 | const newTemplate = templates.find(template => template.id === newTemplateId); 72 | expect(newTemplate).toBeDefined(); 73 | }); 74 | 75 | it('should update the new template', async () => { 76 | const response = await fetch(`${process.env.TEMPLATE_REGISTRY_API_URL}/templates/${newTemplateId}`, { 77 | method: 'PUT', 78 | headers: { 79 | 'Content-Type': 'application/json', 80 | Authorization: `Bearer ${accessToken}` 81 | }, 82 | body: JSON.stringify({ 83 | ...testConsoleTemplate, 84 | updatedBy: 'GitHub Actions', 85 | description: 'new template description updated' 86 | }) 87 | }); 88 | expect(response.status).toBe(200); 89 | 90 | const responseCheckUpdate = await fetch(`${process.env.TEMPLATE_REGISTRY_API_URL}/templates/${newTemplateId}`, { 91 | headers: { 92 | 'Content-Type': 'application/json', 93 | Authorization: `Bearer ${accessToken}` 94 | } 95 | }); 96 | const template = await responseCheckUpdate.json(); 97 | 98 | expect(responseCheckUpdate.status).toBe(200); 99 | expect(template.description).toBe('new template description updated'); 100 | }); 101 | 102 | it('should delete the new template', async () => { 103 | const response = await fetch(`${process.env.TEMPLATE_REGISTRY_API_URL}/templates/${newTemplateId}`, { 104 | method: 'DELETE', 105 | headers: { 106 | 'Content-Type': 'application/json', 107 | Authorization: `Bearer ${accessToken}` 108 | } 109 | }); 110 | expect(response.status).toBe(200); 111 | 112 | const responseCheckDelete = await fetch(`${process.env.TEMPLATE_REGISTRY_API_URL}/templates/${newTemplateId}`, { 113 | headers: { 114 | 'Content-Type': 'application/json', 115 | Authorization: `Bearer ${accessToken}` 116 | } 117 | }); 118 | expect(responseCheckDelete.status).toBe(404); 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /test/acrs.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const { fetchAppIdsWithPendingRequests } = require('../actions/acrs'); 13 | 14 | describe('acrs', () => { 15 | const MOCK_ACRS_RESPONSE = [ 16 | { 17 | applicationIds: ['ContentTaggingSDK1'], 18 | productArrangementCodes: [], 19 | userReason: 'I need access to this or similar application', 20 | status: 'PENDING', 21 | userAccountId: '29F71A96660304330A494229@6e2f199d65cfd5d849402d.e', 22 | createDate: '2024-05-29T05:21:28.000+00:00', 23 | modifyDate: '2024-05-29T05:21:28.000+00:00', 24 | statusDate: null, 25 | orgId: '6CB219EB65CFD5C40A494126@AdobeOrg', 26 | appAuthRequestId: 'e4c0b6f58fc1a9ea018fc2cbce880007' 27 | }, 28 | { 29 | applicationIds: ['ContentTaggingSDK1'], 30 | productArrangementCodes: [], 31 | userReason: 'I need access to this or similar application', 32 | status: 'DENIED', 33 | adminReason: 'Not providing access at this time', 34 | userAccountId: '29F71A96660304330A494229@6e2f199d65cfd5d849402d.e', 35 | createDate: '2024-05-16T06:12:19.000+00:00', 36 | modifyDate: '2024-05-29T04:45:53.000+00:00', 37 | statusDate: '2024-05-29T04:45:53.000+00:00', 38 | orgId: '6CB219EB65CFD5C40A494126@AdobeOrg', 39 | appAuthRequestId: 'e4c0cd538f7dee57018f8007b1050011' 40 | } 41 | ]; 42 | 43 | const mockLogger = { 44 | debug: jest.fn(), 45 | error: jest.fn() 46 | }; 47 | 48 | test('success', async () => { 49 | global.fetch = jest.fn(() => 50 | Promise.resolve({ 51 | json: () => Promise.resolve(MOCK_ACRS_RESPONSE), 52 | ok: true 53 | }) 54 | ); 55 | const orgId = 'orgId'; 56 | const userToken = 'userToken'; 57 | const apiKey = 'api_key'; 58 | 59 | const expectedUrl = `https://acrs-stage.adobe.io/organization/${orgId}/app_auth_requests?userAccountId=self`; 60 | const expectedHeaders = { 61 | 'x-api-key': apiKey, 62 | authorization: 'Bearer ' + userToken 63 | }; 64 | 65 | const result = await fetchAppIdsWithPendingRequests(userToken, orgId, 'stage', apiKey, mockLogger); 66 | 67 | expect(result).toEqual(new Set(['ContentTaggingSDK1'])); 68 | expect(global.fetch).toHaveBeenCalledWith(expectedUrl, { headers: expectedHeaders }); 69 | expect(mockLogger.debug).toHaveBeenCalledWith(`Fetching pending requests from acrs url:${expectedUrl}`); 70 | expect(mockLogger.error).not.toHaveBeenCalled(); 71 | }); 72 | 73 | test('failure when response invalid', async () => { 74 | global.fetch = jest.fn(() => 75 | Promise.resolve({ 76 | text: () => Promise.resolve('Some error'), 77 | ok: false 78 | }) 79 | ); 80 | const orgId = 'orgId'; 81 | const userToken = 'userToken'; 82 | const apiKey = 'api_key'; 83 | 84 | const expectedUrl = `https://acrs-stage.adobe.io/organization/${orgId}/app_auth_requests?userAccountId=self`; 85 | const expectedHeaders = { 86 | 'x-api-key': apiKey, 87 | authorization: 'Bearer ' + userToken 88 | }; 89 | 90 | await expect(async () => await fetchAppIdsWithPendingRequests(userToken, orgId, 'stage', apiKey, mockLogger)).rejects.toThrow(`Failed to fetch pending requests from ACRS for org ${orgId}.`); 91 | 92 | expect(global.fetch).toHaveBeenCalledWith(expectedUrl, { headers: expectedHeaders }); 93 | expect(mockLogger.debug).toHaveBeenCalledWith(`Fetching pending requests from acrs url:${expectedUrl}`); 94 | expect(mockLogger.error).toHaveBeenCalledWith(`Error response from acrs while fetching pending requests for org ${orgId}. Response: Some error`); 95 | }); 96 | 97 | test('failure when throws error', async () => { 98 | const expectedError = new Error('some error'); 99 | global.fetch = jest.fn().mockRejectedValue(expectedError); 100 | const orgId = 'orgId'; 101 | const userToken = 'userToken'; 102 | const apiKey = 'api_key'; 103 | 104 | const expectedUrl = `https://acrs.adobe.io/organization/${orgId}/app_auth_requests?userAccountId=self`; 105 | const expectedHeaders = { 106 | 'x-api-key': apiKey, 107 | authorization: 'Bearer ' + userToken 108 | }; 109 | 110 | await expect(async () => await fetchAppIdsWithPendingRequests(userToken, orgId, 'prod', apiKey, mockLogger)).rejects.toThrow(`Failed to fetch pending requests from ACRS for org ${orgId}.`); 111 | 112 | expect(global.fetch).toHaveBeenCalledWith(expectedUrl, { headers: expectedHeaders }); 113 | expect(mockLogger.debug).toHaveBeenCalledWith(`Fetching pending requests from acrs url:${expectedUrl}`); 114 | expect(mockLogger.error).toHaveBeenCalledWith(`Error while fetching pending requests from acrs for org ${orgId}.`, expectedError); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /app.config.yaml: -------------------------------------------------------------------------------- 1 | application: 2 | actions: actions 3 | runtimeManifest: 4 | packages: 5 | template-registry-api: 6 | version: 1.0 7 | license: Apache-2.0 8 | inputs: 9 | MONGODB_NAME: $MONGODB_NAME 10 | MONGODB_URI: $MONGODB_URI 11 | actions: 12 | templates-post: 13 | function: actions/templates/post/index.js 14 | web: "yes" 15 | runtime: "nodejs:22" 16 | include: 17 | - ["template-registry-api.json"] 18 | inputs: 19 | LOG_LEVEL: debug 20 | IMS_URL: $IMS_URL 21 | IMS_CLIENT_ID: $IMS_CLIENT_ID 22 | IMS_CLIENT_SECRET: $IMS_CLIENT_SECRET 23 | IMS_AUTH_CODE: $IMS_AUTH_CODE 24 | IMS_SCOPES: $IMS_SCOPES 25 | ACCESS_TOKEN_GITHUB: $ACCESS_TOKEN_GITHUB 26 | TEMPLATE_REGISTRY_ORG: $TEMPLATE_REGISTRY_ORG 27 | TEMPLATE_REGISTRY_REPOSITORY: $TEMPLATE_REGISTRY_REPOSITORY 28 | TEMPLATE_REGISTRY_API_URL: $TEMPLATE_REGISTRY_API_URL 29 | METRICS_URL: $METRICS_URL 30 | annotations: 31 | require-adobe-auth: false 32 | require-gw-validation: true 33 | final: true 34 | templates-put: 35 | function: actions/templates/put/index.js 36 | web: "yes" 37 | runtime: "nodejs:22" 38 | include: 39 | - ["template-registry-api.json"] 40 | inputs: 41 | LOG_LEVEL: debug 42 | IMS_URL: $IMS_URL 43 | IMS_CLIENT_ID: $IMS_CLIENT_ID 44 | IMS_CLIENT_SECRET: $IMS_CLIENT_SECRET 45 | IMS_AUTH_CODE: $IMS_AUTH_CODE 46 | IMS_SCOPES: $IMS_SCOPES 47 | ACCESS_TOKEN_GITHUB: $ACCESS_TOKEN_GITHUB 48 | TEMPLATE_REGISTRY_ORG: $TEMPLATE_REGISTRY_ORG 49 | TEMPLATE_REGISTRY_REPOSITORY: $TEMPLATE_REGISTRY_REPOSITORY 50 | TEMPLATE_REGISTRY_API_URL: $TEMPLATE_REGISTRY_API_URL 51 | METRICS_URL: $METRICS_URL 52 | annotations: 53 | require-adobe-auth: false 54 | require-gw-validation: true 55 | final: true 56 | templates-get: 57 | function: actions/templates/get/index.js 58 | web: "yes" 59 | runtime: "nodejs:22" 60 | include: 61 | - ["template-registry-api.json"] 62 | inputs: 63 | LOG_LEVEL: debug 64 | IMS_CLIENT_ID: $IMS_CLIENT_ID 65 | TEMPLATE_REGISTRY_ORG: $TEMPLATE_REGISTRY_ORG 66 | TEMPLATE_REGISTRY_REPOSITORY: $TEMPLATE_REGISTRY_REPOSITORY 67 | TEMPLATE_REGISTRY_API_URL: $TEMPLATE_REGISTRY_API_URL 68 | METRICS_URL: $METRICS_URL 69 | annotations: 70 | require-adobe-auth: false 71 | final: true 72 | templates-delete: 73 | function: actions/templates/delete/index.js 74 | web: "yes" 75 | runtime: "nodejs:22" 76 | include: 77 | - ["template-registry-api.json"] 78 | inputs: 79 | LOG_LEVEL: debug 80 | IMS_URL: $IMS_URL 81 | IMS_CLIENT_ID: $IMS_CLIENT_ID 82 | ADMIN_IMS_ORGANIZATIONS: $ADMIN_IMS_ORGANIZATIONS 83 | ACCESS_TOKEN_GITHUB: $ACCESS_TOKEN_GITHUB 84 | TEMPLATE_REGISTRY_ORG: $TEMPLATE_REGISTRY_ORG 85 | TEMPLATE_REGISTRY_REPOSITORY: $TEMPLATE_REGISTRY_REPOSITORY 86 | TEMPLATE_REGISTRY_API_URL: $TEMPLATE_REGISTRY_API_URL 87 | METRICS_URL: $METRICS_URL 88 | annotations: 89 | require-adobe-auth: false 90 | require-gw-validation: true 91 | final: true 92 | templates-list: 93 | function: actions/templates/list/index.js 94 | web: "yes" 95 | runtime: "nodejs:22" 96 | include: 97 | - ["template-registry-api.json"] 98 | inputs: 99 | LOG_LEVEL: debug 100 | IMS_URL: $IMS_URL 101 | IMS_CLIENT_ID: $IMS_CLIENT_ID 102 | TEMPLATE_REGISTRY_ORG: $TEMPLATE_REGISTRY_ORG 103 | TEMPLATE_REGISTRY_REPOSITORY: $TEMPLATE_REGISTRY_REPOSITORY 104 | TEMPLATE_REGISTRY_API_URL: $TEMPLATE_REGISTRY_API_URL 105 | METRICS_URL: $METRICS_URL 106 | annotations: 107 | require-adobe-auth: false 108 | final: true 109 | templates-install: 110 | function: actions/templates/install/index.js 111 | web: "yes" 112 | runtime: "nodejs:22" 113 | include: 114 | - ["template-registry-api.json"] 115 | inputs: 116 | LOG_LEVEL: debug 117 | IMS_URL: $IMS_URL 118 | IMS_CLIENT_ID: $IMS_CLIENT_ID 119 | ACCESS_TOKEN_GITHUB: $ACCESS_TOKEN_GITHUB 120 | TEMPLATE_REGISTRY_ORG: $TEMPLATE_REGISTRY_ORG 121 | TEMPLATE_REGISTRY_REPOSITORY: $TEMPLATE_REGISTRY_REPOSITORY 122 | TEMPLATE_REGISTRY_API_URL: $TEMPLATE_REGISTRY_API_URL 123 | METRICS_URL: $METRICS_URL 124 | annotations: 125 | require-adobe-auth: false 126 | final: true -------------------------------------------------------------------------------- /actions/ims.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const axios = require('axios').default; 13 | const { Ims, getTokenData } = require('@adobe/aio-lib-ims'); 14 | const { getEnv } = require('./utils'); 15 | 16 | const CACHE_MAX_AGE = 10 * 60 * 1000; // 10 minutes 17 | const credentialCache = { 18 | token: null, 19 | expiration: null 20 | }; 21 | 22 | /** 23 | * Checks that the provided token is a valid IMS access token. 24 | * 25 | * @param {string} accessToken IMS access token 26 | * @param {string} imsUrl IMS host 27 | * @param {string} imsClientId IMS client id 28 | * @returns {Promise} 29 | */ 30 | async function validateAccessToken (accessToken, imsUrl, imsClientId) { 31 | const response = await requestImsResource( 32 | imsUrl + '/ims/validate_token/v1', 33 | accessToken, 34 | { 'X-IMS-ClientId': imsClientId }, 35 | { client_id: imsClientId, type: 'access_token' } 36 | ); 37 | if (!response.valid) { 38 | const error = `Provided IMS access token is invalid. Reason: ${response.reason}`; 39 | throw new Error(error); 40 | } 41 | } 42 | 43 | /** 44 | * Checks if the provided IMS access token belongs to an admin user. 45 | * 46 | * @param {string} accessToken IMS access token 47 | * @param {string} imsUrl IMS host 48 | * @param {Array} adminImsOrganizations IMS organizations related to admin users 49 | * @returns {Promise} true if the user is an admin, false otherwise 50 | */ 51 | async function isAdmin (accessToken, imsUrl, adminImsOrganizations) { 52 | const imsOrganizations = await requestImsResource(imsUrl + '/ims/organizations/v6', accessToken); 53 | let isAdmin = false; 54 | imsOrganizations.forEach(item => { 55 | const imsOrg = item.orgRef.ident + '@' + item.orgRef.authSrc; 56 | if (adminImsOrganizations.includes(imsOrg)) { 57 | isAdmin = true; 58 | } 59 | }); 60 | return isAdmin; 61 | } 62 | 63 | /** 64 | * Checks if the provided IMS access token is a service token and has access to the list of required scopes. 65 | * Checking for "@AdobeService" is the new way to check for a service token, but older service clients won't 66 | * add this to their tokens, so we also check for the "system" scope. 67 | * @param {string} accessToken IMS access token 68 | * @param {Array} requiredScopes Required scopes for the token 69 | * @returns {boolean} If the token is a service token 70 | */ 71 | function isValidServiceToken (accessToken, requiredScopes = []) { 72 | const tokenData = getTokenData(accessToken); 73 | const isServiceToken = tokenData?.user_id?.endsWith('@AdobeService') || tokenData?.scope?.includes('system'); 74 | const hasValidScopes = requiredScopes.every(scope => tokenData?.scope?.includes(scope)); 75 | return isServiceToken && hasValidScopes; 76 | } 77 | 78 | /** 79 | * @param {string} url URL to IMS resource 80 | * @param {string} accessToken IMS access token 81 | * @param {object} headers headers to be set if any 82 | * @param {object} params params to be set if any 83 | * @returns {Promise} 84 | * @private 85 | */ 86 | async function requestImsResource (url, accessToken, headers = {}, params = {}) { 87 | return new Promise((resolve, reject) => { 88 | axios({ 89 | method: 'get', 90 | url, 91 | headers: { 92 | Authorization: `Bearer ${accessToken}`, 93 | ...headers 94 | }, 95 | params 96 | }) 97 | .then(response => { 98 | if (response.status === 200) { 99 | resolve(response.data); 100 | } else { 101 | const error = `Error fetching "${url}". Response code is ${response.status}`; 102 | reject(new Error(error)); 103 | } 104 | }) 105 | .catch(e => { 106 | const error = `Error fetching "${url}". ${e.toString()}`; 107 | reject(new Error(error)); 108 | }); 109 | }); 110 | } 111 | 112 | /** 113 | * Generates an IMS access token using the provided IMS Auth Code. 114 | * @param {string} imsAuthCode - IMS Auth Code 115 | * @param {string} imsClientId - IMS Client ID 116 | * @param {string} imsClientSecret - IMS Client Secret 117 | * @param {string} imsScopes - List of space-separated scopes 118 | * @param {object} logger - Logger object 119 | * @returns {Promise} - IMS access token 120 | */ 121 | async function generateAccessToken (imsAuthCode, imsClientId, imsClientSecret, imsScopes, logger) { 122 | // If the token is not cached or has expired, generate a new one 123 | if (!credentialCache.token || credentialCache.expiration <= Date.now()) { 124 | const ims = new Ims(getEnv(logger)); 125 | const { payload } = await ims.getAccessToken(imsAuthCode, imsClientId, imsClientSecret, imsScopes); 126 | credentialCache.token = payload.access_token; 127 | credentialCache.expiration = Date.now() + CACHE_MAX_AGE; 128 | logger.debug('Generated IMS access token'); 129 | } 130 | 131 | return credentialCache.token; 132 | } 133 | 134 | module.exports = { 135 | validateAccessToken, 136 | isAdmin, 137 | isValidServiceToken, 138 | generateAccessToken 139 | }; 140 | -------------------------------------------------------------------------------- /actions/templateEntitlement.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const consoleLib = require('@adobe/aio-lib-console'); 13 | const { getBearerToken, getHeaderValue, getEnv } = require('./utils'); 14 | const { fetchAppIdsWithPendingRequests } = require('./acrs'); 15 | 16 | /** 17 | * Evaluate entitlements for a set of templates. This function will check if the user is entitled to use the templates. 18 | * @param {Array} templates Array of templates 19 | * @param {object} params Request parameters 20 | * @param {object} logger Logger instance 21 | * @returns {Promise} Array of templates with entitlement information 22 | */ 23 | async function evaluateEntitlements (templates, params, logger) { 24 | const orgId = getHeaderValue(params, 'x-org-id'); 25 | const userToken = getBearerToken(params); 26 | const callerApiKey = getHeaderValue(params, 'x-api-key'); 27 | 28 | if (!orgId || !templates || templates.length === 0) { 29 | logger.debug('No org id or templates specified. Skipping entitlement check.'); 30 | return templates; 31 | } 32 | 33 | if (!userToken) { 34 | throw new Error('Invalid user token or templates'); 35 | } 36 | 37 | logger.debug(`Evaluating entitlements for ${templates.length} templates, orgId: ${orgId}`); 38 | 39 | const env = getEnv(logger); 40 | const consoleClient = await consoleLib.init(userToken, params.IMS_CLIENT_ID, env); 41 | 42 | const sdkCodesSet = new Set(templates.flatMap((template) => template.apis.map((api) => api.code))); 43 | const sdkCodes = Array.from(sdkCodesSet).join(','); 44 | 45 | logger.debug(`Retrieving services for org ${orgId} with sdkCodes: ${sdkCodes}`); 46 | 47 | const orgServicesResult = await consoleClient.getServicesForOrgV2(orgId, sdkCodes); 48 | 49 | if (orgServicesResult.body?.services?.length === 0 || !Array.isArray(orgServicesResult.body?.services)) { 50 | throw new Error('Failed to retrieve services for the organization. Received: ' + JSON.stringify(orgServicesResult.body)); 51 | } 52 | 53 | const orgServices = orgServicesResult.body.services; 54 | 55 | const orgServicesSdkCodesSet = new Set(orgServices.map((service) => service.code)); 56 | // we received less services than we requested this should never happen 57 | // we throw an error if it does 58 | if (orgServicesSdkCodesSet.size < sdkCodesSet.size) { 59 | const missingSdkCodes = [...sdkCodesSet].filter(a => !orgServicesSdkCodesSet.has(a)); 60 | throw new Error(`Not all services were found for the org. Found: ${orgServices.length}, Expected: ${sdkCodesSet.size} Missing: ${missingSdkCodes}`); 61 | } 62 | 63 | const orgServicesBySdkCode = orgServices.reduce((acc, service) => { 64 | acc[service.code] = service; 65 | return acc; 66 | }, {}); 67 | 68 | logger.debug(`Retrieved services for org ${orgId}`); 69 | let checkForPendingRequests = false; 70 | templates = templates.map((template) => { 71 | logger.debug(`Evaluating entitlements for template ${template.name}`); 72 | let userEntitled = true; 73 | let orgEntitled = true; 74 | let canRequestAccess = true; 75 | const disEntitledReasons = new Set(); 76 | 77 | template.apis.forEach((api) => { 78 | const orgService = orgServicesBySdkCode[api.code]; 79 | userEntitled = userEntitled && orgService.enabled; 80 | orgEntitled = orgEntitled && orgService.entitledForOrg; 81 | canRequestAccess = canRequestAccess && orgService.canRequestAccess; 82 | 83 | if (orgService.disabledReasons?.length > 0) { 84 | orgService.disabledReasons.forEach((reason) => { 85 | disEntitledReasons.add(reason); 86 | }); 87 | } 88 | if (Array.isArray(orgService.properties?.licenseConfigs)) { 89 | api.licenseConfigs = orgService.properties.licenseConfigs; 90 | } 91 | }); 92 | 93 | logger.debug(`Entitlements for orgId: ${orgId} template ${template.name}: userEntitled: ${userEntitled}, orgEntitled: ${orgEntitled}, canRequestAccess: ${canRequestAccess}, disEntitledReasons: ${JSON.stringify(disEntitledReasons)}`); 94 | 95 | // if user can request access and an app id has been defined for this template, we check whether there are any pending requests 96 | if (canRequestAccess && template.requestAccessAppId) { 97 | checkForPendingRequests = true; 98 | } 99 | 100 | return { 101 | ...template, 102 | userEntitled, 103 | orgEntitled, 104 | canRequestAccess, 105 | disEntitledReasons: [...disEntitledReasons], 106 | isRequestPending: false 107 | }; 108 | }); 109 | 110 | // if no need to check for pending requests, return the already evaluated templates 111 | if (!checkForPendingRequests) { 112 | return templates; 113 | } 114 | 115 | const appIdsWithPendingRequests = await fetchAppIdsWithPendingRequests(userToken, orgId, env, callerApiKey, logger); 116 | 117 | return templates.map(template => { 118 | if (template.requestAccessAppId && appIdsWithPendingRequests.has(template.requestAccessAppId)) { 119 | template.isRequestPending = true; 120 | } 121 | 122 | return template; 123 | }); 124 | } 125 | 126 | module.exports = { 127 | evaluateEntitlements 128 | }; 129 | -------------------------------------------------------------------------------- /test/fixtures/list/registry.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 4 | "author": "Adobe Inc.", 5 | "name": "@author/app-builder-template-1", 6 | "description": "A template for testing purposes", 7 | "latestVersion": "1.0.0", 8 | "publishDate": "2022-05-01T03:50:39.658Z", 9 | "apis": [ 10 | { 11 | "code": "AnalyticsSDK", 12 | "credentials": "OAuth" 13 | }, 14 | { 15 | "code": "CampaignStandard" 16 | }, 17 | { 18 | "code": "Runtime" 19 | }, 20 | { 21 | "code": "Events", 22 | "hooks": [ 23 | { 24 | "postdeploy": "some command" 25 | } 26 | ] 27 | }, 28 | { 29 | "code": "Mesh", 30 | "endpoints": [ 31 | { 32 | "my-action": "https://some-action.com/action" 33 | } 34 | ] 35 | } 36 | ], 37 | "adobeRecommended": false, 38 | "keywords": [ 39 | "aio", 40 | "adobeio", 41 | "app", 42 | "templates", 43 | "aio-app-builder-template" 44 | ], 45 | "status": "Approved", 46 | "links": { 47 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1", 48 | "github": "https://github.com/author/app-builder-template-1" 49 | }, 50 | "extensions": [ 51 | { 52 | "extensionPointId": "dx/excshell/1" 53 | } 54 | ], 55 | "categories": [ 56 | "action", 57 | "ui" 58 | ], 59 | "runtime": true 60 | }, 61 | { 62 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 63 | "author": "Adobe Inc.", 64 | "name": "@author/app-builder-template-2", 65 | "description": "A template for testing purposes", 66 | "latestVersion": "1.0.1", 67 | "publishDate": "2022-05-01T03:50:39.658Z", 68 | "apis": [ 69 | { 70 | "code": "Events", 71 | "hooks": [ 72 | { 73 | "postdeploy": "some command" 74 | } 75 | ] 76 | }, 77 | { 78 | "code": "Mesh", 79 | "endpoints": [ 80 | { 81 | "my-action": "https://some-action.com/action" 82 | } 83 | ] 84 | } 85 | ], 86 | "adobeRecommended": true, 87 | "keywords": [ 88 | "aio", 89 | "adobeio", 90 | "app", 91 | "templates", 92 | "aio-app-builder-template" 93 | ], 94 | "status": "Approved", 95 | "links": { 96 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2", 97 | "github": "https://github.com/author/app-builder-template-2" 98 | }, 99 | "extensions": [ 100 | { 101 | "extensionPointId": "dx/asset-compute/worker/1" 102 | } 103 | ], 104 | "categories": [ 105 | "events" 106 | ], 107 | "runtime": true, 108 | "event": {} 109 | }, 110 | { 111 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 112 | "author": "Adobe Inc.", 113 | "name": "@author/app-builder-template-3", 114 | "description": "A template for testing purposes", 115 | "latestVersion": "1.0.1", 116 | "publishDate": "2022-05-01T03:50:39.658Z", 117 | "apis": [ 118 | { 119 | "code": "CampaignStandard" 120 | } 121 | ], 122 | "adobeRecommended": true, 123 | "keywords": [ 124 | "aio", 125 | "adobeio", 126 | "app", 127 | "templates", 128 | "aio-app-builder-template" 129 | ], 130 | "status": "Approved", 131 | "links": { 132 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3", 133 | "github": "https://github.com/author/app-builder-template-3" 134 | }, 135 | "categories": [ 136 | "ui" 137 | ], 138 | "runtime": false 139 | }, 140 | { 141 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 142 | "name": "@author/app-builder-template-4", 143 | "status": "InVerification", 144 | "links": { 145 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 146 | "github": "https://github.com/author/app-builder-template-4" 147 | } 148 | }, 149 | { 150 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 151 | "name": "@author/app-builder-template-5", 152 | "status": "Rejected", 153 | "links": { 154 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 155 | "github": "https://github.com/author/app-builder-template-5" 156 | } 157 | }, 158 | { 159 | "id": "d1nc1000-f32e-4472-a3ec-9b2f3ef6ac48", 160 | "author": "Adobe Inc.", 161 | "name": "@author/app-builder-template-6", 162 | "description": "A template for testing purposes", 163 | "latestVersion": "1.0.1", 164 | "publishDate": "2022-06-11T04:50:39.658Z", 165 | "adobeRecommended": false, 166 | "keywords": [ 167 | "aio", 168 | "adobeio", 169 | "app", 170 | "templates", 171 | "aio-app-builder-template" 172 | ], 173 | "status": "Approved", 174 | "links": { 175 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-6", 176 | "github": "https://github.com/author/app-builder-template-6" 177 | }, 178 | "extensions": [ 179 | { 180 | "extensionPointId": "dx/asset-compute/worker/1" 181 | } 182 | ], 183 | "categories": [ 184 | "ui", 185 | "helper-template" 186 | ], 187 | "runtime": true 188 | } 189 | ] 190 | -------------------------------------------------------------------------------- /test/fixtures/list/response.github.issues.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://api.github.com/repos/adobe/aio-template-submission/issues/4", 4 | "repository_url": "https://api.github.com/repos/adobe/aio-template-submission", 5 | "labels_url": "https://api.github.com/repos/adobe/aio-template-submission/issues/4/labels{/name}", 6 | "comments_url": "https://api.github.com/repos/adobe/aio-template-submission/issues/4/comments", 7 | "events_url": "https://api.github.com/repos/adobe/aio-template-submission/issues/4/events", 8 | "html_url": "https://github.com/adobe/aio-template-submission/issues/4", 9 | "id": 1239020290, 10 | "node_id": "I_kwDOHJOaHM5J2fMC", 11 | "number": 4, 12 | "title": "@author/app-builder-template-4", 13 | "user": { 14 | "login": "adobe", 15 | "id": 17548019, 16 | "node_id": "MDQ6VXNlcjE3NTQ4MDE5", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/17548019?v=4", 18 | "gravatar_id": "", 19 | "url": "https://api.github.com/users/adobe", 20 | "html_url": "https://github.com/adobe", 21 | "followers_url": "https://api.github.com/users/adobe/followers", 22 | "following_url": "https://api.github.com/users/adobe/following{/other_user}", 23 | "gists_url": "https://api.github.com/users/adobe/gists{/gist_id}", 24 | "starred_url": "https://api.github.com/users/adobe/starred{/owner}{/repo}", 25 | "subscriptions_url": "https://api.github.com/users/adobe/subscriptions", 26 | "organizations_url": "https://api.github.com/users/adobe/orgs", 27 | "repos_url": "https://api.github.com/users/adobe/repos", 28 | "events_url": "https://api.github.com/users/adobe/events{/privacy}", 29 | "received_events_url": "https://api.github.com/users/adobe/received_events", 30 | "type": "User", 31 | "site_admin": false 32 | }, 33 | "labels": [ 34 | { 35 | "id": 4015123160, 36 | "node_id": "LA_kwDOHJOaHM7vUerY", 37 | "url": "https://api.github.com/repos/adobe/aio-template-submission/labels/add-template", 38 | "name": "add-template", 39 | "color": "c2e0c6", 40 | "default": false, 41 | "description": "" 42 | } 43 | ], 44 | "state": "open", 45 | "locked": false, 46 | "assignee": null, 47 | "assignees": [], 48 | "milestone": null, 49 | "comments": 1, 50 | "created_at": "2022-05-17T18:36:43Z", 51 | "updated_at": "2022-05-17T18:37:43Z", 52 | "closed_at": null, 53 | "author_association": "OWNER", 54 | "active_lock_reason": null, 55 | "body": "### Link to GitHub repo\n\nhttps://github.com/author/app-builder-template-4\n\n### npm package name\n\n@author/app-builder-template-4", 56 | "reactions": { 57 | "url": "https://api.github.com/repos/adobe/aio-template-submission/issues/4/reactions", 58 | "total_count": 0, 59 | "+1": 0, 60 | "-1": 0, 61 | "laugh": 0, 62 | "hooray": 0, 63 | "confused": 0, 64 | "heart": 0, 65 | "rocket": 0, 66 | "eyes": 0 67 | }, 68 | "timeline_url": "https://api.github.com/repos/adobe/aio-template-submission/issues/4/timeline", 69 | "performed_via_github_app": null 70 | }, 71 | { 72 | "url": "https://api.github.com/repos/adobe/aio-template-submission/issues/5", 73 | "repository_url": "https://api.github.com/repos/adobe/aio-template-submission", 74 | "labels_url": "https://api.github.com/repos/adobe/aio-template-submission/issues/5/labels{/name}", 75 | "comments_url": "https://api.github.com/repos/adobe/aio-template-submission/issues/5/comments", 76 | "events_url": "https://api.github.com/repos/adobe/aio-template-submission/issues/5/events", 77 | "html_url": "https://github.com/adobe/aio-template-submission/issues/5", 78 | "id": 1239020290, 79 | "node_id": "I_kwDOHJOaHM5J2fMC", 80 | "number": 5, 81 | "title": "@author/app-builder-template-5", 82 | "user": { 83 | "login": "adobe", 84 | "id": 17558019, 85 | "node_id": "MDQ6VXNlcjE3NTQ5MDE5", 86 | "avatar_url": "https://avatars.githubusercontent.com/u/17558019?v=4", 87 | "gravatar_id": "", 88 | "url": "https://api.github.com/users/adobe", 89 | "html_url": "https://github.com/adobe", 90 | "followers_url": "https://api.github.com/users/adobe/followers", 91 | "following_url": "https://api.github.com/users/adobe/following{/other_user}", 92 | "gists_url": "https://api.github.com/users/adobe/gists{/gist_id}", 93 | "starred_url": "https://api.github.com/users/adobe/starred{/owner}{/repo}", 94 | "subscriptions_url": "https://api.github.com/users/adobe/subscriptions", 95 | "organizations_url": "https://api.github.com/users/adobe/orgs", 96 | "repos_url": "https://api.github.com/users/adobe/repos", 97 | "events_url": "https://api.github.com/users/adobe/events{/privacy}", 98 | "received_events_url": "https://api.github.com/users/adobe/received_events", 99 | "type": "User", 100 | "site_admin": false 101 | }, 102 | "labels": [ 103 | { 104 | "id": 5015123160, 105 | "node_id": "LA_kwDOHJOaHM7vUerY", 106 | "url": "https://api.github.com/repos/adobe/aio-template-submission/labels/add-template", 107 | "name": "add-template", 108 | "color": "c2e0c6", 109 | "default": false, 110 | "description": "" 111 | } 112 | ], 113 | "state": "open", 114 | "locked": false, 115 | "assignee": null, 116 | "assignees": [], 117 | "milestone": null, 118 | "comments": 1, 119 | "created_at": "2022-05-17T18:36:53Z", 120 | "updated_at": "2022-05-17T18:37:53Z", 121 | "closed_at": null, 122 | "author_association": "OWNER", 123 | "active_lock_reason": null, 124 | "body": "### Link to GitHub repo\n\nhttps://github.com/author/app-builder-template-5\n\n### npm package name\n\n@author/app-builder-template-5", 125 | "reactions": { 126 | "url": "https://api.github.com/repos/adobe/aio-template-submission/issues/5/reactions", 127 | "total_count": 0, 128 | "+1": 0, 129 | "-1": 0, 130 | "laugh": 0, 131 | "hooray": 0, 132 | "confused": 0, 133 | "heart": 0, 134 | "rocket": 0, 135 | "eyes": 0 136 | }, 137 | "timeline_url": "https://api.github.com/repos/adobe/aio-template-submission/issues/5/timeline", 138 | "performed_via_github_app": null 139 | } 140 | ] 141 | -------------------------------------------------------------------------------- /actions/templates/delete/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const { Core } = require('@adobe/aio-sdk'); 13 | const { errorResponse, errorMessage, getBearerToken, stringParameters, checkMissingRequestInputs, ERR_RC_SERVER_ERROR, ERR_RC_HTTP_METHOD_NOT_ALLOWED, ERR_RC_INVALID_IMS_ACCESS_TOKEN, ERR_RC_PERMISSION_DENIED } = 14 | require('../../utils'); 15 | const { validateAccessToken, isAdmin, isValidServiceToken } = require('../../ims'); 16 | const { removeTemplateById, removeTemplateByName } = require('../../templateRegistry'); 17 | const { incBatchCounter } = require('@adobe/aio-metrics-client'); 18 | const { getTokenData } = require('@adobe/aio-lib-ims'); 19 | const { setMetricsUrl, incErrorCounterMetrics } = require('../../metrics'); 20 | 21 | const HTTP_METHOD = 'delete'; 22 | const ENDPOINT = 'DELETE /templates'; 23 | const METRICS_KEY = 'recordtemplateregistrymetrics'; 24 | const requiredScopes = ['template_registry.write']; 25 | 26 | const response200 = { statusCode: 200 }; 27 | const response404 = { statusCode: 404 }; 28 | 29 | const deleteTemplateByNameFunc = async (params, dbParams) => { 30 | const orgName = params.orgName; 31 | const templateName = params.templateName; 32 | if ((orgName === undefined) && (templateName === undefined)) { 33 | return response404; 34 | } 35 | const fullTemplateName = (orgName !== undefined) ? orgName + '/' + templateName : templateName; 36 | const dbResponse = await removeTemplateByName(dbParams, fullTemplateName); 37 | if (dbResponse.deletedCount === 0) { 38 | return response404; 39 | } 40 | return response200; 41 | }; 42 | 43 | const deleteTemplateByIdFunc = async (params, dbParams) => { 44 | const templateId = params.templateId; 45 | if ((templateId === undefined) || (templateId === null)) { 46 | return response404; 47 | } 48 | const dbResponse = await removeTemplateById(dbParams, templateId); 49 | if (dbResponse.deletedCount === 0) { 50 | return response404; 51 | } 52 | return response200; 53 | }; 54 | 55 | /** 56 | * Delete a template from the Template Registry. 57 | * @param {object} params request parameters 58 | * @returns {object} response 59 | */ 60 | async function main (params) { 61 | // create a Logger 62 | const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }); 63 | 64 | const imsUrl = params.IMS_URL; 65 | const imsClientId = params.IMS_CLIENT_ID; 66 | const adminImsOrganizations = params.ADMIN_IMS_ORGANIZATIONS.split(','); 67 | 68 | const dbParams = { 69 | MONGODB_URI: params.MONGODB_URI, 70 | MONGODB_NAME: params.MONGODB_NAME 71 | }; 72 | 73 | let requester = 'unauth'; 74 | if (params?.METRICS_URL) { 75 | setMetricsUrl(params.METRICS_URL, METRICS_KEY); 76 | } 77 | 78 | try { 79 | // 'info' is the default level if not set 80 | logger.info('Calling "DELETE templates"'); 81 | 82 | // log parameters, only if params.LOG_LEVEL === 'debug' 83 | logger.debug(stringParameters(params)); 84 | 85 | // check for missing request input parameters and headers 86 | const requiredParams = [/* add required params */]; 87 | const requiredHeaders = ['Authorization']; 88 | const errorMessages = checkMissingRequestInputs(params, requiredParams, requiredHeaders); 89 | if (errorMessages) { 90 | await incErrorCounterMetrics(requester, ENDPOINT, '401'); 91 | return errorResponse(401, errorMessages, logger); 92 | } 93 | 94 | // extract the user Bearer token from the Authorization header 95 | const accessToken = getBearerToken(params); 96 | requester = getTokenData(accessToken)?.user_id; 97 | await incBatchCounter('request_count', requester, ENDPOINT); 98 | 99 | if (params.__ow_method === undefined || params.__ow_method.toLowerCase() !== HTTP_METHOD) { 100 | await incErrorCounterMetrics(requester, ENDPOINT, '405'); 101 | return errorResponse(405, [errorMessage(ERR_RC_HTTP_METHOD_NOT_ALLOWED, `HTTP "${params.__ow_method}" method is unsupported.`)], logger); 102 | } 103 | 104 | try { 105 | // validate the token, an exception will be thrown for a non-valid token 106 | await validateAccessToken(accessToken, imsUrl, imsClientId); 107 | } catch (error) { 108 | await incErrorCounterMetrics(requester, ENDPOINT, '401'); 109 | return errorResponse(401, [errorMessage(ERR_RC_INVALID_IMS_ACCESS_TOKEN, error.message)], logger); 110 | } 111 | 112 | // check if the token is a service token and has access to the required scopes 113 | if (!isValidServiceToken(accessToken, requiredScopes)) { 114 | // check if the token belongs to an admin 115 | const isCallerAdmin = await isAdmin(accessToken, imsUrl, adminImsOrganizations); 116 | if (isCallerAdmin !== true) { 117 | const err = 'This operation is available to admins only. To request template removal from Template Registry, please, create a "Template Removal Request" issue on https://github.com/adobe/aio-template-submission'; 118 | await incErrorCounterMetrics(requester, ENDPOINT, '403'); 119 | return errorResponse(403, [errorMessage(ERR_RC_PERMISSION_DENIED, err)], logger); 120 | } 121 | } 122 | 123 | const shouldDeleteById = ('templateId' in params); 124 | let response = null; 125 | if (!shouldDeleteById) { 126 | response = await deleteTemplateByNameFunc(params, dbParams); 127 | } else { 128 | response = await deleteTemplateByIdFunc(params, dbParams); 129 | } 130 | if (response.statusCode === 404) { 131 | await incErrorCounterMetrics(requester, ENDPOINT, '404'); 132 | } 133 | logger.info('"DELETE templates" executed successfully'); 134 | return response; 135 | } catch (error) { 136 | // log any server errors 137 | logger.error(error); 138 | // return with 500 139 | await incErrorCounterMetrics(requester, ENDPOINT, '500'); 140 | return errorResponse(500, [errorMessage(ERR_RC_SERVER_ERROR, 'An error occurred, please try again later.')], logger); 141 | } 142 | } 143 | 144 | exports.main = main; 145 | -------------------------------------------------------------------------------- /test/fixtures/list/response.exclusion-filter.only.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?categories=!helper-template" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 12 | "author": "Adobe Inc.", 13 | "name": "@author/app-builder-template-1", 14 | "description": "A template for testing purposes", 15 | "latestVersion": "1.0.0", 16 | "publishDate": "2022-05-01T03:50:39.658Z", 17 | "apis": [ 18 | { 19 | "code": "AnalyticsSDK", 20 | "credentials": "OAuth" 21 | }, 22 | { 23 | "code": "CampaignStandard" 24 | }, 25 | { 26 | "code": "Runtime" 27 | }, 28 | { 29 | "code": "Events", 30 | "hooks": [ 31 | { 32 | "postdeploy": "some command" 33 | } 34 | ] 35 | }, 36 | { 37 | "code": "Mesh", 38 | "endpoints": [ 39 | { 40 | "my-action": "https://some-action.com/action" 41 | } 42 | ] 43 | } 44 | ], 45 | "adobeRecommended": false, 46 | "keywords": [ 47 | "aio", 48 | "adobeio", 49 | "app", 50 | "templates", 51 | "aio-app-builder-template" 52 | ], 53 | "status": "Approved", 54 | "links": { 55 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1", 56 | "github": "https://github.com/author/app-builder-template-1" 57 | }, 58 | "extensions": [ 59 | { 60 | "extensionPointId": "dx/excshell/1" 61 | } 62 | ], 63 | "categories": [ 64 | "action", 65 | "ui" 66 | ], 67 | "runtime": true, 68 | "_links": { 69 | "self": { 70 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-1" 71 | } 72 | } 73 | }, 74 | { 75 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 76 | "author": "Adobe Inc.", 77 | "name": "@author/app-builder-template-2", 78 | "description": "A template for testing purposes", 79 | "latestVersion": "1.0.1", 80 | "publishDate": "2022-05-01T03:50:39.658Z", 81 | "apis": [ 82 | { 83 | "code": "Events", 84 | "hooks": [ 85 | { 86 | "postdeploy": "some command" 87 | } 88 | ] 89 | }, 90 | { 91 | "code": "Mesh", 92 | "endpoints": [ 93 | { 94 | "my-action": "https://some-action.com/action" 95 | } 96 | ] 97 | } 98 | ], 99 | "adobeRecommended": true, 100 | "keywords": [ 101 | "aio", 102 | "adobeio", 103 | "app", 104 | "templates", 105 | "aio-app-builder-template" 106 | ], 107 | "status": "Approved", 108 | "links": { 109 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2", 110 | "github": "https://github.com/author/app-builder-template-2" 111 | }, 112 | "extensions": [ 113 | { 114 | "extensionPointId": "dx/asset-compute/worker/1" 115 | } 116 | ], 117 | "categories": [ 118 | "events" 119 | ], 120 | "runtime": true, 121 | "event": {}, 122 | "_links": { 123 | "self": { 124 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-2" 125 | } 126 | } 127 | }, 128 | { 129 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 130 | "author": "Adobe Inc.", 131 | "name": "@author/app-builder-template-3", 132 | "description": "A template for testing purposes", 133 | "latestVersion": "1.0.1", 134 | "publishDate": "2022-05-01T03:50:39.658Z", 135 | "apis": [ 136 | { 137 | "code": "CampaignStandard" 138 | } 139 | ], 140 | "adobeRecommended": true, 141 | "keywords": [ 142 | "aio", 143 | "adobeio", 144 | "app", 145 | "templates", 146 | "aio-app-builder-template" 147 | ], 148 | "status": "Approved", 149 | "links": { 150 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3", 151 | "github": "https://github.com/author/app-builder-template-3" 152 | }, 153 | "categories": [ 154 | "ui" 155 | ], 156 | "runtime": false, 157 | "_links": { 158 | "self": { 159 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-3" 160 | } 161 | } 162 | } 163 | ] 164 | } 165 | } -------------------------------------------------------------------------------- /test/fixtures/list/response.filter-value-any.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?extensions=*" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "_links": { 12 | "self": { 13 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-1" 14 | } 15 | }, 16 | "adobeRecommended": false, 17 | "author": "Adobe Inc.", 18 | "categories": [ 19 | "action", 20 | "ui" 21 | ], 22 | "description": "A template for testing purposes", 23 | "extensions": [ 24 | { 25 | "extensionPointId": "dx/excshell/1" 26 | } 27 | ], 28 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 29 | "keywords": [ 30 | "aio", 31 | "adobeio", 32 | "app", 33 | "templates", 34 | "aio-app-builder-template" 35 | ], 36 | "latestVersion": "1.0.0", 37 | "links": { 38 | "github": "https://github.com/author/app-builder-template-1", 39 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1" 40 | }, 41 | "name": "@author/app-builder-template-1", 42 | "publishDate": "2022-05-01T03:50:39.658Z", 43 | "apis": [ 44 | { 45 | "code": "AnalyticsSDK", 46 | "credentials": "OAuth" 47 | }, 48 | { 49 | "code": "CampaignStandard" 50 | }, 51 | { 52 | "code": "Runtime" 53 | }, 54 | { 55 | "code": "Events", 56 | "hooks": [ 57 | { 58 | "postdeploy": "some command" 59 | } 60 | ] 61 | }, 62 | { 63 | "code": "Mesh", 64 | "endpoints": [ 65 | { 66 | "my-action": "https://some-action.com/action" 67 | } 68 | ] 69 | } 70 | ], 71 | "status": "Approved", 72 | "runtime": true 73 | }, 74 | { 75 | "_links": { 76 | "self": { 77 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-2" 78 | } 79 | }, 80 | "adobeRecommended": true, 81 | "author": "Adobe Inc.", 82 | "categories": [ 83 | "events" 84 | ], 85 | "description": "A template for testing purposes", 86 | "extensions": [ 87 | { 88 | "extensionPointId": "dx/asset-compute/worker/1" 89 | } 90 | ], 91 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 92 | "keywords": [ 93 | "aio", 94 | "adobeio", 95 | "app", 96 | "templates", 97 | "aio-app-builder-template" 98 | ], 99 | "latestVersion": "1.0.1", 100 | "links": { 101 | "github": "https://github.com/author/app-builder-template-2", 102 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2" 103 | }, 104 | "name": "@author/app-builder-template-2", 105 | "publishDate": "2022-05-01T03:50:39.658Z", 106 | "apis": [ 107 | { 108 | "code": "Events", 109 | "hooks": [ 110 | { 111 | "postdeploy": "some command" 112 | } 113 | ] 114 | }, 115 | { 116 | "code": "Mesh", 117 | "endpoints": [ 118 | { 119 | "my-action": "https://some-action.com/action" 120 | } 121 | ] 122 | } 123 | ], 124 | "status": "Approved", 125 | "runtime": true, 126 | "event": {} 127 | }, 128 | { 129 | "_links": { 130 | "self": { 131 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-6" 132 | } 133 | }, 134 | "adobeRecommended": false, 135 | "author": "Adobe Inc.", 136 | "categories": [ 137 | "ui", 138 | "helper-template" 139 | ], 140 | "description": "A template for testing purposes", 141 | "extensions": [ 142 | { 143 | "extensionPointId": "dx/asset-compute/worker/1" 144 | } 145 | ], 146 | "id": "d1nc1000-f32e-4472-a3ec-9b2f3ef6ac48", 147 | "keywords": [ 148 | "aio", 149 | "adobeio", 150 | "app", 151 | "templates", 152 | "aio-app-builder-template" 153 | ], 154 | "latestVersion": "1.0.1", 155 | "links": { 156 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-6", 157 | "github": "https://github.com/author/app-builder-template-6" 158 | }, 159 | "name": "@author/app-builder-template-6", 160 | "publishDate": "2022-06-11T04:50:39.658Z", 161 | "status": "Approved", 162 | "runtime": true 163 | } 164 | ] 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /test/fixtures/list/response.orderBy.names.desc.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?orderBy=names desc" 7 | } 8 | }, 9 | "items": [{ 10 | "_links": { 11 | "self": { 12 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-6" 13 | } 14 | }, 15 | "id": "d1nc1000-f32e-4472-a3ec-9b2f3ef6ac48", 16 | "author": "Adobe Inc.", 17 | "name": "@author/app-builder-template-6", 18 | "description": "A template for testing purposes", 19 | "latestVersion": "1.0.1", 20 | "publishDate": "2022-06-11T04:50:39.658Z", 21 | "adobeRecommended": false, 22 | "keywords": [ 23 | "aio", 24 | "adobeio", 25 | "app", 26 | "templates", 27 | "aio-app-builder-template" 28 | ], 29 | "status": "Approved", 30 | "links": { 31 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-6", 32 | "github": "https://github.com/author/app-builder-template-6" 33 | }, 34 | "extensions": [ 35 | { 36 | "extensionPointId": "dx/asset-compute/worker/1" 37 | } 38 | ], 39 | "categories": [ 40 | "ui", 41 | "helper-template" 42 | ], 43 | "runtime": true 44 | }, { 45 | "_links": { 46 | "self": { 47 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-5" 48 | }, 49 | "review": { 50 | "href": "https://github.com/adobe/aio-templates/issues/100", 51 | "description": "A link to the \"Template Review Request\" Github issue." 52 | } 53 | }, 54 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 55 | "name": "@author/app-builder-template-5", 56 | "status": "Rejected", 57 | "links": { 58 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 59 | "github": "https://github.com/author/app-builder-template-5" 60 | } 61 | }, { 62 | "_links": { 63 | "self": { 64 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-4" 65 | }, 66 | "review": { 67 | "href": "https://github.com/adobe/aio-templates/issues/100", 68 | "description": "A link to the \"Template Review Request\" Github issue." 69 | } 70 | }, 71 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 72 | "name": "@author/app-builder-template-4", 73 | "status": "InVerification", 74 | "links": { 75 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 76 | "github": "https://github.com/author/app-builder-template-4" 77 | } 78 | }, { 79 | "_links": { 80 | "self": { 81 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-3" 82 | } 83 | }, 84 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 85 | "author": "Adobe Inc.", 86 | "name": "@author/app-builder-template-3", 87 | "description": "A template for testing purposes", 88 | "latestVersion": "1.0.1", 89 | "publishDate": "2022-05-01T03:50:39.658Z", 90 | "apis": [{ 91 | "code": "CampaignStandard" 92 | }], 93 | "adobeRecommended": true, 94 | "keywords": ["aio", "adobeio", "app", "templates", "aio-app-builder-template"], 95 | "status": "Approved", 96 | "links": { 97 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3", 98 | "github": "https://github.com/author/app-builder-template-3" 99 | }, 100 | "categories": ["ui"], 101 | "runtime": false 102 | }, { 103 | "_links": { 104 | "self": { 105 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-2" 106 | } 107 | }, 108 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 109 | "author": "Adobe Inc.", 110 | "name": "@author/app-builder-template-2", 111 | "description": "A template for testing purposes", 112 | "latestVersion": "1.0.1", 113 | "publishDate": "2022-05-01T03:50:39.658Z", 114 | "apis": [{ 115 | "code": "Events", 116 | "hooks": [{ 117 | "postdeploy": "some command" 118 | }] 119 | }, { 120 | "code": "Mesh", 121 | "endpoints": [{ 122 | "my-action": "https://some-action.com/action" 123 | }] 124 | }], 125 | "adobeRecommended": true, 126 | "keywords": ["aio", "adobeio", "app", "templates", "aio-app-builder-template"], 127 | "status": "Approved", 128 | "links": { 129 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2", 130 | "github": "https://github.com/author/app-builder-template-2" 131 | }, 132 | "extensions": [ 133 | { 134 | "extensionPointId": "dx/asset-compute/worker/1" 135 | } 136 | ], 137 | "categories": ["events"], 138 | "runtime": true, 139 | "event": {} 140 | }, { 141 | "_links": { 142 | "self": { 143 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-1" 144 | } 145 | }, 146 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 147 | "author": "Adobe Inc.", 148 | "name": "@author/app-builder-template-1", 149 | "description": "A template for testing purposes", 150 | "latestVersion": "1.0.0", 151 | "publishDate": "2022-05-01T03:50:39.658Z", 152 | "apis": [{ 153 | "code": "AnalyticsSDK", 154 | "credentials": "OAuth" 155 | }, { 156 | "code": "CampaignStandard" 157 | }, { 158 | "code": "Runtime" 159 | }, { 160 | "code": "Events", 161 | "hooks": [{ 162 | "postdeploy": "some command" 163 | }] 164 | }, { 165 | "code": "Mesh", 166 | "endpoints": [{ 167 | "my-action": "https://some-action.com/action" 168 | }] 169 | }], 170 | "adobeRecommended": false, 171 | "keywords": ["aio", "adobeio", "app", "templates", "aio-app-builder-template"], 172 | "status": "Approved", 173 | "links": { 174 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1", 175 | "github": "https://github.com/author/app-builder-template-1" 176 | }, 177 | "extensions": [ 178 | { 179 | "extensionPointId": "dx/excshell/1" 180 | } 181 | ], 182 | "categories": ["action", "ui"], 183 | "runtime": true 184 | }] 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /actions/templates/get/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const { Core } = require('@adobe/aio-sdk'); 13 | const { errorResponse, errorMessage, stringParameters, getBearerToken, ERR_RC_SERVER_ERROR, ERR_RC_HTTP_METHOD_NOT_ALLOWED } = require('../../utils'); 14 | const { findTemplateByName, getReviewIssueByTemplateName, TEMPLATE_STATUS_IN_VERIFICATION, TEMPLATE_STATUS_REJECTED, findTemplateById } = 15 | require('../../templateRegistry'); 16 | const Enforcer = require('openapi-enforcer'); 17 | const { evaluateEntitlements } = require('../../templateEntitlement'); 18 | const { incBatchCounter } = require('@adobe/aio-metrics-client'); 19 | const { getTokenData } = require('@adobe/aio-lib-ims'); 20 | const { setMetricsUrl, incErrorCounterMetrics } = require('../../metrics'); 21 | 22 | // GET operation is available to everyone, no IMS access token is required 23 | const HTTP_METHOD = 'get'; 24 | const ENDPOINT = 'GET /templates/{templateId}'; 25 | const METRICS_KEY = 'recordtemplateregistrymetrics'; 26 | 27 | /** 28 | * 29 | * @param {object} params - object with request input 30 | * @param {object} dbParams - database connection parameters 31 | * @param {object} logger - logger object 32 | * @returns {object} response object 33 | */ 34 | async function fetchTemplateById (params, dbParams, logger) { 35 | const templateId = params.templateId; 36 | if (templateId === undefined || templateId === null) { 37 | return { 38 | statusCode: 404 39 | }; 40 | } 41 | const template = await findTemplateById(dbParams, templateId); 42 | if (template === null || template === undefined) { 43 | return { 44 | statusCode: 404 45 | }; 46 | } 47 | const response = { 48 | ...template, 49 | _links: { 50 | self: { 51 | href: `${params.TEMPLATE_REGISTRY_API_URL}/templates/${template.id}` 52 | } 53 | } 54 | }; 55 | 56 | // dev console templates are auto-approved by default, so this block will only be true for app builder templates 57 | const templateStatuses = [TEMPLATE_STATUS_IN_VERIFICATION, TEMPLATE_STATUS_REJECTED]; 58 | if (templateStatuses.includes(template.status)) { 59 | const reviewIssue = await getReviewIssueByTemplateName(template.name, params.TEMPLATE_REGISTRY_ORG, params.TEMPLATE_REGISTRY_REPOSITORY); 60 | if (reviewIssue !== null) { 61 | response._links.review = { 62 | href: reviewIssue, 63 | description: 'A link to the "Template Review Request" Github issue.' 64 | }; 65 | } 66 | } 67 | return response; 68 | } 69 | 70 | /** 71 | * 72 | * @param {object} params - object with request input 73 | * @param {object} dbParams - database connection parameters 74 | * @param {object} logger - logger object 75 | * @returns {object} response object 76 | */ 77 | async function fetchTemplateByName (params, dbParams, logger) { 78 | const orgName = params.orgName; 79 | const templateName = params.templateName; 80 | if ((orgName === undefined) && (templateName === undefined)) { 81 | return { 82 | statusCode: 404 83 | }; 84 | } 85 | const fullTemplateName = (orgName !== undefined) ? orgName + '/' + templateName : templateName; 86 | const template = await findTemplateByName(dbParams, fullTemplateName); 87 | if (template === null) { 88 | return { 89 | statusCode: 404 90 | }; 91 | } 92 | const response = { 93 | ...template, 94 | _links: { 95 | self: { 96 | href: `${params.TEMPLATE_REGISTRY_API_URL}/templates/${fullTemplateName}` 97 | } 98 | } 99 | }; 100 | 101 | // dev console templates are auto-approved by default, so this block will only be true for app builder templates 102 | const templateStatuses = [TEMPLATE_STATUS_IN_VERIFICATION, TEMPLATE_STATUS_REJECTED]; 103 | if (templateStatuses.includes(template.status)) { 104 | const reviewIssue = await getReviewIssueByTemplateName(fullTemplateName, params.TEMPLATE_REGISTRY_ORG, params.TEMPLATE_REGISTRY_REPOSITORY); 105 | if (reviewIssue !== null) { 106 | response._links.review = { 107 | href: reviewIssue, 108 | description: 'A link to the "Template Review Request" Github issue.' 109 | }; 110 | } 111 | } 112 | 113 | return response; 114 | } 115 | 116 | /** 117 | * Get a template from the Template Registry. 118 | * @param {object} params request parameters 119 | * @returns {object} response 120 | */ 121 | async function main (params) { 122 | // create a Logger 123 | const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }); 124 | const dbParams = { 125 | MONGODB_URI: params.MONGODB_URI, 126 | MONGODB_NAME: params.MONGODB_NAME 127 | }; 128 | 129 | let requester = 'unauth'; 130 | if (params?.METRICS_URL) { 131 | setMetricsUrl(params.METRICS_URL, METRICS_KEY); 132 | } 133 | 134 | try { 135 | // 'info' is the default level if not set 136 | logger.info('Calling "GET templates"'); 137 | 138 | // log parameters, only if params.LOG_LEVEL === 'debug' 139 | logger.debug(stringParameters(params)); 140 | 141 | // extract the user Bearer token from the Authorization header 142 | const accessToken = getBearerToken(params); 143 | if (accessToken) { 144 | requester = getTokenData(accessToken)?.user_id; 145 | } 146 | 147 | await incBatchCounter('request_count', requester, ENDPOINT); 148 | 149 | // checking for valid method 150 | if (params.__ow_method === undefined || params.__ow_method.toLowerCase() !== HTTP_METHOD) { 151 | incErrorCounterMetrics(requester, ENDPOINT, '405'); 152 | return errorResponse(405, [errorMessage(ERR_RC_HTTP_METHOD_NOT_ALLOWED, `HTTP "${params.__ow_method}" method is unsupported.`)], logger); 153 | } 154 | // checking for validation based on params call. 155 | const paramField = 'templateId' in params ? 'templateId' : 'templateName'; 156 | Enforcer.v3_0.Schema.defineDataTypeFormat('string', 'uuid', null); 157 | Enforcer.v3_0.Schema.defineDataTypeFormat('string', 'uri', null); 158 | 159 | // WPAR002 - skip a warning about the "allowEmptyValue" property 160 | // see https://swagger.io/docs/specification/describing-parameters/ Empty-Valued and Nullable Parameters 161 | const openapi = await Enforcer('./template-registry-api.json', { componentOptions: { exceptionSkipCodes: ['WPAR002'] } }); 162 | const [req] = openapi.request({ 163 | method: HTTP_METHOD, 164 | path: `/templates/{${paramField}}` 165 | }); 166 | 167 | let response = {}; 168 | if (paramField === 'templateId') { 169 | response = await fetchTemplateById(params, dbParams, logger); 170 | if (response.statusCode === 404) { 171 | await incErrorCounterMetrics(requester, ENDPOINT, '404'); 172 | return response; 173 | } 174 | } else { 175 | response = await fetchTemplateByName(params, dbParams, logger); 176 | if (response.statusCode === 404) { 177 | await incErrorCounterMetrics(requester, ENDPOINT, '404'); 178 | return response; 179 | } 180 | } 181 | 182 | const evaluatedTemplates = await evaluateEntitlements([response], params, logger); 183 | if (evaluatedTemplates?.length > 0) { 184 | response = evaluatedTemplates[0]; 185 | } 186 | 187 | // validate the response data to be sure it complies with OpenApi Schema 188 | const [res, error] = req.response(200, response); 189 | if (error) { 190 | throw new Error(error.toString()); 191 | } 192 | 193 | logger.info('"GET templates" executed successfully'); 194 | return { 195 | statusCode: 200, 196 | body: res.body 197 | }; 198 | } catch (error) { 199 | // log any server errors 200 | logger.error(error); 201 | // return with 500 202 | await incErrorCounterMetrics(requester, ENDPOINT, '500'); 203 | return errorResponse(500, [errorMessage(ERR_RC_SERVER_ERROR, error.message)], logger); 204 | } 205 | } 206 | 207 | exports.main = main; 208 | -------------------------------------------------------------------------------- /test/ims.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const nock = require('nock'); 13 | const { Ims, getTokenData } = require('@adobe/aio-lib-ims'); 14 | const { validateAccessToken, isAdmin, generateAccessToken, isValidServiceToken } = require('../actions/ims'); 15 | 16 | jest.mock('@adobe/aio-lib-ims'); 17 | 18 | const mockLoggerInstance = { info: jest.fn(), debug: jest.fn(), error: jest.fn() }; 19 | 20 | process.env = { 21 | IMS_URL: 'https://ims-na1-stg1.adobelogin.com', 22 | IMS_CLIENT_ID: 'test' 23 | }; 24 | 25 | beforeEach(() => { 26 | jest.clearAllMocks(); 27 | jest.resetAllMocks(); 28 | }); 29 | 30 | describe('Verify communication with IMS', () => { 31 | test('Verify checking that provided IMS access token is valid', async () => { 32 | nock(process.env.IMS_URL) 33 | .get(`/ims/validate_token/v1?client_id=${process.env.IMS_CLIENT_ID}&type=access_token`) 34 | .times(1) 35 | .reply(200, { 36 | valid: true 37 | }); 38 | 39 | await expect(validateAccessToken('', process.env.IMS_URL, process.env.IMS_CLIENT_ID)) 40 | .resolves.toBeUndefined(); 41 | }); 42 | 43 | test('Verify that exception is thrown for non-valid IMS access token', async () => { 44 | nock(process.env.IMS_URL) 45 | .get(`/ims/validate_token/v1?client_id=${process.env.IMS_CLIENT_ID}&type=access_token`) 46 | .times(1) 47 | .reply(200, { 48 | valid: false, 49 | reason: 'bad_signature' 50 | }); 51 | 52 | await expect(validateAccessToken('', process.env.IMS_URL, process.env.IMS_CLIENT_ID)) 53 | .rejects.toThrow('Provided IMS access token is invalid. Reason: bad_signature'); 54 | }); 55 | 56 | test('Verify checking that provided IMS access token belongs to admin user', async () => { 57 | const adminImsOrganizations = [ 58 | 'adminOrg@AdobeOrg' 59 | ]; 60 | nock(process.env.IMS_URL) 61 | .get('/ims/organizations/v6') 62 | .times(1) 63 | .reply(200, [ 64 | { 65 | orgName: 'Org1', 66 | orgRef: { ident: 'adminOrg', authSrc: 'AdobeOrg' }, 67 | orgType: 'Enterprise' 68 | }, 69 | { 70 | orgName: 'Org2', 71 | orgRef: { ident: 'non-adminOrg', authSrc: 'AdobeOrg' }, 72 | orgType: 'Enterprise' 73 | } 74 | ]); 75 | 76 | await expect(isAdmin('', process.env.IMS_URL, adminImsOrganizations)) 77 | .resolves.toBe(true); 78 | }); 79 | 80 | test('Verify checking that provided IMS access token does not belong to admin user', async () => { 81 | const adminImsOrganizations = [ 82 | 'adminOrg@AdobeOrg' 83 | ]; 84 | nock(process.env.IMS_URL) 85 | .get('/ims/organizations/v6') 86 | .times(1) 87 | .reply(200, [ 88 | { 89 | orgName: 'Org1', 90 | orgRef: { ident: 'non-adminOrg1', authSrc: 'AdobeOrg' }, 91 | orgType: 'Enterprise' 92 | }, 93 | { 94 | orgName: 'Org2', 95 | orgRef: { ident: 'non-adminOrg2', authSrc: 'AdobeOrg' }, 96 | orgType: 'Enterprise' 97 | } 98 | ]); 99 | 100 | await expect(isAdmin('', process.env.IMS_URL, adminImsOrganizations)) 101 | .resolves.toBe(false); 102 | }); 103 | 104 | test('Verify that exception is thrown for non-successful IMS communication', async () => { 105 | nock(process.env.IMS_URL) 106 | .get('/ims/organizations/v6') 107 | .times(1) 108 | .reply(400); 109 | 110 | await expect(isAdmin('', process.env.IMS_URL, [])) 111 | .rejects.toThrow(`Error fetching "${process.env.IMS_URL}/ims/organizations/v6". AxiosError: Request failed with status code 400`); 112 | }); 113 | 114 | test('Verify that exception is thrown for non-successful IMS communication, non-error code', async () => { 115 | nock(process.env.IMS_URL) 116 | .get('/ims/organizations/v6') 117 | .times(1) 118 | .reply(201); 119 | 120 | await expect(isAdmin('', process.env.IMS_URL, [])) 121 | .rejects.toThrow(`Error fetching "${process.env.IMS_URL}/ims/organizations/v6". Response code is 201`); 122 | }); 123 | 124 | test('Verify generate access token', async () => { 125 | const getAccessTokenMock = jest.fn(() => ({ 126 | payload: { 127 | access_token: 'my-access-token', 128 | refresh_token: 'my-refresh-token' 129 | } 130 | })); 131 | Ims.mockImplementation(() => ({ 132 | getAccessToken: getAccessTokenMock 133 | })); 134 | 135 | // Initial call that should fetch the token 136 | await expect(generateAccessToken('', process.env.IMS_CLIENT_ID, 'client-secret', 'adobeid', mockLoggerInstance)) 137 | .resolves.toBe('my-access-token'); 138 | expect(getAccessTokenMock).toHaveBeenCalledTimes(1); 139 | 140 | // Subsequent call that should use the cached token 141 | await expect(generateAccessToken('', process.env.IMS_CLIENT_ID, 'client-secret', 'adobeid', mockLoggerInstance)) 142 | .resolves.toBe('my-access-token'); 143 | expect(getAccessTokenMock).toHaveBeenCalledTimes(1); 144 | }); 145 | 146 | test('Verify cached token expiration', async () => { 147 | const getAccessTokenMock = jest.fn(() => ({ 148 | payload: { 149 | access_token: 'my-access-token', 150 | refresh_token: 'my-refresh-token' 151 | } 152 | })); 153 | Ims.mockImplementation(() => ({ 154 | getAccessToken: getAccessTokenMock 155 | })); 156 | 157 | // Initial call that should 158 | await expect(generateAccessToken('', process.env.IMS_CLIENT_ID, 'client-secret', 'adobeid', mockLoggerInstance)) 159 | .resolves.toBe('my-access-token'); 160 | expect(getAccessTokenMock).toHaveBeenCalledTimes(0); 161 | 162 | // Move time forward by 11 minutes 163 | jest 164 | .useFakeTimers() 165 | .setSystemTime(new Date(Date.now() + 1000 * 60 * 11)); 166 | 167 | await expect(generateAccessToken('', process.env.IMS_CLIENT_ID, 'client-secret', 'adobeid', mockLoggerInstance)) 168 | .resolves.toBe('my-access-token'); 169 | expect(getAccessTokenMock).toHaveBeenCalledTimes(1); 170 | 171 | // Reset system time 172 | jest.useRealTimers(); 173 | }); 174 | 175 | test('Verify checking that provided IMS access token is a service token with no required scopes', () => { 176 | getTokenData.mockImplementation(() => ({ 177 | user_id: 'service-account@AdobeService' 178 | })); 179 | 180 | expect(isValidServiceToken('fake-token')).toBe(true); 181 | }); 182 | 183 | test('Verify checking that provided IMS access token is a service token due to the system scope', () => { 184 | getTokenData.mockImplementation(() => ({ 185 | user_id: 'service-account@AdobeID', 186 | scope: 'system' 187 | })); 188 | 189 | expect(isValidServiceToken('fake-token')).toBe(true); 190 | }); 191 | 192 | test('Verify checking that provided IMS access token is not a service token', () => { 193 | getTokenData.mockImplementation(() => ({ 194 | user_id: 'service-account@AdobeID', 195 | scope: 'not-the-scope' 196 | })); 197 | 198 | expect(isValidServiceToken('fake-token')).toBe(false); 199 | }); 200 | 201 | test('Verify checking that provided IMS access token is a service token, but does not have required scopes', () => { 202 | getTokenData.mockImplementation(() => ({ 203 | user_id: 'service-account@AdobeService', 204 | scope: 'not-the-scope' 205 | })); 206 | 207 | expect(isValidServiceToken('fake-token', ['that-scope'])).toBe(false); 208 | }); 209 | 210 | test('Verify checking that provided IMS access token is a service token and has access to multiple scopes', () => { 211 | getTokenData.mockImplementation(() => ({ 212 | user_id: 'service-account@AdobeService', 213 | scope: 'not-the-scope,the-other-scope' 214 | })); 215 | 216 | expect(isValidServiceToken('fake-token', ['that-scope', 'the-other-scope'])).toBe(false); 217 | }); 218 | }); 219 | -------------------------------------------------------------------------------- /test/fixtures/list/response.orderBy.names.asc.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates?orderBy=names" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "_links": { 12 | "self": { 13 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-1" 14 | } 15 | }, 16 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 17 | "author": "Adobe Inc.", 18 | "name": "@author/app-builder-template-1", 19 | "description": "A template for testing purposes", 20 | "latestVersion": "1.0.0", 21 | "publishDate": "2022-05-01T03:50:39.658Z", 22 | "apis": [{ 23 | "code": "AnalyticsSDK", 24 | "credentials": "OAuth" 25 | }, { 26 | "code": "CampaignStandard" 27 | }, { 28 | "code": "Runtime" 29 | }, { 30 | "code": "Events", 31 | "hooks": [{ 32 | "postdeploy": "some command" 33 | }] 34 | }, { 35 | "code": "Mesh", 36 | "endpoints": [{ 37 | "my-action": "https://some-action.com/action" 38 | }] 39 | }], 40 | "adobeRecommended": false, 41 | "keywords": ["aio", "adobeio", "app", "templates", "aio-app-builder-template"], 42 | "status": "Approved", 43 | "links": { 44 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1", 45 | "github": "https://github.com/author/app-builder-template-1" 46 | }, 47 | "extensions": [ 48 | { 49 | "extensionPointId": "dx/excshell/1" 50 | } 51 | ], 52 | "categories": ["action", "ui"], 53 | "runtime": true 54 | }, 55 | { 56 | "_links": { 57 | "self": { 58 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-2" 59 | } 60 | }, 61 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 62 | "author": "Adobe Inc.", 63 | "name": "@author/app-builder-template-2", 64 | "description": "A template for testing purposes", 65 | "latestVersion": "1.0.1", 66 | "publishDate": "2022-05-01T03:50:39.658Z", 67 | "apis": [{ 68 | "code": "Events", 69 | "hooks": [{ 70 | "postdeploy": "some command" 71 | }] 72 | }, { 73 | "code": "Mesh", 74 | "endpoints": [{ 75 | "my-action": "https://some-action.com/action" 76 | }] 77 | }], 78 | "adobeRecommended": true, 79 | "keywords": ["aio", "adobeio", "app", "templates", "aio-app-builder-template"], 80 | "status": "Approved", 81 | "links": { 82 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2", 83 | "github": "https://github.com/author/app-builder-template-2" 84 | }, 85 | "extensions": [ 86 | { 87 | "extensionPointId": "dx/asset-compute/worker/1" 88 | } 89 | ], 90 | "categories": ["events"], 91 | "runtime": true, 92 | "event": {} 93 | }, 94 | { 95 | "_links": { 96 | "self": { 97 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-3" 98 | } 99 | }, 100 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 101 | "author": "Adobe Inc.", 102 | "name": "@author/app-builder-template-3", 103 | "description": "A template for testing purposes", 104 | "latestVersion": "1.0.1", 105 | "publishDate": "2022-05-01T03:50:39.658Z", 106 | "apis": [{ 107 | "code": "CampaignStandard" 108 | }], 109 | "adobeRecommended": true, 110 | "keywords": ["aio", "adobeio", "app", "templates", "aio-app-builder-template"], 111 | "status": "Approved", 112 | "links": { 113 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3", 114 | "github": "https://github.com/author/app-builder-template-3" 115 | }, 116 | "categories": ["ui"], 117 | "runtime": false 118 | }, 119 | { 120 | "_links": { 121 | "self": { 122 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-4" 123 | }, 124 | "review": { 125 | "href": "https://github.com/adobe/aio-templates/issues/100", 126 | "description": "A link to the \"Template Review Request\" Github issue." 127 | } 128 | }, 129 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 130 | "name": "@author/app-builder-template-4", 131 | "status": "InVerification", 132 | "links": { 133 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 134 | "github": "https://github.com/author/app-builder-template-4" 135 | } 136 | }, 137 | { 138 | "_links": { 139 | "self": { 140 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-5" 141 | }, 142 | "review": { 143 | "href": "https://github.com/adobe/aio-templates/issues/100", 144 | "description": "A link to the \"Template Review Request\" Github issue." 145 | } 146 | }, 147 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 148 | "name": "@author/app-builder-template-5", 149 | "status": "Rejected", 150 | "links": { 151 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 152 | "github": "https://github.com/author/app-builder-template-5" 153 | } 154 | }, 155 | { 156 | "_links": { 157 | "self": { 158 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-6" 159 | } 160 | }, 161 | "id": "d1nc1000-f32e-4472-a3ec-9b2f3ef6ac48", 162 | "author": "Adobe Inc.", 163 | "name": "@author/app-builder-template-6", 164 | "description": "A template for testing purposes", 165 | "latestVersion": "1.0.1", 166 | "publishDate": "2022-06-11T04:50:39.658Z", 167 | "adobeRecommended": false, 168 | "keywords": [ 169 | "aio", 170 | "adobeio", 171 | "app", 172 | "templates", 173 | "aio-app-builder-template" 174 | ], 175 | "status": "Approved", 176 | "links": { 177 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-6", 178 | "github": "https://github.com/author/app-builder-template-6" 179 | }, 180 | "extensions": [ 181 | { 182 | "extensionPointId": "dx/asset-compute/worker/1" 183 | } 184 | ], 185 | "categories": [ 186 | "ui", 187 | "helper-template" 188 | ], 189 | "runtime": true 190 | }] 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /actions/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | // ERR_RC - Error Response Code 13 | const ERR_RC_SERVER_ERROR = 'server_error'; 14 | const ERR_RC_HTTP_METHOD_NOT_ALLOWED = 'http_method_not_allowed'; 15 | const ERR_RC_MISSING_REQUIRED_HEADER = 'missing_required_header'; 16 | const ERR_RC_MISSING_REQUIRED_PARAMETER = 'missing_required_parameter'; 17 | const ERR_RC_INCORRECT_REQUEST = 'incorrect_request'; 18 | const ERR_RC_INVALID_IMS_ACCESS_TOKEN = 'invalid_ims_access_token'; 19 | const ERR_RC_PERMISSION_DENIED = 'permission_denied'; 20 | const ERR_RC_INVALID_TEMPLATE_ID = 'invalid_template_id'; 21 | 22 | /** 23 | * 24 | * Returns a log ready string of the action input parameters. 25 | * Any sensitive content will be replaced by ''. 26 | * 27 | * @param {object} params action input parameters. 28 | * @returns {string} a stringified version of the input parameters. 29 | */ 30 | function stringParameters (params) { 31 | const newParams = { 32 | ...params, 33 | ...(params.IMS_AUTH_CODE && { IMS_AUTH_CODE: '' }), 34 | ...(params.IMS_CLIENT_SECRET && { IMS_CLIENT_SECRET: '' }), 35 | ...(params.GITHUB_ACCESS_TOKEN && { GITHUB_ACCESS_TOKEN: '' }), 36 | ...(params.MONGODB_URI && { MONGODB_URI: '' }) 37 | }; 38 | 39 | // hide authorization token without overriding params 40 | let headers = params.__ow_headers || {}; 41 | if (headers.authorization) { 42 | headers = { 43 | ...headers, 44 | authorization: '', 45 | cookie: '' 46 | }; 47 | } 48 | return JSON.stringify({ ...newParams, __ow_headers: headers }); 49 | } 50 | 51 | /** 52 | * 53 | * Returns the list of missing keys giving an object and its required keys. 54 | * A parameter is missing if its value is undefined or ''. 55 | * A value of 0 or null is not considered as missing. 56 | * 57 | * @param {object} obj object to check. 58 | * @param {array} required list of required keys. 59 | * Each element can be multi level deep using a '.' separator e.g. 'myRequiredObj.myRequiredKey' 60 | * 61 | * @returns {array} 62 | * @private 63 | */ 64 | function getMissingKeys (obj, required) { 65 | return required.filter(r => { 66 | const splits = r.split('.'); 67 | const last = splits[splits.length - 1]; 68 | const traverse = splits.slice(0, -1).reduce((tObj, split) => { tObj = (tObj[split] || {}); return tObj; }, obj); 69 | return traverse[last] === undefined || traverse[last] === ''; // missing default params are empty string 70 | }); 71 | } 72 | 73 | /** 74 | * 75 | * Returns the list of missing keys giving an object and its required keys. 76 | * A parameter is missing if its value is undefined or ''. 77 | * A value of 0 or null is not considered as missing. 78 | * 79 | * @param {object} params action input parameters. 80 | * @param {Array} requiredParams list of required input parameters. 81 | * Each element can be multi level deep using a '.' separator e.g. 'myRequiredObj.myRequiredKey'. 82 | * @param {Array} requiredHeaders list of required input headers. 83 | * @returns {Array} if the return value is not null, then it holds an array of error object messages describing the missing inputs. 84 | */ 85 | function checkMissingRequestInputs (params, requiredParams = [], requiredHeaders = []) { 86 | const errorMessages = []; 87 | 88 | // input headers are always lowercase 89 | requiredHeaders = requiredHeaders.map(h => h.toLowerCase()); 90 | // check for missing headers 91 | const missingHeaders = getMissingKeys(params.__ow_headers || {}, requiredHeaders); 92 | if (missingHeaders.length > 0) { 93 | missingHeaders.forEach( 94 | header => errorMessages.push( 95 | errorMessage(ERR_RC_MISSING_REQUIRED_HEADER, `The "${header}" header is not set.`) 96 | ) 97 | ); 98 | } 99 | 100 | // check for missing parameters 101 | const missingParams = getMissingKeys(params, requiredParams); 102 | if (missingParams.length > 0) { 103 | missingParams.forEach( 104 | param => errorMessages.push( 105 | errorMessage(ERR_RC_MISSING_REQUIRED_PARAMETER, `The "${param}" parameter is not set.`) 106 | ) 107 | ); 108 | } 109 | 110 | return errorMessages.length > 0 ? errorMessages : null; 111 | } 112 | 113 | /** 114 | * 115 | * Extracts the bearer token string from the Authorization header in the request parameters. 116 | * 117 | * @param {object} params action input parameters. 118 | * @returns {string|undefined} the token string or undefined if not set in request headers. 119 | */ 120 | function getBearerToken (params) { 121 | if (params.__ow_headers && 122 | params.__ow_headers.authorization && 123 | params.__ow_headers.authorization.startsWith('Bearer ')) { 124 | return params.__ow_headers.authorization.substring('Bearer '.length); 125 | } 126 | return undefined; 127 | } 128 | 129 | /** 130 | * Get header value from the given request parameters. 131 | * @param {object} params action input parameters. 132 | * @param {string} key the header key to get the value from. 133 | * @returns {string|undefined} the token string or undefined if not set in request headers. 134 | */ 135 | function getHeaderValue (params, key) { 136 | if (params.__ow_headers && params.__ow_headers[key]) { 137 | return params.__ow_headers[key]; 138 | } 139 | return undefined; 140 | } 141 | 142 | /** 143 | * Returns the environment based on the apiHost. 144 | * @param {object} logger the logger instance. 145 | * @returns {string} the environment. 146 | */ 147 | function getEnv (logger) { 148 | // set env based on apiHost 149 | const apiHost = process.env.__OW_API_HOST; 150 | logger.debug('apiHost:', apiHost); 151 | const env = apiHost?.includes('prod') ? 'prod' : 'stage'; 152 | logger.debug('env: ', env); 153 | return env; 154 | } 155 | 156 | /** 157 | * 158 | * Returns an error response object and attempts to log.info the status code and error message(s). 159 | * 160 | * @param {number} statusCode the error status code. 161 | * e.g. 400 162 | * @param {Array} messages an array of error object messages. 163 | * e.g. [{"code":"missing_required_parameter","message":"The \"XXX\" parameter is not set."}] 164 | * @param {*} [logger] an optional logger instance object with an `info` method 165 | * e.g. `new require('@adobe/aio-sdk').Core.Logger('name')` 166 | * @returns {object} the error object, ready to be returned from the action main's function. 167 | */ 168 | function errorResponse (statusCode, messages, logger) { 169 | if (logger && typeof logger.info === 'function') { 170 | logger.info(`Status code: ${statusCode}`); 171 | messages.forEach( 172 | item => logger.info(`${item.code}: ${item.message}`) 173 | ); 174 | } 175 | return { 176 | error: { 177 | statusCode, 178 | body: { 179 | errors: messages 180 | } 181 | } 182 | }; 183 | } 184 | 185 | /** 186 | * Returns an error message object. 187 | * 188 | * @param {string} code error response code 189 | * @param {string} message error message 190 | * @returns {object} error message object 191 | */ 192 | function errorMessage (code, message) { 193 | return { 194 | code, 195 | message 196 | }; 197 | } 198 | 199 | /** 200 | * Converts MongoDB ObjectId(s) within an object or an array of objects to strings. 201 | * @param {object | Array} input - The input object or array of objects containing MongoDB ObjectId(s). 202 | * @returns {object | Array} - Returns the input object or array of objects with MongoDB ObjectId(s) converted to strings. 203 | */ 204 | function convertMongoIdToString (input) { 205 | if (Array.isArray(input)) { 206 | input.forEach(obj => { 207 | if (obj._id) { 208 | obj.id = obj._id.toString(); 209 | delete obj._id; 210 | } 211 | }); 212 | } else if (input && typeof input === 'object' && input._id) { 213 | input.id = input._id.toString(); 214 | delete input._id; 215 | } 216 | return input; 217 | } 218 | 219 | module.exports = { 220 | errorResponse, 221 | getBearerToken, 222 | stringParameters, 223 | checkMissingRequestInputs, 224 | errorMessage, 225 | convertMongoIdToString, 226 | ERR_RC_SERVER_ERROR, 227 | ERR_RC_HTTP_METHOD_NOT_ALLOWED, 228 | ERR_RC_MISSING_REQUIRED_HEADER, 229 | ERR_RC_MISSING_REQUIRED_PARAMETER, 230 | ERR_RC_INCORRECT_REQUEST, 231 | ERR_RC_INVALID_IMS_ACCESS_TOKEN, 232 | ERR_RC_PERMISSION_DENIED, 233 | ERR_RC_INVALID_TEMPLATE_ID, 234 | getHeaderValue, 235 | getEnv 236 | }; 237 | -------------------------------------------------------------------------------- /actions/templateRegistry.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const axios = require('axios').default; 13 | const { Octokit } = require('@octokit/rest'); 14 | 15 | const collectionName = 'templates'; 16 | 17 | const TEMPLATE_STATUS_IN_VERIFICATION = 'InVerification'; 18 | const TEMPLATE_STATUS_APPROVED = 'Approved'; 19 | const TEMPLATE_STATUS_REJECTED = 'Rejected'; 20 | 21 | let openReviewIssues = null; 22 | 23 | const { ObjectId } = require('mongodb'); 24 | const { mongoConnection } = require('../db/mongo'); 25 | const { convertMongoIdToString } = require('./utils'); 26 | 27 | /** 28 | * Returns a template record from Template Registry by a template id. 29 | * 30 | * @param {object} dbParams database connection parameters 31 | * @param {string} templateId template id 32 | * @returns {Promise} an existing template record or null 33 | */ 34 | async function findTemplateById (dbParams, templateId) { 35 | const collection = await mongoConnection(dbParams, collectionName); 36 | const result = await collection.findOne({ _id: new ObjectId(templateId) }); 37 | return result ? convertMongoIdToString(result) : null; 38 | } 39 | 40 | /** 41 | * Updates a template to Template Registry. 42 | * 43 | * @param {object} dbParams database connection parameters 44 | * @param {string} templateId template Id 45 | * @param {object} templateBody template data 46 | * @returns {object} mongo response 47 | */ 48 | async function updateTemplate (dbParams, templateId, templateBody) { 49 | const collection = await mongoConnection(dbParams, collectionName); 50 | 51 | const response = await collection.updateOne({ _id: new ObjectId(templateId) }, { 52 | $set: templateBody 53 | }); 54 | 55 | return response; 56 | /** 57 | * { "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 } 58 | */ 59 | } 60 | 61 | /** 62 | * Returns a template record from Template Registry by a template name. 63 | * 64 | * @param {object} dbParams database connection parameters 65 | * @param {string} templateName template name 66 | * @returns {Promise} an existing template record or null 67 | */ 68 | async function findTemplateByName (dbParams, templateName) { 69 | const collection = await mongoConnection(dbParams, collectionName); 70 | const results = await collection.find({ name: templateName }).toArray(); 71 | return results?.length ? convertMongoIdToString(results[0]) : null; 72 | } 73 | 74 | /** 75 | * Adds a template to Template Registry. 76 | * 77 | * @param {object} dbParams database connection parameters 78 | * @param {object} body template data 79 | * @returns {object} a newly created template 80 | */ 81 | async function addTemplate (dbParams, body) { 82 | const collection = await mongoConnection(dbParams, collectionName); 83 | const template = { 84 | ...body, 85 | status: body.status ? body.status : TEMPLATE_STATUS_IN_VERIFICATION, 86 | adobeRecommended: body.adobeRecommended ? body.adobeRecommended : false 87 | }; 88 | 89 | // Only add npm link if github link is provided 90 | if (template.links?.github) { 91 | template.links.npm = `https://www.npmjs.com/package/${body.name}`; 92 | } 93 | 94 | const result = await collection.insertOne(template); 95 | const output = { ...template, id: result?.insertedId?.toString() }; 96 | return convertMongoIdToString(output); 97 | } 98 | 99 | /** 100 | * Removes a template from Template Registry. 101 | * 102 | * @param {object} dbParams database connection parameters 103 | * @param {string} templateName template name 104 | * @returns {object} response 105 | */ 106 | async function removeTemplateByName (dbParams, templateName) { 107 | const collection = await mongoConnection(dbParams, collectionName); 108 | const response = await collection.deleteOne({ name: templateName }); 109 | return response; 110 | } 111 | 112 | /** 113 | * Removes a template from Template Registry. 114 | * 115 | * @param {object} dbParams database connection parameters 116 | * @param {string} templateId template id 117 | * @returns {object} response 118 | */ 119 | async function removeTemplateById (dbParams, templateId) { 120 | const collection = await mongoConnection(dbParams, collectionName); 121 | const response = await collection.deleteOne({ _id: new ObjectId(templateId) }); 122 | return response; 123 | } 124 | 125 | /** 126 | * Creates a Template Review Request issue. 127 | * 128 | * @param {string} templateName template name 129 | * @param {string} githubRepoUrl Github repo URL 130 | * @param {string} githubAccessToken Github access token 131 | * @param {string} templateRegistryOrg Template Registry organization name 132 | * @param {string} templateRegistryRepository Template Registry repository name 133 | * @returns {Promise} created issue number 134 | */ 135 | async function createReviewIssue (templateName, githubRepoUrl, githubAccessToken, templateRegistryOrg, templateRegistryRepository) { 136 | const octokit = new Octokit({ 137 | auth: githubAccessToken 138 | }); 139 | const response = await octokit.rest.issues.create({ 140 | owner: templateRegistryOrg, 141 | repo: templateRegistryRepository, 142 | title: `Add ${templateName}`, 143 | labels: ['add-template', 'template-registry-api'], 144 | body: `### Link to GitHub repo\n${githubRepoUrl}\n### npm package name\n${templateName}` 145 | }); 146 | return response.data.number; 147 | } 148 | 149 | /** 150 | * Finds an open "Template Review Request" issue by a template name if any. 151 | * 152 | * @param {string} templateName template name 153 | * @param {string} templateRegistryOrg Template Registry organization name 154 | * @param {string} templateRegistryRepository Template Registry repository name 155 | * @returns {Promise} an open "Template Review Request" issue or null 156 | */ 157 | async function getReviewIssueByTemplateName (templateName, templateRegistryOrg, templateRegistryRepository) { 158 | const issues = await getOpenReviewIssues(templateRegistryOrg, templateRegistryRepository); 159 | const reviewIssue = issues.find(item => item.body.endsWith(templateName)); 160 | return (reviewIssue !== undefined) ? reviewIssue.html_url : null; 161 | } 162 | 163 | /** 164 | * Finds all open "Template Review Request" issues. 165 | * 166 | * @param {string} templateRegistryOrg Template Registry organization name 167 | * @param {string} templateRegistryRepository Template Registry repository name 168 | * @returns {Array} An array of open "Template Review Request" issues 169 | * @private 170 | */ 171 | async function getOpenReviewIssues (templateRegistryOrg, templateRegistryRepository) { 172 | if (openReviewIssues === null) { 173 | openReviewIssues = await fetchUrl( 174 | `https://api.github.com/repos/${templateRegistryOrg}/${templateRegistryRepository}/issues?state=open&labels=add-template&sort=updated-desc` 175 | ); 176 | } 177 | return openReviewIssues; 178 | } 179 | 180 | /** 181 | * Returns Template Registry records. 182 | * 183 | * @param {object} dbParams database connection parameters 184 | * @returns {Promise} existing Template Registry records 185 | */ 186 | async function getTemplates (dbParams) { 187 | const collection = await mongoConnection(dbParams, collectionName); 188 | const results = await collection.find({}).toArray(); 189 | const result = results?.length ? convertMongoIdToString(results) : []; 190 | return result; 191 | } 192 | 193 | /** 194 | * @param {string} url URL of a resource to fetch 195 | * @param {object} headers headers to be set if any 196 | * @param {object} params params to be set if any 197 | * @returns {Promise} response data 198 | */ 199 | async function fetchUrl (url, headers = {}, params = {}) { 200 | return new Promise((resolve, reject) => { 201 | axios({ 202 | method: 'get', 203 | url, 204 | headers, 205 | params 206 | }) 207 | .then(response => { 208 | if (response.status === 200) { 209 | resolve(response.data); 210 | } else { 211 | const error = `Error fetching "${url}". Response code is ${response.status}`; 212 | reject(new Error(error)); 213 | } 214 | }) 215 | .catch(e => { 216 | const error = `Error fetching "${url}". ${e.toString()}`; 217 | reject(new Error(error)); 218 | }); 219 | }); 220 | } 221 | 222 | module.exports = { 223 | fetchUrl, 224 | getTemplates, 225 | findTemplateByName, 226 | findTemplateById, 227 | addTemplate, 228 | removeTemplateByName, 229 | createReviewIssue, 230 | updateTemplate, 231 | getReviewIssueByTemplateName, 232 | TEMPLATE_STATUS_IN_VERIFICATION, 233 | TEMPLATE_STATUS_APPROVED, 234 | TEMPLATE_STATUS_REJECTED, 235 | removeTemplateById 236 | }; 237 | -------------------------------------------------------------------------------- /test/fixtures/list/response.full.no-review-issues.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "_links": { 12 | "self": { 13 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-1" 14 | } 15 | }, 16 | "adobeRecommended": false, 17 | "author": "Adobe Inc.", 18 | "categories": [ 19 | "action", 20 | "ui" 21 | ], 22 | "description": "A template for testing purposes", 23 | "extensions": [ 24 | { 25 | "extensionPointId": "dx/excshell/1" 26 | } 27 | ], 28 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 29 | "keywords": [ 30 | "aio", 31 | "adobeio", 32 | "app", 33 | "templates", 34 | "aio-app-builder-template" 35 | ], 36 | "latestVersion": "1.0.0", 37 | "links": { 38 | "github": "https://github.com/author/app-builder-template-1", 39 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1" 40 | }, 41 | "name": "@author/app-builder-template-1", 42 | "publishDate": "2022-05-01T03:50:39.658Z", 43 | "apis": [ 44 | { 45 | "code": "AnalyticsSDK", 46 | "credentials": "OAuth" 47 | }, 48 | { 49 | "code": "CampaignStandard" 50 | }, 51 | { 52 | "code": "Runtime" 53 | }, 54 | { 55 | "code": "Events", 56 | "hooks": [ 57 | { 58 | "postdeploy": "some command" 59 | } 60 | ] 61 | }, 62 | { 63 | "code": "Mesh", 64 | "endpoints": [ 65 | { 66 | "my-action": "https://some-action.com/action" 67 | } 68 | ] 69 | } 70 | ], 71 | "status": "Approved", 72 | "runtime": true 73 | }, 74 | { 75 | "_links": { 76 | "self": { 77 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-2" 78 | } 79 | }, 80 | "adobeRecommended": true, 81 | "author": "Adobe Inc.", 82 | "categories": [ 83 | "events" 84 | ], 85 | "description": "A template for testing purposes", 86 | "extensions": [ 87 | { 88 | "extensionPointId": "dx/asset-compute/worker/1" 89 | } 90 | ], 91 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 92 | "keywords": [ 93 | "aio", 94 | "adobeio", 95 | "app", 96 | "templates", 97 | "aio-app-builder-template" 98 | ], 99 | "latestVersion": "1.0.1", 100 | "links": { 101 | "github": "https://github.com/author/app-builder-template-2", 102 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2" 103 | }, 104 | "name": "@author/app-builder-template-2", 105 | "publishDate": "2022-05-01T03:50:39.658Z", 106 | "apis": [ 107 | { 108 | "code": "Events", 109 | "hooks": [ 110 | { 111 | "postdeploy": "some command" 112 | } 113 | ] 114 | }, 115 | { 116 | "code": "Mesh", 117 | "endpoints": [ 118 | { 119 | "my-action": "https://some-action.com/action" 120 | } 121 | ] 122 | } 123 | ], 124 | "status": "Approved", 125 | "runtime": true, 126 | "event": {} 127 | }, 128 | { 129 | "_links": { 130 | "self": { 131 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-3" 132 | } 133 | }, 134 | "adobeRecommended": true, 135 | "author": "Adobe Inc.", 136 | "categories": [ 137 | "ui" 138 | ], 139 | "description": "A template for testing purposes", 140 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 141 | "keywords": [ 142 | "aio", 143 | "adobeio", 144 | "app", 145 | "templates", 146 | "aio-app-builder-template" 147 | ], 148 | "latestVersion": "1.0.1", 149 | "links": { 150 | "github": "https://github.com/author/app-builder-template-3", 151 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3" 152 | }, 153 | "name": "@author/app-builder-template-3", 154 | "publishDate": "2022-05-01T03:50:39.658Z", 155 | "apis": [ 156 | { 157 | "code": "CampaignStandard" 158 | } 159 | ], 160 | "status": "Approved", 161 | "runtime": false 162 | }, 163 | { 164 | "_links": { 165 | "self": { 166 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-4" 167 | } 168 | }, 169 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 170 | "name": "@author/app-builder-template-4", 171 | "status": "InVerification", 172 | "links": { 173 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 174 | "github": "https://github.com/author/app-builder-template-4" 175 | } 176 | }, 177 | { 178 | "_links": { 179 | "self": { 180 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-5" 181 | } 182 | }, 183 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 184 | "name": "@author/app-builder-template-5", 185 | "status": "Rejected", 186 | "links": { 187 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 188 | "github": "https://github.com/author/app-builder-template-5" 189 | } 190 | }, 191 | { 192 | "_links": { 193 | "self": { 194 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-6" 195 | } 196 | }, 197 | "adobeRecommended": false, 198 | "author": "Adobe Inc.", 199 | "categories": [ 200 | "ui", 201 | "helper-template" 202 | ], 203 | "description": "A template for testing purposes", 204 | "id": "d1nc1000-f32e-4472-a3ec-9b2f3ef6ac48", 205 | "keywords": [ 206 | "aio", 207 | "adobeio", 208 | "app", 209 | "templates", 210 | "aio-app-builder-template" 211 | ], 212 | "latestVersion": "1.0.1", 213 | "links": { 214 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-6", 215 | "github": "https://github.com/author/app-builder-template-6" 216 | }, 217 | "name": "@author/app-builder-template-6", 218 | "publishDate": "2022-06-11T04:50:39.658Z", 219 | "extensions": [ 220 | { 221 | "extensionPointId": "dx/asset-compute/worker/1" 222 | } 223 | ], 224 | "status": "Approved", 225 | "runtime": true 226 | } 227 | ] 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const utils = require('./../actions/utils.js'); 13 | 14 | test('interface', () => { 15 | expect(typeof utils.errorResponse).toBe('function'); 16 | expect(typeof utils.stringParameters).toBe('function'); 17 | expect(typeof utils.checkMissingRequestInputs).toBe('function'); 18 | expect(typeof utils.getBearerToken).toBe('function'); 19 | }); 20 | 21 | describe('errorResponse', () => { 22 | test('(400, errorMessages)', () => { 23 | const errors = [{ 24 | code: utils.ERR_RC_MISSING_REQUIRED_PARAMETER, 25 | message: 'The "a" parameter is not set.' 26 | }]; 27 | const res = utils.errorResponse(400, errors); 28 | expect(res).toEqual({ 29 | error: { 30 | statusCode: 400, 31 | body: { errors } 32 | } 33 | }); 34 | }); 35 | 36 | test('(400, errorMessages, logger)', () => { 37 | const logger = { 38 | info: jest.fn() 39 | }; 40 | const errors = [{ 41 | code: utils.ERR_RC_MISSING_REQUIRED_PARAMETER, 42 | message: 'The "a" parameter is not set.' 43 | }]; 44 | const res = utils.errorResponse(400, errors, logger); 45 | expect(logger.info).toHaveBeenCalledWith('Status code: 400'); 46 | expect(logger.info).toHaveBeenCalledWith('missing_required_parameter: The "a" parameter is not set.'); 47 | expect(res).toEqual({ 48 | error: { 49 | statusCode: 400, 50 | body: { errors } 51 | } 52 | }); 53 | }); 54 | }); 55 | 56 | describe('stringParameters', () => { 57 | test('no auth header', () => { 58 | const params = { 59 | a: 1, b: 2, __ow_headers: { 'x-api-key': 'fake-api-key' } 60 | }; 61 | expect(utils.stringParameters(params)).toEqual(JSON.stringify(params)); 62 | }); 63 | test('with auth header', () => { 64 | const params = { 65 | a: 1, b: 2, __ow_headers: { 'x-api-key': 'fake-api-key', authorization: 'secret' } 66 | }; 67 | expect(utils.stringParameters(params)).toEqual(expect.stringContaining('"authorization":""')); 68 | expect(utils.stringParameters(params)).not.toEqual(expect.stringContaining('secret')); 69 | }); 70 | test('with sensitive values in params', () => { 71 | const params = { 72 | IMS_AUTH_CODE: '1234', 73 | MONGODB_URI: 'mongodb://mongo:27017', 74 | GITHUB_ACCESS_TOKEN: 'fake-token', 75 | IMS_CLIENT_SECRET: 'secret' 76 | }; 77 | expect(utils.stringParameters(params)).toEqual(expect.stringContaining( 78 | '"IMS_AUTH_CODE":"","MONGODB_URI":"","GITHUB_ACCESS_TOKEN":"","IMS_CLIENT_SECRET":""' 79 | )); 80 | }); 81 | test('with only some sensitive values in params', () => { 82 | const params = { 83 | IMS_AUTH_CODE: '1234', 84 | MONGODB_URI: 'mongodb://mongo:27017', 85 | GITHUB_ACCESS_TOKEN: 'fake-token' 86 | }; 87 | const stringParams = utils.stringParameters(params); 88 | expect(stringParams).toEqual(expect.stringContaining( 89 | '"IMS_AUTH_CODE":"","MONGODB_URI":"","GITHUB_ACCESS_TOKEN":""' 90 | )); 91 | expect(stringParams).toEqual(expect.not.stringContaining('IMS_CLIENT_SECRET')); 92 | }); 93 | }); 94 | 95 | describe('checkMissingRequestInputs', () => { 96 | test('({ a: 1, b: 2 }, [a])', () => { 97 | expect(utils.checkMissingRequestInputs({ a: 1, b: 2 }, ['a'])).toEqual(null); 98 | }); 99 | test('({ a: 1 }, [a, b])', () => { 100 | expect(utils.checkMissingRequestInputs({ a: 1 }, ['a', 'b'])).toEqual([{ 101 | code: utils.ERR_RC_MISSING_REQUIRED_PARAMETER, 102 | message: 'The "b" parameter is not set.' 103 | }]); 104 | }); 105 | test('({ a: { b: { c: 1 } }, f: { g: 2 } }, [a.b.c, f.g.h.i])', () => { 106 | expect(utils.checkMissingRequestInputs({ a: { b: { c: 1 } }, f: { g: 2 } }, ['a.b.c', 'f.g.h.i'])).toEqual([{ 107 | code: utils.ERR_RC_MISSING_REQUIRED_PARAMETER, 108 | message: 'The "f.g.h.i" parameter is not set.' 109 | }]); 110 | }); 111 | test('({ a: { b: { c: 1 } }, f: { g: 2 } }, [a.b.c, f.g.h])', () => { 112 | expect(utils.checkMissingRequestInputs({ a: { b: { c: 1 } }, f: { g: 2 } }, ['a.b.c', 'f'])).toEqual(null); 113 | }); 114 | test('({ a: 1, __ow_headers: { h: 1, i: 2 } }, undefined, [h])', () => { 115 | expect(utils.checkMissingRequestInputs({ a: 1, __ow_headers: { h: 1, i: 2 } }, undefined, ['h'])).toEqual(null); 116 | }); 117 | test('({ a: 1, __ow_headers: { f: 2 } }, [a], [h, i])', () => { 118 | expect(utils.checkMissingRequestInputs({ a: 1, __ow_headers: { f: 2 } }, ['a'], ['h', 'i'])).toEqual([{ 119 | code: utils.ERR_RC_MISSING_REQUIRED_HEADER, 120 | message: 'The "h" header is not set.' 121 | }, 122 | { 123 | code: utils.ERR_RC_MISSING_REQUIRED_HEADER, 124 | message: 'The "i" header is not set.' 125 | }]); 126 | }); 127 | test('({ c: 1, __ow_headers: { f: 2 } }, [a, b], [h, i])', () => { 128 | expect(utils.checkMissingRequestInputs({ c: 1 }, ['a', 'b'], ['h', 'i'])).toEqual([{ 129 | code: utils.ERR_RC_MISSING_REQUIRED_HEADER, 130 | message: 'The "h" header is not set.' 131 | }, 132 | { 133 | code: utils.ERR_RC_MISSING_REQUIRED_HEADER, 134 | message: 'The "i" header is not set.' 135 | }, 136 | { 137 | code: utils.ERR_RC_MISSING_REQUIRED_PARAMETER, 138 | message: 'The "a" parameter is not set.' 139 | }, { 140 | code: utils.ERR_RC_MISSING_REQUIRED_PARAMETER, 141 | message: 'The "b" parameter is not set.' 142 | }]); 143 | }); 144 | test('({ a: 0 }, [a])', () => { 145 | expect(utils.checkMissingRequestInputs({ a: 0 }, ['a'])).toEqual(null); 146 | }); 147 | test('({ a: null }, [a])', () => { 148 | expect(utils.checkMissingRequestInputs({ a: null }, ['a'])).toEqual(null); 149 | }); 150 | test('({ a: \'\' }, [a])', () => { 151 | expect(utils.checkMissingRequestInputs({ a: '' }, ['a'])).toEqual([{ 152 | code: utils.ERR_RC_MISSING_REQUIRED_PARAMETER, 153 | message: 'The "a" parameter is not set.' 154 | }]); 155 | }); 156 | test('({ a: undefined }, [a])', () => { 157 | expect(utils.checkMissingRequestInputs({ a: undefined }, ['a'])).toEqual([{ 158 | code: utils.ERR_RC_MISSING_REQUIRED_PARAMETER, 159 | message: 'The "a" parameter is not set.' 160 | }]); 161 | }); 162 | }); 163 | 164 | describe('getBearerToken', () => { 165 | test('({})', () => { 166 | expect(utils.getBearerToken({})).toEqual(undefined); 167 | }); 168 | test('({ authorization: Bearer fake, __ow_headers: {} })', () => { 169 | expect(utils.getBearerToken({ authorization: 'Bearer fake', __ow_headers: {} })).toEqual(undefined); 170 | }); 171 | test('({ authorization: Bearer fake, __ow_headers: { authorization: fake } })', () => { 172 | expect(utils.getBearerToken({ authorization: 'Bearer fake', __ow_headers: { authorization: 'fake' } })).toEqual(undefined); 173 | }); 174 | test('({ __ow_headers: { authorization: Bearerfake} })', () => { 175 | expect(utils.getBearerToken({ __ow_headers: { authorization: 'Bearerfake' } })).toEqual(undefined); 176 | }); 177 | test('({ __ow_headers: { authorization: Bearer fake} })', () => { 178 | expect(utils.getBearerToken({ __ow_headers: { authorization: 'Bearer fake' } })).toEqual('fake'); 179 | }); 180 | test('({ __ow_headers: { authorization: Bearer fake Bearer fake} })', () => { 181 | expect(utils.getBearerToken({ __ow_headers: { authorization: 'Bearer fake Bearer fake' } })).toEqual('fake Bearer fake'); 182 | }); 183 | }); 184 | 185 | describe('convertMongoIdToString', () => { 186 | test('should convert _id field to string for single object', () => { 187 | const input = { _id: 123, name: 'Test' }; 188 | const expectedOutput = { id: '123', name: 'Test' }; 189 | expect(utils.convertMongoIdToString(input)).toEqual(expectedOutput); 190 | }); 191 | 192 | test('should convert _id field to string for array of objects', () => { 193 | const input = [{ _id: 123, name: 'Test1' }, { _id: 456, name: 'Test2' }]; 194 | const expectedOutput = [{ id: '123', name: 'Test1' }, { id: '456', name: 'Test2' }]; 195 | expect(utils.convertMongoIdToString(input)).toEqual(expectedOutput); 196 | }); 197 | 198 | test('should handle input with no _id field', () => { 199 | const input = { name: 'Test' }; 200 | const expectedOutput = { name: 'Test' }; 201 | expect(utils.convertMongoIdToString(input)).toEqual(expectedOutput); 202 | }); 203 | 204 | test('should handle empty array input', () => { 205 | const input = []; 206 | const expectedOutput = []; 207 | expect(utils.convertMongoIdToString(input)).toEqual(expectedOutput); 208 | }); 209 | 210 | test('should handle null input', () => { 211 | const input = null; 212 | const expectedOutput = null; 213 | expect(utils.convertMongoIdToString(input)).toEqual(expectedOutput); 214 | }); 215 | 216 | test('should handle undefined input', () => { 217 | const input = undefined; 218 | const expectedOutput = undefined; 219 | expect(utils.convertMongoIdToString(input)).toEqual(expectedOutput); 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /actions/templates/put/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 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 | Unless required by applicable law or agreed to in writing, software distributed under 7 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 8 | OF ANY KIND, either express or implied. See the License for the specific language 9 | governing permissions and limitations under the License. 10 | */ 11 | 12 | const { Core } = require('@adobe/aio-sdk'); 13 | const { errorResponse, errorMessage, stringParameters, checkMissingRequestInputs, getBearerToken, ERR_RC_SERVER_ERROR, ERR_RC_HTTP_METHOD_NOT_ALLOWED, ERR_RC_INCORRECT_REQUEST, ERR_RC_MISSING_REQUIRED_PARAMETER, getEnv } = 14 | require('../../utils'); 15 | const { generateAccessToken } = require('../../ims'); 16 | const { findTemplateById, updateTemplate } = require('../../templateRegistry'); 17 | const Enforcer = require('openapi-enforcer'); 18 | const consoleLib = require('@adobe/aio-lib-console'); 19 | const { incBatchCounter } = require('@adobe/aio-metrics-client'); 20 | const { setMetricsUrl, incErrorCounterMetrics } = require('../../metrics'); 21 | const { getTokenData } = require('@adobe/aio-lib-ims'); 22 | 23 | const HTTP_METHOD = 'put'; 24 | const ENDPOINT = 'PUT /templates/{templateId}'; 25 | const METRICS_KEY = 'recordtemplateregistrymetrics'; 26 | const PUT_PARAM_NAME = 'templateId'; 27 | 28 | const serializeRequestBody = (params) => { 29 | return { 30 | ...(params.description && { description: params.description }), // developer console only 31 | ...(params.latestVersion && { latestVersion: params.latestVersion }), // developer console only 32 | ...(params.updatedBy && { updatedBy: params.updatedBy }), 33 | ...('adobeRecommended' in params && { adobeRecommended: params.adobeRecommended }), 34 | ...('keywords' in params && params.keywords.length && { keywords: params.keywords }), 35 | ...('categories' in params && params.categories.length && { categories: params.categories }), 36 | ...('extensions' in params && params.extensions.length && { extensions: params.extensions }), 37 | ...('credentials' in params && params.credentials.length && { credentials: params.credentials }), 38 | ...('codeSamples' in params && params.codeSamples.length && { codeSamples: params.codeSamples }), 39 | ...('requestAccessAppId' in params && { requestAccessAppId: params.requestAccessAppId }), 40 | ...('apis' in params && params.apis.length && { apis: params.apis }), 41 | ...('status' in params && { status: params.status }), 42 | ...('runtime' in params && { runtime: params.runtime }), 43 | ...('links' in params && { links: params.links }) 44 | }; 45 | }; 46 | 47 | /** 48 | * Updates a new template in the Template Registry. 49 | * @param {object} params request parameters 50 | * @returns {object} response 51 | */ 52 | async function main (params) { 53 | // create a Logger 54 | const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }); 55 | 56 | const dbParams = { 57 | MONGODB_URI: params.MONGODB_URI, 58 | MONGODB_NAME: params.MONGODB_NAME 59 | }; 60 | 61 | let requester = 'unauth'; 62 | if (params?.METRICS_URL) { 63 | setMetricsUrl(params.METRICS_URL, METRICS_KEY); 64 | } 65 | 66 | try { 67 | // 'info' is the default level if not set 68 | logger.info('Calling "PUT templates"'); 69 | 70 | // log parameters, only if params.LOG_LEVEL === 'debug' 71 | logger.debug(stringParameters(params)); 72 | 73 | // check for missing request input parameters and headers 74 | const requiredHeaders = ['Authorization']; 75 | const errorMessages = checkMissingRequestInputs(params, [], requiredHeaders); 76 | if (errorMessages) { 77 | await incBatchCounter('request_count', requester, ENDPOINT); 78 | await incErrorCounterMetrics(requester, ENDPOINT, '400'); 79 | return errorResponse(401, errorMessages, logger); 80 | } 81 | 82 | // extract the user Bearer token from the Authorization header 83 | const accessToken = getBearerToken(params); 84 | requester = getTokenData(accessToken)?.user_id; 85 | await incBatchCounter('request_count', requester, ENDPOINT); 86 | 87 | if (params.__ow_method === undefined || params.__ow_method.toLowerCase() !== HTTP_METHOD) { 88 | await incErrorCounterMetrics(requester, ENDPOINT, '405'); 89 | return errorResponse(405, [errorMessage(ERR_RC_HTTP_METHOD_NOT_ALLOWED, `HTTP "${params.__ow_method}" method is unsupported.`)], logger); 90 | } 91 | 92 | const isTemplateIdValid = PUT_PARAM_NAME in params && typeof params[PUT_PARAM_NAME] === 'string' && params[PUT_PARAM_NAME].length > 0; 93 | if (!isTemplateIdValid) { 94 | await incErrorCounterMetrics(requester, ENDPOINT, '400'); 95 | return errorResponse(400, [errorMessage(ERR_RC_MISSING_REQUIRED_PARAMETER, `The "${PUT_PARAM_NAME}" parameter is not set.`)], logger); 96 | } 97 | 98 | Enforcer.v3_0.Schema.defineDataTypeFormat('string', 'uuid', null); 99 | Enforcer.v3_0.Schema.defineDataTypeFormat('string', 'uri', null); 100 | 101 | // WPAR002 - skip a warning about the "allowEmptyValue" property 102 | // see https://swagger.io/docs/specification/describing-parameters/ Empty-Valued and Nullable Parameters 103 | const openapi = await Enforcer('./template-registry-api.json', { componentOptions: { exceptionSkipCodes: ['WPAR002'] } }); 104 | 105 | let body = serializeRequestBody(params); 106 | 107 | const [req, reqError] = openapi.request({ 108 | method: HTTP_METHOD, 109 | path: `/templates/{${PUT_PARAM_NAME}}`, 110 | params: { 111 | templateId: params[PUT_PARAM_NAME] 112 | }, 113 | body 114 | }); 115 | if (reqError) { 116 | await incErrorCounterMetrics(requester, ENDPOINT, '400'); 117 | return errorResponse(400, [errorMessage(ERR_RC_INCORRECT_REQUEST, reqError.toString().split('\n').map(line => line.trim()).join(' => '))], logger); 118 | } 119 | 120 | const templateId = params.templateId; 121 | const consoleProjectUrl = params?.links?.consoleProject; 122 | 123 | const hasCredentialsOrApiInParams = (('credentials' in params && params.credentials.length > 0) || ('apis' in params && params.apis.length > 0)); 124 | 125 | if (hasCredentialsOrApiInParams) { 126 | // scenario 1 : if apis or credentials, just overwrite the template 127 | const dbResponse = await updateTemplate(dbParams, templateId, body); 128 | if (dbResponse.matchedCount < 1) { 129 | await incErrorCounterMetrics(requester, ENDPOINT, '404'); 130 | return { 131 | statusCode: 404 132 | }; 133 | } 134 | } else if (consoleProjectUrl) { 135 | // scenario 2 : if consoleProject in payload, replace apis and credentials 136 | const projectId = consoleProjectUrl.split('/').at(-2); 137 | const accessToken = await generateAccessToken(params.IMS_AUTH_CODE, params.IMS_CLIENT_ID, params.IMS_CLIENT_SECRET, params.IMS_SCOPES, logger); 138 | const consoleClient = await consoleLib.init(accessToken, params.IMS_CLIENT_ID, getEnv(logger)); 139 | const { body: installConfig } = await consoleClient.getProjectInstallConfig(projectId); 140 | 141 | // We have to get the install config in this format to maintain backwards 142 | // compatibility with current template registry 143 | const credentials = []; 144 | const apis = []; 145 | for (const credential of installConfig.credentials) { 146 | credentials.push({ 147 | type: credential.type, 148 | flowType: credential.flowType 149 | }); 150 | 151 | if (credential.apis) { 152 | apis.push(...credential.apis.map(api => { 153 | return { 154 | credentialType: credential.type, 155 | flowType: credential.flowType, 156 | code: api.code 157 | }; 158 | })); 159 | } 160 | } 161 | 162 | body = { ...body, credentials, apis }; 163 | } 164 | 165 | // an app builder template scenario 166 | const dbResponse = await updateTemplate(dbParams, templateId, body); 167 | if (dbResponse.matchedCount < 1) { 168 | logger.info('"PUT templates" not executed successfully'); 169 | await incErrorCounterMetrics(requester, ENDPOINT, '404'); 170 | return { 171 | statusCode: 404 172 | }; 173 | } 174 | 175 | // fetch the updated template from the database 176 | const template = await findTemplateById(dbParams, templateId); 177 | 178 | const response = { 179 | ...template, 180 | _links: { 181 | self: { 182 | // if name is npm package name (i.e. @adobe/template), then use the name, otherwise use the id 183 | href: template.name.includes('/') ? `${params.TEMPLATE_REGISTRY_API_URL}/templates/${template.name}` : `${params.TEMPLATE_REGISTRY_API_URL}/templates/${template.id}` 184 | } 185 | } 186 | }; 187 | 188 | // validate the response data to be sure it complies with OpenApi Schema 189 | const [res, resError] = req.response(200, response); 190 | if (resError) { 191 | throw new Error(resError.toString()); 192 | } 193 | 194 | logger.info('"PUT templates" executed successfully'); 195 | return { 196 | statusCode: 200, 197 | body: res.body 198 | }; 199 | } catch (error) { 200 | // log any server errors 201 | logger.error(error); 202 | // return with 500 203 | await incErrorCounterMetrics(requester, ENDPOINT, '500'); 204 | return errorResponse(500, [errorMessage(ERR_RC_SERVER_ERROR, error.message)], logger); 205 | } 206 | } 207 | 208 | exports.main = main; 209 | -------------------------------------------------------------------------------- /test/fixtures/list/response.full.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusCode": 200, 3 | "body": { 4 | "_links": { 5 | "self": { 6 | "href": "https://template-registry-api.tbd/apis/v1/templates" 7 | } 8 | }, 9 | "items": [ 10 | { 11 | "_links": { 12 | "self": { 13 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-1" 14 | } 15 | }, 16 | "adobeRecommended": false, 17 | "author": "Adobe Inc.", 18 | "categories": [ 19 | "action", 20 | "ui" 21 | ], 22 | "description": "A template for testing purposes", 23 | "extensions": [ 24 | { 25 | "extensionPointId": "dx/excshell/1" 26 | } 27 | ], 28 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac47", 29 | "keywords": [ 30 | "aio", 31 | "adobeio", 32 | "app", 33 | "templates", 34 | "aio-app-builder-template" 35 | ], 36 | "latestVersion": "1.0.0", 37 | "links": { 38 | "github": "https://github.com/author/app-builder-template-1", 39 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-1" 40 | }, 41 | "name": "@author/app-builder-template-1", 42 | "publishDate": "2022-05-01T03:50:39.658Z", 43 | "apis": [ 44 | { 45 | "code": "AnalyticsSDK", 46 | "credentials": "OAuth" 47 | }, 48 | { 49 | "code": "CampaignStandard" 50 | }, 51 | { 52 | "code": "Runtime" 53 | }, 54 | { 55 | "code": "Events", 56 | "hooks": [ 57 | { 58 | "postdeploy": "some command" 59 | } 60 | ] 61 | }, 62 | { 63 | "code": "Mesh", 64 | "endpoints": [ 65 | { 66 | "my-action": "https://some-action.com/action" 67 | } 68 | ] 69 | } 70 | ], 71 | "status": "Approved", 72 | "runtime": true 73 | }, 74 | { 75 | "_links": { 76 | "self": { 77 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-2" 78 | } 79 | }, 80 | "adobeRecommended": true, 81 | "author": "Adobe Inc.", 82 | "categories": [ 83 | "events" 84 | ], 85 | "description": "A template for testing purposes", 86 | "extensions": [ 87 | { 88 | "extensionPointId": "dx/asset-compute/worker/1" 89 | } 90 | ], 91 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6cc48", 92 | "keywords": [ 93 | "aio", 94 | "adobeio", 95 | "app", 96 | "templates", 97 | "aio-app-builder-template" 98 | ], 99 | "latestVersion": "1.0.1", 100 | "links": { 101 | "github": "https://github.com/author/app-builder-template-2", 102 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-2" 103 | }, 104 | "name": "@author/app-builder-template-2", 105 | "publishDate": "2022-05-01T03:50:39.658Z", 106 | "apis": [ 107 | { 108 | "code": "Events", 109 | "hooks": [ 110 | { 111 | "postdeploy": "some command" 112 | } 113 | ] 114 | }, 115 | { 116 | "code": "Mesh", 117 | "endpoints": [ 118 | { 119 | "my-action": "https://some-action.com/action" 120 | } 121 | ] 122 | } 123 | ], 124 | "status": "Approved", 125 | "runtime": true, 126 | "event": {} 127 | }, 128 | { 129 | "_links": { 130 | "self": { 131 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-3" 132 | } 133 | }, 134 | "adobeRecommended": true, 135 | "author": "Adobe Inc.", 136 | "categories": [ 137 | "ui" 138 | ], 139 | "description": "A template for testing purposes", 140 | "id": "d1dc1000-f32e-4172-a0ec-9b2f3ef6ac48", 141 | "keywords": [ 142 | "aio", 143 | "adobeio", 144 | "app", 145 | "templates", 146 | "aio-app-builder-template" 147 | ], 148 | "latestVersion": "1.0.1", 149 | "links": { 150 | "github": "https://github.com/author/app-builder-template-3", 151 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-3" 152 | }, 153 | "name": "@author/app-builder-template-3", 154 | "publishDate": "2022-05-01T03:50:39.658Z", 155 | "apis": [ 156 | { 157 | "code": "CampaignStandard" 158 | } 159 | ], 160 | "status": "Approved", 161 | "runtime": false 162 | }, 163 | { 164 | "_links": { 165 | "review": { 166 | "description": "A link to the \"Template Review Request\" Github issue.", 167 | "href": "https://github.com/adobe/aio-template-submission/issues/4" 168 | }, 169 | "self": { 170 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-4" 171 | } 172 | }, 173 | "id": "d1dc1000-f32e-4172-a0ec-9b2f4ef6cc48", 174 | "name": "@author/app-builder-template-4", 175 | "status": "InVerification", 176 | "links": { 177 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-4", 178 | "github": "https://github.com/author/app-builder-template-4" 179 | } 180 | }, 181 | { 182 | "_links": { 183 | "review": { 184 | "description": "A link to the \"Template Review Request\" Github issue.", 185 | "href": "https://github.com/adobe/aio-template-submission/issues/5" 186 | }, 187 | "self": { 188 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-5" 189 | } 190 | }, 191 | "id": "d1dc1000-f32e-4172-a0ac-9b2f3ef6ac48", 192 | "name": "@author/app-builder-template-5", 193 | "status": "Rejected", 194 | "links": { 195 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-5", 196 | "github": "https://github.com/author/app-builder-template-5" 197 | } 198 | }, 199 | { 200 | "_links": { 201 | "self": { 202 | "href": "https://template-registry-api.tbd/apis/v1/templates/@author/app-builder-template-6" 203 | } 204 | }, 205 | "adobeRecommended": false, 206 | "author": "Adobe Inc.", 207 | "categories": [ 208 | "ui", 209 | "helper-template" 210 | ], 211 | "description": "A template for testing purposes", 212 | "id": "d1nc1000-f32e-4472-a3ec-9b2f3ef6ac48", 213 | "keywords": [ 214 | "aio", 215 | "adobeio", 216 | "app", 217 | "templates", 218 | "aio-app-builder-template" 219 | ], 220 | "latestVersion": "1.0.1", 221 | "links": { 222 | "npm": "https://www.npmjs.com/package/@author/app-builder-template-6", 223 | "github": "https://github.com/author/app-builder-template-6" 224 | }, 225 | "name": "@author/app-builder-template-6", 226 | "publishDate": "2022-06-11T04:50:39.658Z", 227 | "extensions": [ 228 | { 229 | "extensionPointId": "dx/asset-compute/worker/1" 230 | } 231 | ], 232 | "status": "Approved", 233 | "runtime": true 234 | } 235 | ] 236 | } 237 | } 238 | --------------------------------------------------------------------------------