Skip to content

Commit f970e04

Browse files
authored
Merge pull request #10 from overture-stack/authorization_switch_impl
Authorization switch impl
2 parents 91b0c9a + 39d02d5 commit f970e04

20 files changed

+653
-181
lines changed

.dockerignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
target
2+
Dockerfile
3+
docker-compose.yml
4+
README.md
5+
Makefile
6+
.*
7+
!.mvn
8+
npm-debug.log
9+
node_modules
10+
dist
11+
coverage
12+
*.drawio

.env.example

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11

2-
NODE_ENV=development
32
PORT=
43

5-
#DB connection details
4+
# --- DB connection details ---
65
DB_HOST=
76
DB_PORT=
87
DB_USERNAME=
@@ -11,9 +10,28 @@ DB_NAME=
1110
DB_SYNCHRONIZE=true
1211
DB_SCHEMA:
1312

14-
#list of permissions.
15-
SCOPES= []
16-
EGO_URL: "https://ego.url"
13+
# ----- Auth -----
14+
15+
# client id and secret as present is keycloak or ego for this service
16+
CLIENT_ID=
17+
CLIENT_SECRET=
18+
19+
# scopes as defined in EGO or KEYCLOAK eg. DICTIONARY.WRITE (For Keycloak DICTIONAY is the rsname i.e resource name and WRITE is the scope)
20+
# AUTH_WRITE_SCOPES property can be empty or absent for AUTH_STRATEGY = NONE.
21+
AUTH_WRITE_SCOPES=[]
22+
AUTH_STRATEGY= # valid values -> EGO or KEYCLOAK or NONE
23+
AUTH_SERVER_URL: # "https://ego.url" OR "http://localhost/realms/keycloak-realm". This property will be empty or absent for AUTH_STRATEGY = NONE
24+
25+
# SECURED_API can be used to specify which of the APIs need authentication.
26+
# Can be empty or absent for AUTH_STRATEGY = NONE.
27+
# If omitted for AUTH_STRATEGY != NONE then all apis will need authentication by default.
28+
SECURED_API = ["CREATE", "FIND"]
29+
AUTH_PUBLICKEY_CACHE = 1d
30+
31+
32+
33+
34+
# ----- Entity Details -----
1735

1836
# list of valid entity types
1937
ENTITY_LIST=["donor", "specimen", "sample"]
@@ -25,12 +43,10 @@ REQUEST_ROUTE=/:column1/:column2/:column3
2543
DB_SEQUENCES=
2644

2745
# Repeat the below set for each entity type.
28-
2946
# Sequences in defaultValue can be given as "nextval('argo.specimen_seq')" or "0" or "'A'".
3047
# The unique property under the columns array is not mandatory - default value for unique is false
31-
# The index property (in the end) holds the columns for composite unique index creation
32-
<ENTITY_TYPE>_SCHEMA={"tablename": "tablename", "columns": [{"name": "entityId","type": "varchar", "defaultValue": "default-value or sequence call", "unique": true}, {"name": "column1", "type": "db-datatype", "defaultValue": "default-value or sequence call", "unique": false},{"name": "column2", "type": "db-datatype", "defaultValue": "default-value or sequence call"}], "index": ["column1", "column2"]}
48+
# The index property (in the end) holds the columns for composite unique index creation. Note: This is an array of list of columns where each list constitutes a composite index.
49+
<ENTITY_TYPE>_SCHEMA={"tablename": "tablename", "columns": [{"name": "entityId","type": "varchar", "defaultValue": "default-value or sequence call", "unique": true}, {"name": "column1", "type": "db-datatype", "defaultValue": "default-value or sequence call", "unique": false},{"name": "column2", "type": "db-datatype", "defaultValue": "default-value or sequence call"}], "index": [[column list for composite index 1], [column list for composite index 2]]}
3350
# <ENTITY_TYPE>_SEARCH is the structure of the request/search-criteria/saved-entity. Should match column names in <ENTITY_TYPE>_SCHEMA.
3451
# values against each of the keys should remain blank.
3552
<ENTITY_TYPE>_SEARCH>={"column1":"", "column2":"", "column3": ""}
36-

