@studyportals/cors 2.8.0 → 2.8.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codeclimate.yml +25 -0
- package/.github/CODEOWNERS +1 -0
- package/.nycrc +22 -0
- package/.travis.yml +18 -0
- package/README.md +110 -0
- package/bin/package.json +38 -0
- package/{src → bin/src}/domain/allowed-explicit-domains-rules.js +1 -4
- package/index.ts +2 -0
- package/package.json +3 -3
- package/src/domain/allowed-domain-reg-ex-list.ts +28 -0
- package/src/domain/allowed-explicit-domains-rules.ts +53 -0
- package/src/domain/cors.ts +32 -0
- package/src/domain/enums/environment.ts +4 -0
- package/src/domain/enums/index.ts +2 -0
- package/src/domain/enums/scope.ts +4 -0
- package/src/domain/index.ts +3 -0
- package/src/implementations/express-middleware.ts +21 -0
- package/src/implementations/index.ts +3 -0
- package/src/implementations/options.ts +50 -0
- package/src/implementations/s3-rules.ts +12 -0
- package/test/domain/cors.s3.test.ts +92 -0
- package/test/domain/cors.valid.domain.test.ts +155 -0
- package/test/mocha.opts +3 -0
- package/tsconfig.json +38 -0
- package/tslint.json +132 -0
- /package/{index.d.ts → bin/index.d.ts} +0 -0
- /package/{index.js → bin/index.js} +0 -0
- /package/{src → bin/src}/domain/allowed-domain-reg-ex-list.d.ts +0 -0
- /package/{src → bin/src}/domain/allowed-domain-reg-ex-list.js +0 -0
- /package/{src → bin/src}/domain/allowed-explicit-domains-rules.d.ts +0 -0
- /package/{src → bin/src}/domain/cors.d.ts +0 -0
- /package/{src → bin/src}/domain/cors.js +0 -0
- /package/{src → bin/src}/domain/enums/environment.d.ts +0 -0
- /package/{src → bin/src}/domain/enums/environment.js +0 -0
- /package/{src → bin/src}/domain/enums/index.d.ts +0 -0
- /package/{src → bin/src}/domain/enums/index.js +0 -0
- /package/{src → bin/src}/domain/enums/scope.d.ts +0 -0
- /package/{src → bin/src}/domain/enums/scope.js +0 -0
- /package/{src → bin/src}/domain/index.d.ts +0 -0
- /package/{src → bin/src}/domain/index.js +0 -0
- /package/{src → bin/src}/implementations/express-middleware.d.ts +0 -0
- /package/{src → bin/src}/implementations/express-middleware.js +0 -0
- /package/{src → bin/src}/implementations/index.d.ts +0 -0
- /package/{src → bin/src}/implementations/index.js +0 -0
- /package/{src → bin/src}/implementations/options.d.ts +0 -0
- /package/{src → bin/src}/implementations/options.js +0 -0
- /package/{src → bin/src}/implementations/s3-rules.d.ts +0 -0
- /package/{src → bin/src}/implementations/s3-rules.js +0 -0
package/.codeclimate.yml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
engines:
|
|
2
|
+
duplication:
|
|
3
|
+
enabled: true
|
|
4
|
+
config:
|
|
5
|
+
languages:
|
|
6
|
+
- ruby:
|
|
7
|
+
- javascript:
|
|
8
|
+
- php:
|
|
9
|
+
- python:
|
|
10
|
+
eslint:
|
|
11
|
+
enabled: true
|
|
12
|
+
# 5.7.0
|
|
13
|
+
channel: "eslint-5"
|
|
14
|
+
tslint:
|
|
15
|
+
enabled: true
|
|
16
|
+
# 5.9.1
|
|
17
|
+
config: tslint.json
|
|
18
|
+
fixme:
|
|
19
|
+
enabled: true
|
|
20
|
+
ratings:
|
|
21
|
+
paths:
|
|
22
|
+
- "**.js"
|
|
23
|
+
- "**.ts"
|
|
24
|
+
exclude_paths:
|
|
25
|
+
- "**/node_modules/"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @studyportals/plop @studyportals/samurai-pizza-cats
|
package/.nycrc
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": [
|
|
3
|
+
"src/**/*.ts"
|
|
4
|
+
],
|
|
5
|
+
"exclude": [
|
|
6
|
+
"src/**/*.interface.ts"
|
|
7
|
+
],
|
|
8
|
+
"require": [
|
|
9
|
+
"ts-node/register"
|
|
10
|
+
],
|
|
11
|
+
"extension": [
|
|
12
|
+
".ts"
|
|
13
|
+
],
|
|
14
|
+
"reporter": [
|
|
15
|
+
"lcov",
|
|
16
|
+
"text-summary",
|
|
17
|
+
"html"
|
|
18
|
+
],
|
|
19
|
+
"sourceMap": true,
|
|
20
|
+
"instrument": true,
|
|
21
|
+
"all": false
|
|
22
|
+
}
|
package/.travis.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
env:
|
|
2
|
+
global:
|
|
3
|
+
- CC_TEST_REPORTER_ID=81a49eda82f1c6ca2bcaf8475befb5a95136032496a53d6754f5e657d8c56a48
|
|
4
|
+
language: node_js
|
|
5
|
+
node_js:
|
|
6
|
+
- "8"
|
|
7
|
+
cache:
|
|
8
|
+
npm: true # this cashes ~/.npm folder on Travis
|
|
9
|
+
install:
|
|
10
|
+
- npm ci
|
|
11
|
+
script:
|
|
12
|
+
- npm run test:coverage
|
|
13
|
+
before_script:
|
|
14
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
|
15
|
+
- chmod +x ./cc-test-reporter
|
|
16
|
+
- ./cc-test-reporter before-build
|
|
17
|
+
after_script:
|
|
18
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+

