Skip to content
This repository has been archived by the owner on Aug 22, 2024. It is now read-only.

feat: Support for request verification #31

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,12 @@ $ npm test
```
### Integration Tests

The Integration Tests require the following environment variables which can be supplied in a .env file:
The Integration Tests require the following environment variables which can be extracted from Vault by:

```
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
QUEUE_URL=

ALGOLIA_APP_NAME=
ALGOLIA_API_KEY=
ALGOLIA_CI_INDEX=
```
1. create a .env file with the contents `# VAULT_PATHS /dx_aem_content_lake/dev/aws/content-lake-circle-ci-user /dx_aem_content_lake/local/sqs /dx_aem_content_lake/local/frontegg`
2. run the script [update-env.sh](https://github.com/adobe/content-lake-infra/blob/main/update-env.sh), ex: `./update.env ../content-lake-commons/.env`

Run the test with:

```bash
$ npm test:integration
Expand Down
121 changes: 113 additions & 8 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ objects against the schemas.</p>
<dd></dd>
<dt><del><a href="#extractSqsRecords">extractSqsRecords()</a></del></dt>
<dd></dd>
<dt><a href="#Security.">Security.(allowedPermissions, actualPermissions)</a> ⇒ <code>boolean</code></dt>
<dd><p>Checks to see if any of the allowed permissions are present in the actualPermissions,
using globbing expansion.</p>
<p>The following would return true:</p>
<pre>
allowedPermissions=['app.read']; actualPermissions=['app.read','app.write']
allowedPermissions=['app.write']; actualPermissions=['app.*']
</pre>

<p>The following would return false:</p>
<pre>
allowedPermissions=['app.write']; actualPermissions=['app.read']
allowedPermissions=['app2.read']; actualPermissions=['app.*']
</pre></dd>
</dl>

## Typedefs
Expand All @@ -45,11 +59,19 @@ objects against the schemas.</p>
<dd></dd>
<dt><a href="#QueueRecord">QueueRecord</a> : <code>Object</code></dt>
<dd></dd>
<dt><a href="#SecurityConfig">SecurityConfig</a></dt>
<dd></dd>
<dt><a href="#Handler">Handler</a> ⇒ <code>Promise.&lt;Response&gt;</code></dt>
<dd><p>Function for handling a routes inside Frankin / Content Lake services</p>
<dd><p>Function for handling a routes inside Franklin / Content Lake services</p>
</dd>
<dt><a href="#SecretConfig">SecretConfig</a> : <code><a href="#AwsConfig">AwsConfig</a></code> | <code>SecretConfigExt</code></dt>
<dd></dd>
<dt><a href="#AuthenticationRequirement">AuthenticationRequirement</a> : <code>Object</code></dt>
<dd></dd>
<dt><a href="#TokenRequest">TokenRequest</a> : <code>Object</code></dt>
<dd></dd>
<dt><a href="#TokenPayload">TokenPayload</a> : <code>Object</code></dt>
<dd></dd>
</dl>

<a name="SchemaValidator"></a>
Expand All @@ -59,18 +81,21 @@ Loads schemas from this project in the <code>schemas</code> directory and suppor
objects against the schemas.

**Kind**: global class
<a name="SchemaValidator+validateIngestionRequest"></a>
<a name="SchemaValidator+validateObject"></a>

### schemaValidator.validateIngestionRequest(ingestionRequest, additionalRequiredData)
Validates the <code>ingestionRequest</code> object against the Ingestion Request schema
### schemaValidator.validateObject(obj, schemaName, additionalRequiredData)
Validates an object against the schema specified by <code>schemaName</code>
as specified in https://wiki.corp.adobe.com/display/WEM/Ingestor+API+Contract

Throws <code>Error</code> if request does not match

**Kind**: instance method of [<code>SchemaValidator</code>](#SchemaValidator)
**See**: https://wiki.corp.adobe.com/display/WEM/Ingestor+API+Contract?

| Param | Type |
| --- | --- |
| ingestionRequest | <code>any</code> |
| obj | <code>any</code> |
| schemaName | <code>string</code> |
| additionalRequiredData | <code>Array.&lt;string&gt;</code> |

<a name="extractAwsConfig"></a>
Expand All @@ -97,6 +122,32 @@ as specified in https://wiki.corp.adobe.com/display/WEM/Ingestor+API+Contract
***Deprecated***

**Kind**: global function
<a name="Security."></a>

## Security.(allowedPermissions, actualPermissions) ⇒ <code>boolean</code>
Checks to see if any of the allowed permissions are present in the actualPermissions,
using globbing expansion.

The following would return true:
<pre>
allowedPermissions=['app.read']; actualPermissions=['app.read','app.write']
allowedPermissions=['app.write']; actualPermissions=['app.*']
</pre>

The following would return false:

<pre>
allowedPermissions=['app.write']; actualPermissions=['app.read']
allowedPermissions=['app2.read']; actualPermissions=['app.*']
</pre>

**Kind**: global function

| Param | Type | Description |
| --- | --- | --- |
| allowedPermissions | <code>Array.&lt;string&gt;</code> | the list of permissions which are allowed |
| actualPermissions | <code>Array.&lt;string&gt;</code> | the actual permissions from the request |

<a name="BlobStorageConfig"></a>

## BlobStorageConfig : <code>Object</code>
Expand Down Expand Up @@ -197,19 +248,30 @@ as specified in https://wiki.corp.adobe.com/display/WEM/Ingestor+API+Contract
| messageAttributes | <code>Record.&lt;string, any&gt;</code> |
| eventSource | <code>string</code> |

<a name="SecurityConfig"></a>

## SecurityConfig
**Kind**: global typedef
**Properties**

| Name | Type |
| --- | --- |
| authRequired; | <code>boolean</code> |
| [allowedRoles] | <code>Array.&lt;string&gt;</code> |
| [allowedPermissions] | <code>Array.&lt;string&gt;</code> |

<a name="Handler"></a>

## Handler ⇒ <code>Promise.&lt;Response&gt;</code>
Function for handling a routes inside Frankin / Content Lake services
Function for handling a routes inside Franklin / Content Lake services

**Kind**: global typedef
**Returns**: <code>Promise.&lt;Response&gt;</code> - the response from the request

| Param | Type | Description |
| --- | --- | --- |
| req | <code>Request</code> | the request |
| context | <code>UniversalContext</code> | the context of the request |
| params | <code>Record.&lt;string, string&gt;</code> | the parameters parsed from the request |
| info | <code>Object</code> | the additional request information |

<a name="SecretConfig"></a>

Expand All @@ -220,4 +282,47 @@ Function for handling a routes inside Frankin / Content Lake services
| Name | Type |
| --- | --- |
| [client] | <code>SecretsManagerClient</code> |
| application | <code>string</code> |
| [companyId] | <code>string</code> |
| [scope] | <code>string</code> |

<a name="AuthenticationRequirement"></a>

## AuthenticationRequirement : <code>Object</code>
**Kind**: global typedef
**Properties**

| Name | Type |
| --- | --- |
| [allowedRoles] | <code>Array.&lt;string&gt;</code> |
| [allowedPermissions] | <code>Array.&lt;string&gt;</code> |

<a name="TokenRequest"></a>

## TokenRequest : <code>Object</code>
**Kind**: global typedef
**Properties**

| Name | Type | Description |
| --- | --- | --- |
| spaceId | <code>string</code> | the space for which the token will be generated |
| roleKeys | <code>Array.&lt;string&gt;</code> | the role keys for which the token should be generated |
| generator | <code>string</code> | provides attribution for the key in the description |
| [expiresInMinutes] | <code>number</code> | the number of minutes before the token expires (or 14 days) |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qs: why isn't the companyId needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not used for validating the token, but it does make sense that we need to validate that as well as the space id.


<a name="TokenPayload"></a>

## TokenPayload : <code>Object</code>
**Kind**: global typedef
**Properties**

| Name | Type | Description |
| --- | --- | --- |
| permissions | <code>Array.&lt;string&gt;</code> | the permissions granted to this token |
| roles | <code>Array.&lt;string&gt;</code> | the permissions granted to this token |
| sub | <code>string</code> | the subject (or identifier) for this token |
| tenantId | <code>string</code> | the primary tenant for the token |
| [tenantIds] | <code>Array.&lt;string&gt;</code> | the optional list of additional allowed tenants for the token |
| type | <code>string</code> | the type of the token |
| exp | <code>number</code> | the timestamp at which the token will expire |

22 changes: 16 additions & 6 deletions it/secret.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,21 @@ import {
DeleteSecretCommand,
ListSecretsCommand,
} from '@aws-sdk/client-secrets-manager';
import { extractAwsConfig } from '../src/context.js';
import { ContextHelper } from '../src/context.js';
import { SecretsManager } from '../src/secret.js';

dotenv.config();

const SLOW_TEST_TIMEOUT = 5000;

describe('Secrets Manager Integration Tests', async () => {
const extractor = 'it';
const defaultConfig = { scope: 'test', application: 'commons-secrets-it' };
const helper = new ContextHelper(process);
it('fails on non-existing secret', async () => {
const mgr = new SecretsManager(extractor, extractAwsConfig(process));
const mgr = new SecretsManager({
...defaultConfig,
...helper.extractAwsConfig(),
});
let caught;
try {
await mgr.getSecret('not-a-secret');
Expand All @@ -41,7 +45,10 @@ describe('Secrets Manager Integration Tests', async () => {

it('can create, get and delete secret', async () => {
const secretId = randomUUID();
const mgr = new SecretsManager(extractor, extractAwsConfig(process));
const mgr = new SecretsManager({
...defaultConfig,
...helper.extractAwsConfig(),
});
let caught;
try {
await mgr.getSecret(secretId);
Expand All @@ -64,7 +71,10 @@ describe('Secrets Manager Integration Tests', async () => {
}).timeout(SLOW_TEST_TIMEOUT);

after(async () => {
const secretManager = new SecretsManagerClient(extractAwsConfig(process));
const secretManager = new SecretsManagerClient({
...defaultConfig,
...helper.extractAwsConfig(),
});
await secretManager
.send(
new ListSecretsCommand({
Expand All @@ -75,7 +85,7 @@ describe('Secrets Manager Integration Tests', async () => {
.then((res) => {
Promise.all(
res.SecretList.filter(
(secret) => secret.Name.startsWith('test') || secret.Name.startsWith('it'),
(secret) => secret.Name.startsWith('test/shared/commons-secrets-it/'),
).map((secret) => secretManager.send(
new DeleteSecretCommand({
SecretId: secret.Name,
Expand Down
67 changes: 67 additions & 0 deletions it/security.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2021 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

/* eslint-env mocha */
import assert from 'assert';
import * as dotenv from 'dotenv';
import { Security } from '../src/security.js';
import { LocalKeySecurity } from '../src/mocks/local-key-security.js';

dotenv.config();

const SLOW_TEST_TIMEOUT = 5000;
const spaceId = process.env.SPACE_ID;

function createRequest(token) {
return new Request('http://localhost/', {
headers: {
'x-space-id': spaceId,
authorization: `Bearer ${token}`,
},
});
}

describe('Security Integration Tests', async () => {
const security = new Security({ ...process, scope: 'test' });
klcodanr marked this conversation as resolved.
Show resolved Hide resolved

it('can generate and authorize tokens', async () => {
const token = await security.generateToken({
generator: 'Commons Security IT',
spaceId,
roleKeys: ['Admin'],
});
assert.ok(token);
await security.authorize(createRequest(token));
}).timeout(SLOW_TEST_TIMEOUT);

it('generate fails without roles', async () => {
await assert.rejects(() => security.generateToken({
generator: 'Commons Security IT',
spaceId,
roleKeys: [],
}));
});

it('will not authorize invalid token', async () => {
await assert.rejects(() => security.authorize(createRequest('not a valid token')));

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "not a valid token" is used as [authorization header](1).
});

it('will not authorize token generated elsewhere', async () => {
const localSecurity = new LocalKeySecurity();
const key = await localSecurity.generateToken({
generator: 'Commons Security IT',
spaceId,
roleKeys: ['Admin'],
});
await assert.rejects(() => security.authorize(createRequest(key)));
});
});
klcodanr marked this conversation as resolved.
Show resolved Hide resolved
Loading