Dockerfile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
FROM node:20-alpine as builder
2+
# Create app directory
3+
WORKDIR /app
4+
RUN chown -R node:node /app
5+
USER node
6+
# copy the package json and install first to optimize docker cache for node modules
7+
COPY package.json /app/
8+
COPY package-lock.json /app/
9+
RUN npm ci
10+
COPY . ./
11+
RUN npm run build
12+
13+
# Runtime image
14+
FROM node:20-alpine
15+
ENV APP_UID=9999
16+
ENV APP_GID=9999
17+
RUN apk --no-cache add shadow
18+
RUN groupmod -g $APP_GID node
19+
RUN usermod -u $APP_UID -g $APP_GID node
20+
WORKDIR /app
21+
RUN chown -R node:node /app
22+
USER node
23+
RUN mkdir dist && mkdir node_modules
24+
COPY --from=builder /app/dist ./dist
25+
COPY --from=builder /app/node_modules ./node_modules
26+
COPY --from=builder /app/package.json .
27+
COPY --from=builder /app/package-lock.json .
28+
EXPOSE 3221
29+
CMD node dist/server.js

Jenkinsfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@Library(value='jenkins-pipeline-library@master', changelog=false) _
2+
pipelineOVERTUREIDGeneration()

package-lock.json

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"jsonwebtoken": "^9.0.2",
4747
"memoizee": "^0.4.15",
4848
"ms": "^2.1.3",
49+
"openid-client": "^5.6.5",
4950
"pg": "^8.11.3",
5051
"swagger-ui-express": "^5.0.0",
5152
"typeorm": "^0.3.19",

src/config-validator.ts

Lines changed: 87 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { z, ZodRecord, ZodString } from 'zod';
2-
import { SchemaInfo } from './middlewares/datasource';
1+
import {z, ZodEnum, ZodRecord, ZodString} from 'zod';
2+
import { SchemaInfo } from './middlewares/datasource.js';
33