|
|
2
|
+
# CORS
|
|
3
|
+
|
|
4
|
+
# Getting Started
|
|
5
|
+
|
|
6
|
+
This library can be used to add CORS validation to your API. It can also replace API gateway's implementation of CORS by adding an OPTIONS lambda. This is necessary when you API needs to function for wildcard domains like release candidates.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @studyportals/cors
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Setting up the CORS check in your repo
|
|
15
|
+
To use this package in a Lambda the CORS class needs to be constructed outside of the lambda handler. Example:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import {CORS, Scope, Environment} from '@studyportals/cors'
|
|
19
|
+
|
|
20
|
+
const cors = new CORS(Scope.Student, Environment.Production)
|
|
21
|
+
|
|
22
|
+
export async function handler(event: any) {
|
|
23
|
+
const response = {body: 'someBody', headers: {}};
|
|
24
|
+
const origin = event.headers['Origin']; // BEAWARE HEADERS ARE CASE SENSITIVE!!
|
|
25
|
+
|
|
26
|
+
if(await cors.isValidDomain(origin)){
|
|
27
|
+
response.headers['Access-Control-Allow-Origin'] = origin;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return response;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Make sure to use the correct `Scope` And `Environment`
|
|
36
|
+
|
|
37
|
+
The lambda needs to have access to these buckets. If this bucket is not in your account you need to ask `Samurai Pizza Cats` for access.
|
|
38
|
+
|
|
39
|
+
## Setting up the Options lambda.
|
|
40
|
+
To add the OPTIONS lambda you need to make sure all lambda's have CORS disabled. After this you can create a lambda using the following snippet:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import {OptionsLambda, Scope, Environment} from '@studyportals/cors'
|
|
44
|
+
|
|
45
|
+
const optionsLambda = new OptionsLambda(Scope.Student, Environment.Production);
|
|
46
|
+
const handler = optionsLambda.getHandler();
|
|
47
|
+
|
|
48
|
+
// Lambda hook
|
|
49
|
+
export function handler;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This lambda handler needs to be added to the `serverless.yaml` file. The incoming event needs to be a http event with the path `/{any+}` and method `OPTIONS`
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Express CORS
|
|
56
|
+
Express applications can use the `ExpressMiddleware` provided by this package. Example:
|
|
57
|
+
```typescript
|
|
58
|
+
import { express } from 'express';
|
|
59
|
+
import {ExpressMiddleware, Scope, Environment} from '@studyportals/cors'
|
|
60
|
+
const app = express();
|
|
61
|
+
|
|
62
|
+
const corsMiddleWare = ExpressMiddleware.getMiddleWare(Scope.Student, Environment.Production);
|
|
63
|
+
app.use(corsMiddleWare);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## S3 CORS rules
|
|
67
|
+
[GoldenEye](https://github.com/studyportals/goldeneye) and other tools that need to generate S3 IAM policies can use the `S3Rules` provided in this package. Example:
|
|
68
|
+
```typescript
|
|
69
|
+
import { CorsRule } from "aws-cdk-lib/aws-s3";
|
|
70
|
+
import * as cors from "@studyportals/cors";
|
|
71
|
+
|
|
72
|
+
generateRules = (scope: cors.Scope, environment: cors.Environment): CorsRule[] => {
|
|
73
|
+
return cors.S3Rules.generate(scope, environment) as CorsRule[];
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Developing the package
|
|
79
|
+
|
|
80
|
+
## Modifying allowed domain list
|
|
81
|
+
When modifying the allowed domain list follow the following rules
|
|
82
|
+
|
|
83
|
+
1. When a domain is added or removed for security reasons (Unauthorized access by thirdparty) the major version should be increased. All microservices need to update their version of this package ASAP.
|
|
84
|
+
|
|
85
|
+
2. When a domain is added for feature reasons (Added another micro website) the minor version should be increased. Only affected microservices should be required to update their CORS package ASAP
|
|
86
|
+
|
|
87
|
+
## Compiling
|
|
88
|
+
In a terminal, run
|
|
89
|
+
```
|
|
90
|
+
tsc
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The output JavaScript can be found in `/bin`.
|
|
94
|
+
|
|
95
|
+
## Testing
|
|
96
|
+
|
|
97
|
+
Run `npm test` to execute the unit tests.
|
|
98
|
+
|
|
99
|
+
run `npm test:coverage` to run the unit tests as well as generate a coverage report.
|
|
100
|
+
|
|
101
|
+
If `test` fails, publishing will not continue.
|
|
102
|
+
Consult the terminal to find out why this has happened.
|
|
103
|
+
|
|
104
|
+
We have a reference document for writing tests [here](https://github.com/studyportals/SamuraiPizzaCats/blob/master/Testing.md "SamuraiPizzaCats/Testing.md").
|
|
105
|
+
|
|
106
|
+
## Deploying
|
|
107
|
+
We publish an npm package, this is done by first committing and pushing the changes. updating the version number, followed by a `npm run deploy` command. This command prepares for deployment and then automatically publishes and pushes to GitHub.
|
|
108
|
+
|
|
109
|
+
# House Rules
|
|
110
|
+
House Rules are located in the [SamuraiPizzaCats](https://github.com/studyportals/SamuraiPizzaCats) repository.
|
package/bin/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@studyportals/cors",
|
|
3
|
+
"version": "2.8.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "mocha test/**/*.test.ts",
|
|
8
|
+
"test:watch": "mocha -w --reporter min test/**/*.test.ts",
|
|
9
|
+
"deploy-patch": "npm version patch && npm run deploy",
|
|
10
|
+
"deploy-minor": "npm version minor && npm run deploy",
|
|
11
|
+
"deploy-major": "npm version major && npm run deploy",
|
|
12
|
+
"prepare-deployment": "cp package.json bin/package.json",
|
|
13
|
+
"deploy": "npm run build && npm run prepare-deployment && npm publish ./bin",
|
|
14
|
+
"build": "rm -rf bin && tsc && rm -rf bin/test",
|
|
15
|
+
"lint": "tslint 'src/**/*.ts'",
|
|
16
|
+
"lint-fix": "tslint --fix 'src/**/*.ts'",
|
|
17
|
+
"test:coverage": "nyc mocha test/**/*.test.ts"
|
|
18
|
+
},
|
|
19
|
+
"private": false,
|
|
20
|
+
"author": "SP - Samurai Pizza Cats",
|
|
21
|
+
"license": "BSD3",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@types/express": "^4.17.3"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/chai": "^4.1.7",
|
|
27
|
+
"@types/node": "^10.12.15",
|
|
28
|
+
"chai": "^4.2.0",
|
|
29
|
+
"mocha": "^5.2.0",
|
|
30
|
+
"mocha-typescript": "1.1.12",
|
|
31
|
+
"nyc": "^13.3.0",
|
|
32
|
+
"ts-loader": "^2.3.7",
|
|
33
|
+
"ts-node": "^7.0.1",
|
|
34
|
+
"tslint": "^5.11.0",
|
|
35
|
+
"typemoq": "^2.1.0",
|
|
36
|
+
"typescript": "^4.8.4"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -33,10 +33,7 @@ exports.AllowedExplicitDomainsRules = {
|
|
|
33
33
|
maxAge: 3000
|
|
34
34
|
},
|
|
35
35
|
[environment_1.Environment.Testing]: {
|
|
36
|
-
allowedOrigins: ["
|
|
37
|
-
"*.administration.tst.studyportals.eu", "administration.tst.studyportals.eu",
|
|
38
|
-
"https://administration.dev.studyportals.eu:*",
|
|
39
|
-
"*-uniadmin.studyportals.eu"],
|
|
36
|
+
allowedOrigins: ["*.studyportals.eu"],
|
|
40
37
|
allowedMethods: ["GET"],
|
|
41
38
|
allowedHeaders: ["*"],
|
|
42
39
|
exposedHeaders: [],
|
package/index.ts
ADDED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studyportals/cors",
|
|
3
|
-
"version": "2.8.0",
|
|
3
|
+
"version": "2.8.1-alpha.0",
|
|
4
4
|
"description": "",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "mocha test/**/*.test.ts",
|
|
8
8
|
"test:watch": "mocha -w --reporter min test/**/*.test.ts",
|
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
"typemoq": "^2.1.0",
|
|
36
36
|
"typescript": "^4.8.4"
|
|
37
37
|
}
|
|
38
|
-
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Environment } from './enums/environment';
|
|
2
|
+
import { Scope } from './enums/scope';
|
|
3
|
+
|
|
4
|
+
export const AllowedDomainRegExList = {
|
|
5
|
+
[Scope.Student]: {
|
|
6
|
+
[Environment.Production]: [
|
|
7
|
+
/^https?:\/\/(www(?:\.dev)?|cypress\.rc|rc|beta|[0-9]{4,16}(?:-bob)?\.rc)\.mastersportal\.(com|fyi)(?::3030)?$/,
|
|
8
|
+
/^https?:\/\/(www(?:\.dev)?|cypress\.rc|rc|beta|[0-9]{4,16}(?:-bob)?\.rc)\.bachelorsportal\.(com|fyi)(?::3030)?$/,
|
|
9
|
+
/^https?:\/\/(www(?:\.dev)?|cypress\.rc|rc|beta|[0-9]{4,16}(?:-bob)?\.rc)\.phdportal\.(com|fyi)(?::3030)?$/,
|
|
10
|
+
/^https?:\/\/(www(?:\.dev)?|cypress\.rc|rc|beta|[0-9]{4,16}(?:-bob)?\.rc)\.preparationcoursesportal\.(com|fyi)(?::3030)?$/,
|
|
11
|
+
/^https?:\/\/(www(?:\.dev)?|cypress\.rc|rc|beta|[0-9]{4,16}(?:-bob)?\.rc)\.distancelearningportal\.(com|fyi)(?::3030)?$/,
|
|
12
|
+
/^https?:\/\/(www(?:\.dev)?|cypress\.rc|rc|beta|[0-9]{4,16}(?:-bob)?\.rc)\.shortcoursesportal\.(com|fyi)(?::3030)?$/,
|
|
13
|
+
/^https?:\/\/(www.)?admissiontestportal.com?$/,
|
|
14
|
+
/^https?:\/\/studentemailing\.prtl\.co$/,
|
|
15
|
+
/^https?:\/\/scholarship\.mastersportal\.com$/,
|
|
16
|
+
/^https?:\/\/students\.mastersportal\.com$/,
|
|
17
|
+
/^https?:\/\/search-page\.prtl\.co$/,
|
|
18
|
+
/^https?:\/\/reviews\.studyportals\.com$/,
|
|
19
|
+
],
|
|
20
|
+
[Environment.Testing]: [
|
|
21
|
+
/(.*?)/,
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
[Scope.UniAdmin]: {
|
|
25
|
+
[Environment.Production]: [/(.*?)/],
|
|
26
|
+
[Environment.Testing]: [/(.*?)/],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Environment } from "./enums/environment";
|
|
2
|
+
import { Scope } from "./enums/scope";
|
|
3
|
+
|
|
4
|
+
export type AllowedExplicitDomainsRulesType = typeof AllowedExplicitDomainsRules
|
|
5
|
+
|
|
6
|
+
export type CorsRule = {
|
|
7
|
+
allowedOrigins: string[],
|
|
8
|
+
allowedMethods: string[],
|
|
9
|
+
allowedHeaders: string[],
|
|
10
|
+
exposedHeaders: string[],
|
|
11
|
+
maxAge: number,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const AllowedExplicitDomainsRules = {
|
|
15
|
+
[Scope.Student]: {
|
|
16
|
+
[Environment.Production]: {
|
|
17
|
+
allowedOrigins: ["*.mastersportal.com", "*.bachelorsportal.com", "*.shortcoursesportal.com",
|
|
18
|
+
"*.distancelearningportal.com", "*.preparationcoursesportal.com", "*.phdportal.com",
|
|
19
|
+
"*.prtl.co", "*.rc.mastersportal.fyi", "*.rc.bachelorsportal.fyi", "*.rc.shortcoursesportal.fyi",
|
|
20
|
+
"*.rc.distancelearningportal.fyi", "*.rc.preparationcoursesportal.fyi", "*.rc.phdportal.fyi",
|
|
21
|
+
"*.admissiontestportal.com", "https://admissiontestportal.com", "*.studyportals.com"],
|
|
22
|
+
allowedHeaders: ["Authorization"],
|
|
23
|
+
allowedMethods: ["GET"],
|
|
24
|
+
exposedHeaders: [],
|
|
25
|
+
maxAge: 3000
|
|
26
|
+
|
|
27
|
+
},
|
|
28
|
+
[Environment.Testing]: {
|
|
29
|
+
allowedOrigins: ["*"],
|
|
30
|
+
allowedMethods: ["GET"],
|
|
31
|
+
allowedHeaders: ["Authorization"],
|
|
32
|
+
exposedHeaders: [],
|
|
33
|
+
maxAge: 3000
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
[Scope.UniAdmin]: {
|
|
37
|
+
[Environment.Production]: {
|
|
38
|
+
allowedOrigins: ["*.studyportals.eu"],
|
|
39
|
+
allowedHeaders: ["Authorization"],
|
|
40
|
+
allowedMethods: ["GET", "POST"],
|
|
41
|
+
exposedHeaders: [],
|
|
42
|
+
maxAge: 3000
|
|
43
|
+
|
|
44
|
+
},
|
|
45
|
+
[Environment.Testing]: {
|
|
46
|
+
allowedOrigins: ["*.studyportals.eu"],
|
|
47
|
+
allowedMethods: ["GET"],
|
|
48
|
+
allowedHeaders: ["*"],
|
|
49
|
+
exposedHeaders: [],
|
|
50
|
+
maxAge: 3000
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { AllowedDomainRegExList } from './allowed-domain-reg-ex-list';
|
|
2
|
+
import { AllowedExplicitDomainsRules, CorsRule } from './allowed-explicit-domains-rules';
|
|
3
|
+
import { Environment } from './enums/environment';
|
|
4
|
+
import { Scope } from './enums/scope';
|
|
5
|
+
|
|
6
|
+
export class CORS {
|
|
7
|
+
|
|
8
|
+
private regExes: RegExp[];
|
|
9
|
+
private s3Rules: CorsRule[]
|
|
10
|
+
|
|
11
|
+
constructor(scope: Scope, environment: Environment = Environment.Production) {
|
|
12
|
+
this.regExes = AllowedDomainRegExList[scope][environment];
|
|
13
|
+
this.s3Rules = [ AllowedExplicitDomainsRules[scope][environment] ] ;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* isValidDomain
|
|
18
|
+
*/
|
|
19
|
+
public async isValidDomain(domain: string): Promise<boolean> {
|
|
20
|
+
const hasMatch = this.regExes.filter((regex) => domain.match(regex) !== null);
|
|
21
|
+
return (hasMatch.length !== 0);
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* getIamRules
|
|
27
|
+
*/
|
|
28
|
+
public getS3Rules(): CorsRule[] {
|
|
29
|
+
return this.s3Rules;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
2
|
+
import { CORS } from '../domain';
|
|
3
|
+
import { Environment, Scope } from '../domain/enums';
|
|
4
|
+
|
|
5
|
+
export class ExpressMiddleware {
|
|
6
|
+
|
|
7
|
+
public static getMiddleWare(scope: Scope, environment: Environment = Environment.Production): RequestHandler {
|
|
8
|
+
const cors = new CORS(scope, environment);
|
|
9
|
+
|
|
10
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
11
|
+
const origin = req.get('origin') || '';
|
|
12
|
+
|
|
13
|
+
if (await cors.isValidDomain(origin)) {
|
|
14
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
next();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import {CORS} from '../domain';
|
|
4
|
+
import { Environment, Scope } from '../domain/enums';
|
|
5
|
+
|
|
6
|
+
export class OptionsLambda {
|
|
7
|
+
private cors: CORS;
|
|
8
|
+
|
|
9
|
+
constructor(scope: Scope, environment: Environment = Environment.Production) {
|
|
10
|
+
this.cors = new CORS(scope, environment);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getLowercasedHeaders(event: any): any {
|
|
14
|
+
const headers = {};
|
|
15
|
+
|
|
16
|
+
Object.keys(event.headers).forEach((key) => {
|
|
17
|
+
headers[key.toLowerCase()] = event.headers[key];
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return headers;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getOrigin(event: any): string {
|
|
24
|
+
const headers = this.getLowercasedHeaders(event);
|
|
25
|
+
return headers['origin'];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getHandler(): (event: any) => Promise<any> {
|
|
29
|
+
return async (event) => {
|
|
30
|
+
|
|
31
|
+
const origin = this.getOrigin(event);
|
|
32
|
+
const headers = {
|
|
33
|
+
'Cache-Control': 'max-age=600, s-maxage=600, proxy-revalidate',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (await this.cors.isValidDomain(origin)) {
|
|
37
|
+
headers['Access-Control-Allow-Origin'] = origin;
|
|
38
|
+
headers['Access-Control-Allow-Headers'] = 'authorization, x-api-key, content-type';
|
|
39
|
+
headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE, PATCH, HEAD, PUT, OPTIONS';
|
|
40
|
+
headers['Access-Control-Max-Age'] = '600';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
statusCode: 200,
|
|
45
|
+
headers,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
2
|
+
import { CORS } from '../domain';
|
|
3
|
+
import { AllowedExplicitDomainsRulesType, CorsRule } from '../domain/allowed-explicit-domains-rules';
|
|
4
|
+
import { Environment, Scope } from '../domain/enums';
|
|
5
|
+
|
|
6
|
+
export class S3Rules {
|
|
7
|
+
|
|
8
|
+
public static generate(scope: Scope, environment: Environment = Environment.Production): CorsRule[] {
|
|
9
|
+
const cors = new CORS(scope, environment);
|
|
10
|
+
return cors.getS3Rules();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { assert, expect } from "chai";
|
|
2
|
+
import { suite, test } from "mocha-typescript";
|
|
3
|
+
import * as Moq from "typemoq";
|
|
4
|
+
|
|
5
|
+
import { CORS } from "../../src/domain";
|
|
6
|
+
import { Environment } from "../../src/domain/enums/environment";
|
|
7
|
+
import { Scope } from "../../src/domain/enums/scope";
|
|
8
|
+
|
|
9
|
+
@suite()
|
|
10
|
+
class CORSS3RulesTest {
|
|
11
|
+
private testInstanceMock: Moq.IMock<CORS>;
|
|
12
|
+
|
|
13
|
+
private get testInstance(): CORS {
|
|
14
|
+
return this.testInstanceMock.object;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public before(): void {
|
|
18
|
+
|
|
19
|
+
const cors = new CORS(Scope.Student, Environment.Production);
|
|
20
|
+
this.testInstanceMock = Moq.Mock.ofInstance(cors);
|
|
21
|
+
this.testInstanceMock.callBase = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@test
|
|
25
|
+
public async getS3Rules_Should_Return_All_Required_Properties(): Promise<void> {
|
|
26
|
+
// given
|
|
27
|
+
const expectedProperties = ['allowedOrigins','allowedHeaders','allowedMethods','exposedHeaders','maxAge']
|
|
28
|
+
|
|
29
|
+
// when
|
|
30
|
+
const allDefaultRules = await this.testInstance.getS3Rules();
|
|
31
|
+
|
|
32
|
+
//then
|
|
33
|
+
expectedProperties.forEach((key) => {
|
|
34
|
+
allDefaultRules.every( rule => expect(rule).to.have.property(key));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@test
|
|
40
|
+
public async getS3Rules_Should_Return_Correct_Default_Domains(): Promise<void> {
|
|
41
|
+
// given
|
|
42
|
+
const expectedDomains = ['*.mastersportal.com', '*.bachelorsportal.com', '*.phdportal.com'];
|
|
43
|
+
|
|
44
|
+
// when
|
|
45
|
+
const allDefaultRules = await this.testInstance.getS3Rules();
|
|
46
|
+
|
|
47
|
+
// then
|
|
48
|
+
allDefaultRules.some( rule => expectedDomains.every( domain => rule.allowedOrigins.includes(domain) ) )
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test
|
|
52
|
+
public async getS3Rules_Should_Return_Correct_Allowed_Headers(): Promise<void> {
|
|
53
|
+
// given
|
|
54
|
+
const expectedHeaders = ['Authorization'];
|
|
55
|
+
|
|
56
|
+
// when
|
|
57
|
+
const allDefaultRules = await this.testInstance.getS3Rules();
|
|
58
|
+
|
|
59
|
+
// then
|
|
60
|
+
allDefaultRules.some( rule => expectedHeaders.every( header => !rule.allowedHeaders.includes(header) ) )
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@test
|
|
64
|
+
public async getS3Rules_Non_Production_Should_Not_Return_Production_Domains(): Promise<void> {
|
|
65
|
+
// given
|
|
66
|
+
const expectedDomains = ['*.mastersportal.fyi', '*.bachelorsportal.fyi', '*.phdportal.fyi'];
|
|
67
|
+
|
|
68
|
+
// when
|
|
69
|
+
const allDefaultRules = await this.testInstance.getS3Rules();
|
|
70
|
+
|
|
71
|
+
// then
|
|
72
|
+
allDefaultRules.some( rule => expectedDomains.every( domain => !rule.allowedOrigins.includes(domain) ) )
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@test
|
|
77
|
+
public async getS3Rules_Should_Return_Correct_Testing_Domains(): Promise<void> {
|
|
78
|
+
|
|
79
|
+
const cors = new CORS(Scope.Student, Environment.Testing);
|
|
80
|
+
this.testInstanceMock = Moq.Mock.ofInstance(cors);
|
|
81
|
+
this.testInstanceMock.callBase = true;
|
|
82
|
+
|
|
83
|
+
// given
|
|
84
|
+
const expectedDomains = ['*'];
|
|
85
|
+
|
|
86
|
+
// when
|
|
87
|
+
const allDefaultRules = await this.testInstance.getS3Rules();
|
|
88
|
+
|
|
89
|
+
// then
|
|
90
|
+
allDefaultRules.some( rule => expectedDomains.every( domain => rule.allowedOrigins.includes(domain) ) )
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { assert } from 'chai';
|
|
2
|
+
import { suite, test } from 'mocha-typescript';
|
|
3
|
+
import * as Moq from 'typemoq';
|
|
4
|
+
|
|
5
|
+
import { CORS } from '../../src/domain';
|
|
6
|
+
import { Environment } from '../../src/domain/enums/environment';
|
|
7
|
+
import { Scope } from '../../src/domain/enums/scope';
|
|
8
|
+
|
|
9
|
+
@suite()
|
|
10
|
+
class CORSValidDomainTest {
|
|
11
|
+
private testInstanceMock: Moq.IMock<CORS>;
|
|
12
|
+
|
|
13
|
+
private get testInstance(): CORS {
|
|
14
|
+
return this.testInstanceMock.object;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public before(): void {
|
|
18
|
+
|
|
19
|
+
const cors = new CORS(Scope.Student, Environment.Production);
|
|
20
|
+
this.testInstanceMock = Moq.Mock.ofInstance(cors);
|
|
21
|
+
this.testInstanceMock.callBase = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@test
|
|
25
|
+
public async isValidDomain_Should_Return_True_When_DomainIs_Mastersportal(): Promise<void> {
|
|
26
|
+
// given
|
|
27
|
+
const providedDomain = 'https://www.mastersportal.com';
|
|
28
|
+
|
|
29
|
+
// when
|
|
30
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
31
|
+
|
|
32
|
+
// then
|
|
33
|
+
assert.isTrue(validDomain);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@test
|
|
37
|
+
public async isValidDomain_Should_Return_True_When_DomainIs_Mastersportal_With_HTTP(): Promise<void> {
|
|
38
|
+
// given
|
|
39
|
+
const providedDomain = 'http://www.mastersportal.com';
|
|
40
|
+
|
|
41
|
+
// when
|
|
42
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
43
|
+
|
|
44
|
+
// then
|
|
45
|
+
assert.isTrue(validDomain);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@test
|
|
49
|
+
public async isValidDomain_Should_Return_True_When_DomainIs_Bachelorsportal(): Promise<void> {
|
|
50
|
+
// given
|
|
51
|
+
const providedDomain = 'https://www.bachelorsportal.com';
|
|
52
|
+
|
|
53
|
+
// when
|
|
54
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
55
|
+
|
|
56
|
+
// then
|
|
57
|
+
assert.isTrue(validDomain);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@test
|
|
61
|
+
public async isValidDomain_Should_Return_True_When_DomainIs_PhDportal(): Promise<void> {
|
|
62
|
+
// given
|
|
63
|
+
const providedDomain = 'https://www.phdportal.com';
|
|
64
|
+
|
|
65
|
+
// when
|
|
66
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
67
|
+
|
|
68
|
+
// then
|
|
69
|
+
assert.isTrue(validDomain);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@test
|
|
73
|
+
public async isValidDomain_Should_Return_True_When_DomainIs_PhDportalRC(): Promise<void> {
|
|
74
|
+
// given
|
|
75
|
+
const providedDomain = 'https://1554112850.rc.phdportal.fyi';
|
|
76
|
+
|
|
77
|
+
// when
|
|
78
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
79
|
+
|
|
80
|
+
// then
|
|
81
|
+
assert.isTrue(validDomain);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@test
|
|
85
|
+
public async isValidDomain_Should_Return_False_When_DomainIs_BabyShower(): Promise<void> {
|
|
86
|
+
// given
|
|
87
|
+
const providedDomain = 'https://www.babyshower.com';
|
|
88
|
+
|
|
89
|
+
// when
|
|
90
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
91
|
+
|
|
92
|
+
// then
|
|
93
|
+
assert.isFalse(validDomain);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@test
|
|
97
|
+
public async isValidDomain_Should_Return_False_When_DomainIs_Masterportal(): Promise<void> {
|
|
98
|
+
// given
|
|
99
|
+
const providedDomain = 'https://www.masterportal.com';
|
|
100
|
+
|
|
101
|
+
// when
|
|
102
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
103
|
+
|
|
104
|
+
// then
|
|
105
|
+
assert.isFalse(validDomain);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@test
|
|
109
|
+
public async isValidDomain_Should_Return_False_When_DomainIs_Mastersportal_With_SPDY(): Promise<void> {
|
|
110
|
+
// given
|
|
111
|
+
const providedDomain = 'spdy://www.mastersportal.com';
|
|
112
|
+
|
|
113
|
+
// when
|
|
114
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
115
|
+
|
|
116
|
+
// then
|
|
117
|
+
assert.isFalse(validDomain);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@test
|
|
121
|
+
public async isValidDomain_Should_Return_True_When_DomainIs_searchPageMicroFrontend(): Promise<void> {
|
|
122
|
+
// given
|
|
123
|
+
const providedDomain = 'https://search-page.prtl.co';
|
|
124
|
+
|
|
125
|
+
// when
|
|
126
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
127
|
+
|
|
128
|
+
// then
|
|
129
|
+
assert.isTrue(validDomain);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@test
|
|
133
|
+
public async isValidDomain_Should_Return_True_When_DomainIs_AdmissionTestPortalWithoutWWW(): Promise<void> {
|
|
134
|
+
// given
|
|
135
|
+
const providedDomain = 'https://admissiontestportal.com';
|
|
136
|
+
|
|
137
|
+
// when
|
|
138
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
139
|
+
|
|
140
|
+
// then
|
|
141
|
+
assert.isTrue(validDomain);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@test
|
|
145
|
+
public async isValidDomain_Should_Return_True_When_DomainIs_AdmissionTestPortalWithWWW(): Promise<void> {
|
|
146
|
+
// given
|
|
147
|
+
const providedDomain = 'https://www.admissiontestportal.com';
|
|
148
|
+
|
|
149
|
+
// when
|
|
150
|
+
const validDomain = await this.testInstance.isValidDomain(providedDomain);
|
|
151
|
+
|
|
152
|
+
// then
|
|
153
|
+
assert.isTrue(validDomain);
|
|
154
|
+
}
|
|
155
|
+
}
|
package/test/mocha.opts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "bin",
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"alwaysStrict": true,
|
|
7
|
+
"strictNullChecks": true,
|
|
8
|
+
"allowJs": false,
|
|
9
|
+
"allowUnreachableCode": false,
|
|
10
|
+
"allowUnusedLabels": false,
|
|
11
|
+
"noUnusedLocals": false,
|
|
12
|
+
"noUnusedParameters": false,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"noImplicitAny": false,
|
|
15
|
+
"noImplicitReturns": true,
|
|
16
|
+
"noImplicitThis": true,
|
|
17
|
+
"target": "ES6",
|
|
18
|
+
"module": "commonjs",
|
|
19
|
+
"experimentalDecorators": true,
|
|
20
|
+
"esModuleInterop": true,
|
|
21
|
+
"lib": [
|
|
22
|
+
"es2017",
|
|
23
|
+
"dom",
|
|
24
|
+
"es5",
|
|
25
|
+
"es6"
|
|
26
|
+
],
|
|
27
|
+
"types": [
|
|
28
|
+
"node"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"typeRoots": [
|
|
32
|
+
"node_modules/@types/"
|
|
33
|
+
],
|
|
34
|
+
"exclude": [
|
|
35
|
+
"node_modules",
|
|
36
|
+
"bin"
|
|
37
|
+
]
|
|
38
|
+
}
|
package/tslint.json
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"tslint:latest"
|
|
4
|
+
],
|
|
5
|
+
"rules": {
|
|
6
|
+
"ordered-imports": true,
|
|
7
|
+
"arrow-return-shorthand": true,
|
|
8
|
+
"callable-types": true,
|
|
9
|
+
"class-name": true,
|
|
10
|
+
"comment-format": [
|
|
11
|
+
true,
|
|
12
|
+
"check-space"
|
|
13
|
+
],
|
|
14
|
+
"curly": true,
|
|
15
|
+
"eofline": true,
|
|
16
|
+
"forin": true,
|
|
17
|
+
"import-spacing": false,
|
|
18
|
+
"indent": [
|
|
19
|
+
true,
|
|
20
|
+
"tabs"
|
|
21
|
+
],
|
|
22
|
+
"interface-over-type-literal": true,
|
|
23
|
+
"label-position": true,
|
|
24
|
+
"max-line-length": [
|
|
25
|
+
true,
|
|
26
|
+
200
|
|
27
|
+
],
|
|
28
|
+
"member-access": false,
|
|
29
|
+
"member-ordering": [
|
|
30
|
+
true,
|
|
31
|
+
{
|
|
32
|
+
"order": [
|
|
33
|
+
"static-field",
|
|
34
|
+
"instance-field",
|
|
35
|
+
"instance-method",
|
|
36
|
+
"static-method"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"no-arg": true,
|
|
41
|
+
"no-console": [
|
|
42
|
+
true,
|
|
43
|
+
"debug",
|
|
44
|
+
"info",
|
|
45
|
+
"time",
|
|
46
|
+
"timeEnd",
|
|
47
|
+
"trace"
|
|
48
|
+
],
|
|
49
|
+
"no-construct": true,
|
|
50
|
+
"no-debugger": true,
|
|
51
|
+
"no-duplicate-super": true,
|
|
52
|
+
"no-empty": false,
|
|
53
|
+
"no-empty-interface": true,
|
|
54
|
+
"no-eval": true,
|
|
55
|
+
"no-inferrable-types": [
|
|
56
|
+
true,
|
|
57
|
+
"ignore-params"
|
|
58
|
+
],
|
|
59
|
+
"no-misused-new": true,
|
|
60
|
+
"no-non-null-assertion": true,
|
|
61
|
+
"no-shadowed-variable": true,
|
|
62
|
+
"no-string-literal": false,
|
|
63
|
+
"no-string-throw": true,
|
|
64
|
+
"no-switch-case-fall-through": true,
|
|
65
|
+
"no-trailing-whitespace": true,
|
|
66
|
+
"no-unnecessary-initializer": true,
|
|
67
|
+
"no-unused-expression": true,
|
|
68
|
+
"no-use-before-declare": true,
|
|
69
|
+
"no-var-keyword": true,
|
|
70
|
+
"object-literal-sort-keys": false,
|
|
71
|
+
"one-line": [
|
|
72
|
+
true,
|
|
73
|
+
"check-open-brace",
|
|
74
|
+
"check-whitespace"
|
|
75
|
+
],
|
|
76
|
+
"prefer-const": false,
|
|
77
|
+
"radix": true,
|
|
78
|
+
"quotemark": [
|
|
79
|
+
true,
|
|
80
|
+
"single"
|
|
81
|
+
],
|
|
82
|
+
"semicolon": [
|
|
83
|
+
true,
|
|
84
|
+
"always"
|
|
85
|
+
],
|
|
86
|
+
"triple-equals": [
|
|
87
|
+
true,
|
|
88
|
+
"allow-null-check"
|
|
89
|
+
],
|
|
90
|
+
"typedef": [
|
|
91
|
+
true,
|
|
92
|
+
"call-signature",
|
|
93
|
+
"parameter",
|
|
94
|
+
"member-variable-declaration",
|
|
95
|
+
"property-declaration"
|
|
96
|
+
],
|
|
97
|
+
"typedef-whitespace": [
|
|
98
|
+
true,
|
|
99
|
+
{
|
|
100
|
+
"call-signature": "nospace",
|
|
101
|
+
"index-signature": "nospace",
|
|
102
|
+
"parameter": "nospace",
|
|
103
|
+
"property-declaration": "nospace",
|
|
104
|
+
"variable-declaration": "nospace"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"no-implicit-dependencies": [
|
|
108
|
+
false,
|
|
109
|
+
[
|
|
110
|
+
"mocha-typescript",
|
|
111
|
+
"chai",
|
|
112
|
+
"typemoq"
|
|
113
|
+
]
|
|
114
|
+
],
|
|
115
|
+
"unified-signatures": true,
|
|
116
|
+
"variable-name": false,
|
|
117
|
+
"whitespace": [
|
|
118
|
+
true,
|
|
119
|
+
"check-branch",
|
|
120
|
+
"check-decl",
|
|
121
|
+
"check-operator",
|
|
122
|
+
"check-separator",
|
|
123
|
+
"check-type",
|
|
124
|
+
"check-typecast"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"linterOptions": {
|
|
128
|
+
"exclude": [
|
|
129
|
+
"node_modules/**"
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|