Skip to content

Commit 6a8cafc

Browse files
authored
Feature/security (#21)
* Add security management
1 parent 6d252fe commit 6a8cafc

File tree

13 files changed

+190
-48
lines changed

13 files changed

+190
-48
lines changed

.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
NEO4J_HOST='bolt://localhost'
2+
DEBUG=@neo4j/graphql:*
3+
GRAPH_SECURITY_ENABLED=true
4+
GRAPH_TOKEN_ROLES_PATH=resource_access.graph-graphql.roles
5+
GRAPH_TOKEN_AUTHORITY=http://localhost:9080/auth/realms/dependencies

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ This image is base on **Linux**.
3131

3232
You can configure container by setting environment variables.
3333

34-
| Environment variable | Comment | default value |
34+
| Environment variable | Comment | sample value |
3535
|------------------------- | :--------------------------|-------------------- |
3636
| NEO4J_HOST | Noe4j database uri | bolt://localhost |
37+
| GRAPH_SECURITY_ENABLED | Enable jwt validation | bolt://localhost |
38+
| GRAPH_TOKEN_ROLES_PATH | Roles path inside jwt | resource_access.graph-graphql.roles |
39+
| GRAPH_TOKEN_AUTHORITY | Authority for token validation | http://localhost:9080/auth/realms/dependencies |
40+
| DEBUG | Activate @neo4j/graphql traces | @neo4j/graphql:* |
3741

3842
Port exposed by Container:
3943

doc/releases/v2.0.0.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
### Changes
2+
- Add security management (token validation)
3+
- Docker image: ARM architecture support (linux/arm64, linux/arm/v7)
4+
- Migrating from "neo4j-graphql-js" (deprecated) to "@neo4j/graphql"
5+
6+
Docker image available on:
7+
- [github](https://github.com/xclemence/dependencies-graph-graphql/packages)
8+
- [dockerhub](https://hub.docker.com/r/xclemence/dependencies-graph-graphql)

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"name": "dependencies-graph-graphql",
3-
"version": "1.1.0",
3+
"version": "2.0.0",
44
"description": "",
5-
"main": "index.js",
5+
"main": "server.js",
66
"scripts": {
77
"start": "node dotenv/config 'dist/server.js'",
8-
"build": "tsc -p . && ncp src/definitions dist/definitions",
8+
"build": "tsc -p . && ncp src/definitions dist/definitions && ncp src/definitions-rights dist/definitions-rights",
99
"dev": "nodemon --exec \"yarn ts-node\" src/server.ts -e ts,graphql ",
10-
"ts-node": "ts-node"
10+
"ts-node": "ts-node "
1111
},
1212
"repository": {
1313
"type": "git",
@@ -24,6 +24,7 @@
2424
"@types/compression": "^1.7.0",
2525
"@types/dotenv": "^8.2.0",
2626
"@types/express": "^4.17.12",
27+
"@types/express-jwt": "^6.0.1",
2728
"@types/graphql-depth-limit": "^1.1.2",
2829
"@types/lodash": "^4.14.170",
2930
"@types/node": "^15.12.1",
@@ -40,6 +41,7 @@
4041
"@neo4j/graphql-ogm": "^1.0.2",
4142
"apollo-server": "^2.25.0",
4243
"apollo-server-express": "^2.25.0",
44+
"axios": "^0.21.1",
4345
"compression": "^1.7.4",
4446
"cors": "^2.8.5",
4547
"dotenv": "^10.0.0",

src/apollo-server.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import './env';
2+
3+
import depthLimit from 'graphql-depth-limit';
4+
import { driver } from 'neo4j-driver';
5+
import { OGM } from '@neo4j/graphql-ogm';
6+
7+
import { getTypesFiles, getResolvers } from './schema';
8+
import { Context } from './types/context';
9+
import { Neo4jGraphQL } from '@neo4j/graphql';
10+
import { Neo4jGraphQLConfig } from '@neo4j/graphql/dist/classes';
11+
import { ApolloServer } from 'apollo-server-express';
12+
13+
export function createApolloServerWithToken(
14+
neo4jHost: string,
15+
tokenValidation: { publicKey: string, rolesPath: string | undefined }
16+
): ApolloServer {
17+
18+
return createApolloServerBase(
19+
neo4jHost,
20+
true,
21+
{
22+
jwt: {
23+
secret: tokenValidation.publicKey,
24+
rolesPath: tokenValidation.rolesPath
25+
}
26+
}
27+
);
28+
}
29+
30+
export function createApolloServerNoToken(neo4jHost: string): ApolloServer {
31+
return createApolloServerBase(neo4jHost, false);
32+
}
33+
34+
35+
function createApolloServerBase(
36+
neo4jHost: string,
37+
enabledSecurity: boolean,
38+
config?: Neo4jGraphQLConfig
39+
): ApolloServer {
40+
41+
const driverInstance = driver(neo4jHost);
42+
const typesFiles = getTypesFiles(enabledSecurity);
43+
44+
const ogm = new OGM({
45+
typeDefs: typesFiles,
46+
driver: driverInstance,
47+
});
48+
49+
const neo4jGraphQL = new Neo4jGraphQL({
50+
typeDefs: typesFiles,
51+
resolvers: getResolvers(),
52+
config
53+
});
54+
55+
return new ApolloServer({
56+
schema: neo4jGraphQL.schema,
57+
validationRules: [depthLimit(10)],
58+
context: ({ req }) => ({ req, ogm, driver: driverInstance } as Context)
59+
});
60+
}

src/app-process-env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ declare global {
33
interface ProcessEnv {
44
NEO4J_HOST: string;
55
NODE_ENV: 'development' | 'production';
6+
GRAPH_TOKEN_ROLES_PATH: string;
7+
GRAPH_SECURITY_ENABLED: string;
8+
GRAPH_TOKEN_AUTHORITY: string;
69
}
710
}
811
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type Mutation {
2+
removeAssembly(assemblyName: String!): Assembly @auth(rules: [{ operations: [DELETE], roles: ["write"] }])
3+
}

src/keycloak.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import axios from 'axios';
2+
3+
export async function getPublicKey(server: string): Promise<string> {
4+
const response = await axios.get(server);
5+
return `-----BEGIN PUBLIC KEY-----\r\n${response.data.public_key}\r\n-----END PUBLIC KEY-----`;
6+
}
7+

src/resolvers/main-resolver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ const mainResolver = {
22
Query: {
33
isAlive() {
44
return true;
5-
},
5+
}
66
},
77
Mutation: {
8-
hello: (parent: any, parameters: { name: string }): string => {
8+
hello: (_: any, parameters: { name: string }): string => {
99
return `Hello ${parameters.name}!`;
1010
}
1111
}

src/schema.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
import { loadFilesSync } from '@graphql-tools/load-files';
22
import { mergeResolvers } from '@graphql-tools/merge';
33
import path from 'path';
4-
import { Neo4jGraphQL } from '@neo4j/graphql';
54

6-
export const typesFiles = loadFilesSync(path.join(__dirname, 'definitions/*.graphql'))
5+
export function getTypesFiles(enabledSecurity: boolean): any[] {
6+
let typesFilesPatterns = [
7+
path.join(__dirname, 'definitions/*.graphql'),
8+
];
79

8-
const resolverPatterns = [
9-
path.join(__dirname, 'resolvers/*.ts'),
10-
path.join(__dirname, 'resolvers/*.js')
11-
];
12-
13-
const resolversFiles = loadFilesSync(resolverPatterns);
14-
const resolvers = mergeResolvers(resolversFiles);
10+
if (enabledSecurity) {
11+
typesFilesPatterns = [
12+
...typesFilesPatterns,
13+
path.join(__dirname, 'definitions-rights/*.graphql')
14+
];
15+
}
1516

16-
const neo4jGraphQL = new Neo4jGraphQL({
17-
typeDefs: typesFiles,
18-
resolvers,
19-
});
17+
return loadFilesSync(typesFilesPatterns);
18+
}
2019

21-
const schema = neo4jGraphQL.schema;
20+
export function getResolvers(): any {
21+
const resolverPatterns = [
22+
path.join(__dirname, 'resolvers/*.ts'),
23+
path.join(__dirname, 'resolvers/*.js')
24+
];
2225

23-
export default schema;
26+
const resolversFiles = loadFilesSync(resolverPatterns);
27+
return mergeResolvers(resolversFiles);
28+
}

src/server.ts

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,64 @@
11
import './env';
22

3-
import { ApolloServer } from 'apollo-server-express';
43
import compression from 'compression';
54
import cors from 'cors';
65
import express from 'express';
7-
import depthLimit from 'graphql-depth-limit';
86
import { createServer } from 'http';
9-
import { driver } from 'neo4j-driver';
10-
import { OGM } from '@neo4j/graphql-ogm';
117

12-
import schema, { typesFiles } from './schema';
13-
import { Context } from './types/context';
8+
import { getPublicKey } from './keycloak';
9+
import { createApolloServerNoToken, createApolloServerWithToken } from './apollo-server';
10+
import { ApolloServer } from 'apollo-server-express';
1411

1512
const port = 4001;
1613

17-
if(!process.env.NEO4J_HOST) {
14+
if (!process.env.NEO4J_HOST) {
1815
throw new Error('Unexpected error: Missing host name');
1916
}
2017

21-
const driverInstance = driver(process.env.NEO4J_HOST);
18+
const host = process.env.NEO4J_HOST;
19+
const rolesPath = process.env.GRAPH_TOKEN_ROLES_PATH;
20+
const securityEnabled = process.env.GRAPH_SECURITY_ENABLED === 'true';
21+
const tokenAuthority = process.env.GRAPH_TOKEN_AUTHORITY;
22+
23+
(async () => {
24+
try {
25+
26+
const app = express();
27+
28+
let server: ApolloServer;
29+
30+
if (securityEnabled) {
31+
32+
if (!tokenAuthority) {
33+
throw new Error('Unexpected error: Missing token Authority');
34+
}
35+
36+
if (!rolesPath) {
37+
throw new Error('Unexpected error: Missing token roles path');
38+
}
39+
40+
const publicKey = await getPublicKey(tokenAuthority);
41+
server = createApolloServerWithToken(host, { publicKey, rolesPath });
42+
}
43+
else {
44+
server = createApolloServerNoToken(host);
45+
}
2246

23-
const ogm = new OGM({
24-
typeDefs: typesFiles,
25-
driver: driverInstance,
26-
});
47+
app.use(cors());
48+
app.use(compression());
2749

28-
const server = new ApolloServer({
29-
schema,
30-
validationRules: [depthLimit(10)],
31-
context: () => ({ ogm, driver: driverInstance } as Context),
32-
});
50+
server.applyMiddleware({ app, path: '/graphql' });
3351

34-
const app = express();
52+
const httpServer = createServer(app);
3553

36-
app.use(cors());
37-
app.use(compression());
54+
httpServer.listen(
55+
{ port },
56+
(): void => console.log(`\nGraphQL is now running on http://localhost:${port}/graphql`)
57+
);
3858

39-
server.applyMiddleware({ app, path: '/graphql' });
59+
} catch(error: any) {
60+
console.error(error);
61+
}
4062

41-
const httpServer = createServer(app);
63+
})();
4264

43-
httpServer.listen(
44-
{ port },
45-
(): void => console.log(`\nGraphQL is now running on http://localhost:${port}/graphql`)
46-
);

src/types/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ import { OGM } from '@neo4j/graphql-ogm';
44
export type Context = {
55
ogm: OGM;
66
driver: Driver;
7+
req: any;
78
};

yarn.lock

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,14 @@
323323
dependencies:
324324
dotenv "*"
325325

326+
"@types/express-jwt@^6.0.1":
327+
version "6.0.1"
328+
resolved "https://registry.yarnpkg.com/@types/express-jwt/-/express-jwt-6.0.1.tgz#616cbd149438345084c41544d7dd49cfeca60079"
329+
integrity sha512-zB/oXzS8/NTWUzAG343frlqUrsygHPeyYMVcbJ8YYk7rF1G15eUapPgWh0HdeFi51ajFkkUOU+Q540z1Eu4hJQ==
330+
dependencies:
331+
"@types/express" "*"
332+
"@types/express-unless" "*"
333+
326334
"@types/express-serve-static-core@4.17.19":
327335
version "4.17.19"
328336
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz#00acfc1632e729acac4f1530e9e16f6dd1508a1d"
@@ -341,6 +349,13 @@
341349
"@types/qs" "*"
342350
"@types/range-parser" "*"
343351

352+
"@types/express-unless@*":
353+
version "0.5.1"
354+
resolved "https://registry.yarnpkg.com/@types/express-unless/-/express-unless-0.5.1.tgz#4f440b905e42bbf53382b8207bc337dc5ff9fd1f"
355+
integrity sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==
356+
dependencies:
357+
"@types/express" "*"
358+
344359
"@types/express@*", "@types/express@^4.17.12":
345360
version "4.17.12"
346361
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.12.tgz#4bc1bf3cd0cfe6d3f6f2853648b40db7d54de350"
@@ -718,6 +733,13 @@ available-typed-arrays@^1.0.2:
718733
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz#9e0ae84ecff20caae6a94a1c3bc39b955649b7a9"
719734
integrity sha512-SA5mXJWrId1TaQjfxUYghbqQ/hYioKmLJvPJyDuYRtXXenFNMjj4hSSt1Cf1xsuXSXrtxrVC5Ot4eU6cOtBDdA==
720735

736+
axios@^0.21.1:
737+
version "0.21.1"
738+
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
739+
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
740+
dependencies:
741+
follow-redirects "^1.10.0"
742+
721743
backo2@^1.0.2:
722744
version "1.0.2"
723745
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
@@ -1291,6 +1313,11 @@ finalhandler@~1.1.2:
12911313
statuses "~1.5.0"
12921314
unpipe "~1.0.0"
12931315

1316+
follow-redirects@^1.10.0:
1317+
version "1.14.1"
1318+
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
1319+
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
1320+
12941321
for-each@^0.3.3:
12951322
version "0.3.3"
12961323
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"

0 commit comments

Comments
 (0)