44
function getRequiredEnvVar(name: string) {
55
const property = process.env[name];
@@ -27,6 +27,15 @@ export const getRequiredString = (name: string): string => {
2727
return stringValue.data;
2828
};
2929

30+
export const getString = (name: string): string => {
31+
const value = process.env[name] || '';
32+
const stringValue = z.string().safeParse(value);
33+
if (!stringValue.success) {
34+
throw new Error(`Environment variable ${name} in config is not a valid string`);
35+
}
36+
return stringValue.data;
37+
};
38+
3039
export const getUrl = (name: string): string => {
3140
const value = process.env[name] || 'a://';
3241
const stringValue = z.string().url().safeParse(value);
@@ -56,73 +65,91 @@ export const getArray = (name: string): ZodString['_output'][] => {
5665
} catch (e) {
5766
throw new Error(`Environment variable ${name} is not valid. Please provide an array of strings.`)
5867
}
68+
};
69+
70+
export const getEnum = <T extends [string, ...string[]]>(name: string, zEnum: ZodEnum<T>): z.infer<ZodEnum<T>> => {
71+
const stringValue = zEnum.safeParse(process.env[name]);
72+
if (!stringValue.success) {
73+
throw new Error(`Environment variable ${name} has an invalid value. Valid values are: ${zEnum.options}`);
74+
}
75+
return stringValue.data;
76+
};
5977

78+
export const validateArrayContents = <T extends [string, ...string[]]>(name: string, zEnum: ZodEnum<T>, value: string | undefined = ''): z.infer<ZodEnum<T>> => {
79+
const stringValue = zEnum.safeParse(value);
80+
if (!stringValue.success) {
81+
throw new Error(`Environment variable ${name} has an invalid value. Valid values are: ${zEnum.options}`);
82+
}
83+
return stringValue.data;
6084
};
6185

86+
6287
export const getRecord = (name: string): ZodRecord<ZodString, ZodString> => {
6388
const config_entry = `${name.toUpperCase()}_SEARCH`;
6489
return z.record(
65-
z.string({ invalid_type_error: 'Invalid key in ' + config_entry + '. Should be a string' }),
66-
z.string({ invalid_type_error: 'Invalid value in ' + config_entry + '. Should be a string' }),
90+
z.string({ invalid_type_error: `Invalid key in ${config_entry}. Should be a string` }),
91+
z.string({ invalid_type_error: `Invalid value in ${config_entry}. Should be a string` }),
6792
{ invalid_type_error: `Environment variable ${config_entry} is invalid or missing` },
6893
);
6994
};
7095

7196
export const getSchemaDef = (name: string): z.ZodType<SchemaInfo> => {
72-
const config_entry = name.toUpperCase() + `_SCHEMA`;
73-
return z.object(
74-
{
75-
tablename: z
76-
.string({
77-
required_error: 'tablename in ' + config_entry + ' is required',
78-
invalid_type_error: 'tablename in ' + config_entry + ' is invalid',
79-
})
80-
.trim()
81-
.min(1, { message: 'tablename in ' + config_entry + ' cannot be empty' }),
82-
columns: z
83-
.array(
84-
z.object({
85-
name: z.string({ required_error: 'column `name` in ' + config_entry + ' is missing' }),
86-
type: z.union(
87-
[
88-
z.literal('float'),
89-
z.literal('varchar'),
90-
z.literal('number'),
91-
z.literal('date'),
92-
z.literal('varchar2'),
93-
z.literal('string'),
94-
z.literal('timestamp'),
95-
z.literal('double'),
96-
z.literal('boolean'),
97-
],
98-
{
99-
invalid_type_error: '`type` in ' + config_entry + ' has an invalid value.',
100-
required_error: '`type` in ' + config_entry + ' is required.',
101-
},
102-
),
103-
defaultValue: z
104-
.string({ invalid_type_error: 'value for `defaultValue` in ' + config_entry + ' is invalid' })
105-
.optional(),
106-
unique: z.boolean({ invalid_type_error: 'value for `unique` in ' + config_entry + ' is invalid' }).optional(),
107-
}),
108-
{
109-
invalid_type_error: '`columns` in ' + config_entry + ' should be an array',
110-
},
111-
)
112-
.nonempty({ message: '`column` array in ' + config_entry + ' should not be empty' }),
113-
index: z
114-
.array(
115-
z.string({
116-
invalid_type_error: '`index` in ' + config_entry + ' is invalid. It should be a list of column name strings',
117-
}),
118-
{
119-
invalid_type_error: '`index` in ' + config_entry + ' is invalid. It should be a list of column name strings',
120-
required_error: '`index` in ' + config_entry + ' is missing.',
121-
},
122-
)
123-
.nonempty({ message: '`index` in ' + config_entry + ' is required' }),
124-
},
125-
{ invalid_type_error: 'Schema definition missing for entity <' + name + '> in the ENTITY_LIST' },
126-
);
97+
const config_entry = name.toUpperCase() + `_SCHEMA`;
98+
return z.object(
99+
{
100+
tablename: z
101+
.string({
102+
required_error: 'table name in ' + config_entry + ' is required',
103+
invalid_type_error: 'table name in ' + config_entry + ' is invalid',
104+
})
105+
.trim()
106+
.min(1, { message: 'table name in ' + config_entry + ' cannot be empty' }),
107+
columns: z
108+
.array(
109+
z.object({
110+
name: z.string({ required_error: 'column name in ' + config_entry + ' is missing' }),
111+
type: z.union([
112+
z.literal('float'),
113+
z.literal('varchar'),
114+
z.literal('number'),
115+
z.literal('date'),
116+
z.literal('varchar2'),
117+
z.literal('string'),
118+
z.literal('timestamp'),
119+
z.literal('double'),
120+
z.literal('boolean'),
121+
]),
122+
defaultValue: z
123+
.string({ invalid_type_error: 'value for defaultValue in ' + config_entry + ' is invalid' })
124+
.optional(),
125+
unique: z.boolean({ invalid_type_error: 'value for unique in ' + config_entry + ' is invalid' }).optional(),
126+
}),
127+
{
128+
invalid_type_error: 'columns in ' + config_entry + ' should be an array',
129+
},
130+
)
131+
.nonempty({ message: 'column array in ' + config_entry + ' should not be empty' }),
132+
index: z
133+
.array(
134+
z.array(
135+
z.string({
136+
invalid_type_error:
137+
'index in ' + config_entry + ' is invalid. It should be a list of column name strings',
138+
}),
139+
{
140+
invalid_type_error:
141+
'index in ' + config_entry + ' is invalid. It should be a list of column name strings',
142+
required_error: 'index in ' + config_entry + ' is missing.',
143+
},
144+
),
145+
{
146+
invalid_type_error:
147+
'index in ' + config_entry + ' is invalid. It should be a list of list of column name strings',
148+
required_error: 'index in ' + config_entry + ' is missing.',
149+
},
150+
)
151+
.nonempty({ message: 'index in ' + config_entry + ' is required' }),
152+
},
153+
{ invalid_type_error: 'Schema definition missing for entity <' + name + '> in the ENTITY_LIST' },
154+
);
127155
};
128-

0 commit comments

Comments
 (0)