diff --git a/.eslintrc.json b/.eslintrc.json index 0517186..206fce3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,8 +24,9 @@ "max-classes-per-file": 0, "no-var-requires": 0, "array-type": 0, - "semi": 1, - "semi-style": ["error", "last"] + "semi": ["error", "always"], + "semi-style": ["error", "last"], + "no-extra-semi": "off" } - + } diff --git a/.gitignore b/.gitignore index 76add87..02551b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -dist \ No newline at end of file +dist +.env +.idea diff --git a/package-lock.json b/package-lock.json index d5e0b5a..a8762f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -306,6 +306,15 @@ "@types/express": "*" } }, + "@types/crc": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/crc/-/crc-3.4.0.tgz", + "integrity": "sha1-I2a+tDmc1zSzPkLHrICVduYX1Io=", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/express": { "version": "4.17.8", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.8.tgz", @@ -2071,6 +2080,14 @@ "vary": "^1" } }, + "crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "requires": { + "buffer": "^5.1.0" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4958,6 +4975,11 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, + "messagepack": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/messagepack/-/messagepack-1.1.12.tgz", + "integrity": "sha512-pNB6K4q4VMLRXdvlGZkTtQhmKFntvLisnOQnL0VhKpZooL8B8Wsv5TXuidIJil0bCH6V172p3+Onfyow0usPYQ==" + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -5893,6 +5915,45 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise-duplex": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/promise-duplex/-/promise-duplex-6.0.0.tgz", + "integrity": "sha512-ZL7rquzjTFzInDBeWYcsT+qddolNvzigahk6MI6qLSbQvlyRRCJkU3JztgaVunzvkH28smRa2Qu/cY9RXtSkgA==", + "requires": { + "core-js": "^3.6.5", + "promise-readable": "^6.0.0", + "promise-writable": "^6.0.0" + } + }, + "promise-readable": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/promise-readable/-/promise-readable-6.0.0.tgz", + "integrity": "sha512-5NxtmUswijvX5cAM0zPSy6yiCXH/eKBpiiBq6JfAUrmngMquMbzcBhF2qA+ocs4rYYKdvAfv3cOvZxADLtL1CA==", + "requires": { + "core-js": "^3.6.5" + } + }, + "promise-socket": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/promise-socket/-/promise-socket-7.0.0.tgz", + "integrity": "sha512-Oic9BrxmcHOPEnzKp2Js+ehFyvsbd0WxsE5khweCTHuRvdzbXjHUZmSDT6F9TW8SIkAJ0lCzoHjMYnb0WQJPiw==", + "requires": { + "promise-duplex": "^6.0.0", + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + } + } + }, + "promise-writable": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/promise-writable/-/promise-writable-6.0.0.tgz", + "integrity": "sha512-b81zre/itgJFS7dwWzIdKNVVqvLiUxYRS/wolUB0H1YY/tAaS146XGKa4Q/5wCbsnXLyn0MCeV6f8HHe4iUHLg==" + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", diff --git a/package.json b/package.json index cc9fd74..8d895ba 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,19 @@ "gulp-eslint": "^6.0.0", "gulp-nodemon": "^2.5.0", "gulp-typescript": "^6.0.0-alpha.1", - "typescript": "^4.0.2" + "typescript": "^4.0.2", + "@types/crc": "^3.4.0" }, "dependencies": { "apollo-server": "^2.17.0", + "apollo-server-express": "^2.17.0", + "crc": "^3.8.0", + "dotenv": "^8.2.0", + "express": "^4.17.1", "graphql": "^15.3.0", + "messagepack": "^1.1.12", "pg": "^8.3.3", + "promise-socket": "^7.0.0", "reflect-metadata": "^0.1.13", "typeorm": "^0.2.26" } diff --git a/src/datasources/db/cargobikeAPI.ts b/src/datasources/db/cargobikeAPI.ts index 1a3709d..f23571c 100644 --- a/src/datasources/db/cargobikeAPI.ts +++ b/src/datasources/db/cargobikeAPI.ts @@ -1,14 +1,14 @@ -import { DataSource } from 'apollo-datasource' -import { getConnection, Connection } from 'typeorm' -import { CargoBike } from '../../model/CargoBike' +import { DataSource } from 'apollo-datasource'; +import { getConnection, Connection } from 'typeorm'; +import { CargoBike } from '../../model/CargoBike'; /** * extended datasource to feed resolvers with data about cargoBikes */ export class CargoBikeAPI extends DataSource { connection : Connection constructor () { - super() - this.connection = getConnection() + super(); + this.connection = getConnection(); } /** @@ -18,15 +18,15 @@ export class CargoBikeAPI extends DataSource { return { id, name: token - } + }; } async updateBike ({ id, token, name }:{id:any, token: string, name: string }) { - const bike = new CargoBike() - bike.id = id - bike.description = token - bike.name = name - this.connection.manager.save(bike) + const bike = new CargoBike(); + bike.id = id; + bike.description = token; + bike.name = name; + await this.connection.manager.save(bike); return { success: true, message: token, @@ -34,6 +34,6 @@ export class CargoBikeAPI extends DataSource { id, name } - } + }; } } diff --git a/src/datasources/userserver/message.ts b/src/datasources/userserver/message.ts new file mode 100644 index 0000000..547d3f9 --- /dev/null +++ b/src/datasources/userserver/message.ts @@ -0,0 +1,43 @@ +import { encode, decode } from 'messagepack'; +import { Method } from './method'; +import { crc32 } from 'crc'; + +export class RPCMessage { + private readonly method: Method + readonly data: T + + constructor (method: Method, data: any) { + this.method = method; + this.data = data; + } + + static fromBuffer (raw: Buffer): RPCMessage { + const length = raw.readUInt32BE(); + if (raw.length !== length) { + throw new Error('Invalid Buffer length'); + } + const crcNum = raw.readUInt32BE(length - 4); + + if (crc32(raw.slice(0, length - 4)) !== crcNum) { + throw new Error('Validation check failed'); + } + const method = raw.readUInt32BE(4); + const msgData = decode(raw.slice(8, length)); + + return new RPCMessage(method, msgData); + } + + public toBuffer (): Buffer { + const msgData = encode(this.data); + const length = msgData.length + 12; + const buffer = Buffer.alloc(length - 4); + buffer.writeUInt32BE(length); + buffer.writeUInt32BE(this.method, 4); + buffer.fill(msgData, 8); + const crcNum = crc32(buffer); + const resultBuffer = Buffer.alloc(length, buffer); + resultBuffer.writeUInt32BE(crcNum, length - 4); + + return resultBuffer; + } +} diff --git a/src/datasources/userserver/method.ts b/src/datasources/userserver/method.ts new file mode 100644 index 0000000..70ec609 --- /dev/null +++ b/src/datasources/userserver/method.ts @@ -0,0 +1,11 @@ +/* eslint no-unused-vars: 0 */ +export enum Method { + Null = 0x00, + Error = 0x0F_0F_0F_0F, + Info = 0x49_4e_46_4f, + ValidateToken = 0x56_41_4c_49, + GetRoles = 0x52_4f_4c_45, + GetRolePermissions = 0x50_45_52_4d, + CreateRole = 0x43_52_4f_4c, + CreatePermissions = 0x43_50_45_52 +} diff --git a/src/datasources/userserver/permission.ts b/src/datasources/userserver/permission.ts new file mode 100644 index 0000000..a7a639a --- /dev/null +++ b/src/datasources/userserver/permission.ts @@ -0,0 +1,17 @@ +/* eslint no-unused-vars: 0 */ +export enum Permission { + ReadBike = 'BIKE_READ', + WriteBike = 'BIKE_WRITE', +} + +// Permissions where the creation will be requested on startup +export const requiredPermissions = [ + { + name: Permission.ReadBike, + description: 'Allows to read of bike information' + }, + { + name: Permission.WriteBike, + description: 'Allows the modification of bike information' + } +]; diff --git a/src/datasources/userserver/responses.ts b/src/datasources/userserver/responses.ts new file mode 100644 index 0000000..9da667c --- /dev/null +++ b/src/datasources/userserver/responses.ts @@ -0,0 +1,13 @@ +// Response in the form of [valid, ttl] +export type ValidateTokenResponse = [boolean, number] + +// Info in the form of [name, binary, description, request_structure][] +export type GetInfoResponse = [string, string, string][] + +// Role array in the form of [id, name, description][] +export type GetRolesResponse = [number, string, string][] + +// Permissions map where each roleId maps to an array of permissions +export type GetRolesPermissionsResponse = {[id: string]: [number, string, string][]} + +export type CreateRoleResponse = [number, string, string] diff --git a/src/datasources/userserver/userserviceAPI.ts b/src/datasources/userserver/userserviceAPI.ts index 2dede6f..09174cd 100644 --- a/src/datasources/userserver/userserviceAPI.ts +++ b/src/datasources/userserver/userserviceAPI.ts @@ -1,13 +1,127 @@ -import { DataSource } from 'apollo-datasource' +import { DataSource } from 'apollo-datasource'; +import { Socket } from 'net'; +import { PromiseSocket } from 'promise-socket'; +import { RPCMessage } from './message'; +import { Method } from './method'; +import { + CreateRoleResponse, + GetInfoResponse, + GetRolesPermissionsResponse, + GetRolesResponse, + ValidateTokenResponse +} from './responses'; +import { requiredPermissions } from './permission'; /** * fetches datafrom user server, especially validates user tokens */ export class UserServerAPI extends DataSource { + port: number + host: string + + constructor (address: string) { + super(); + const parts = address.split(':'); + this.host = parts[0]; + if (parts.length === 2) { + this.port = Number(parts[1]); + } else { + this.port = 5000; + } + } + + /** + * Returns the information about all available rpc methods + */ + async getInfo (): Promise { + const response = await this.send(new RPCMessage(Method.Info, null)); + + return response.data; + } + + /** + * Creates required API permissions + */ + async createDefinedPermissions () { + await this.send(new RPCMessage(Method.CreatePermissions, { permissions: requiredPermissions })); + } + + /** + * Creates a new role with the given parameters + * @param name - the name of the role + * @param description - a description of the role + * @param permissionIDs - an array of IDs the role is created with + */ + async createRole (name: string, description: string, permissionIDs: number[]): Promise { + const response = await this.send(new RPCMessage(Method.CreateRole, { + name, + description, + permissions: permissionIDs + })); + + return response.data; + } + /** * validates user token */ - async validateToken (token:string) { - return true + async validateToken (token:string): Promise { + const response = await this.send(new RPCMessage(Method.ValidateToken, { token })); + if (response) { + return response.data[0]; + } else { + return false; + } + } + + /** + * Returns a list of roles the user is assigned to + * @param token + */ + async getUserRoles (token: String): Promise { + const response = await this.send(new RPCMessage(Method.GetRoles, { token })); + return response.data; + } + + /** + * Returns all permissions of the user + * @param token + */ + async getUserPermissions (token: String): Promise { + const roles = await this.getUserRoles(token); + const roleIds = roles.map(role => role[0]); + const permissionsResponse = await this.send(new RPCMessage(Method.GetRolePermissions, { roles: roleIds })); + const permissions: string[] = []; + + for (const id of roleIds) { + permissions.push(...permissionsResponse.data[id].map(entry => entry[1])); + } + + return permissions; + } + + /** + * Connects to the socket and returns the client + */ + async getSocket (): Promise> { + const socket = new Socket(); + const promiseSocket = new PromiseSocket(socket); + await promiseSocket.connect(this.port, this.host); + + return promiseSocket; + } + + /** + * Sends a message and reads and parses the response of it + * @param message + */ + async send (message: RPCMessage): Promise> { + const socket = await this.getSocket(); + await socket.writeAll(message.toBuffer()); + const response = await socket.readAll() as Buffer; + + if (response?.length > 0) { + return RPCMessage.fromBuffer(response); + } } } diff --git a/src/index.ts b/src/index.ts index 61fc39d..1d0e8fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,68 @@ -import { ApolloServer } from 'apollo-server' -import bikeresolver from './resolvers/cargobikeResolver' -import { CargoBikeAPI } from './datasources/db/cargobikeAPI' -import typeDefs from './schema/type-defs' -import 'reflect-metadata' -import { createConnection } from 'typeorm' +import { ApolloServer } from 'apollo-server-express'; +import bikeresolver from './resolvers/cargobikeResolver'; +import { CargoBikeAPI } from './datasources/db/cargobikeAPI'; +import typeDefs from './schema/type-defs'; +import 'reflect-metadata'; +import { createConnection } from 'typeorm'; +import { UserServerAPI } from './datasources/userserver/userserviceAPI'; +import express from 'express'; +import { requiredPermissions } from './datasources/userserver/permission'; + +require('dotenv').config(); + +/** + * Function that is called to authenticate a user by using the user rpc server + * @param req + * @param res + * @param next + */ +async function authenticate (req: any, res: any, next: any) { + if (process.env.NODE_ENV === 'develop') { + req.permissions = requiredPermissions.map((e) => e.name); + next(); + } else { + const token = req.headers.authorization?.replace('Bearer ', ''); + if (token) { + if (await userAPI.validateToken(token)) { + req.permissions = await userAPI.getUserPermissions(token); + next(); + } else { + res.status(401); + res.send('Unauthorized'); + } + } else { + res.status(401); + res.send('Unauthorized'); + } + } +} createConnection().then(async () => { - console.log('connected to db') -}).catch(error => console.log(error)) + console.log('connected to db'); +}).catch(error => console.log(error)); + +const userAPI = new UserServerAPI(process.env.RPC_HOST); const server = new ApolloServer({ resolvers: [bikeresolver], typeDefs, dataSources: () => ({ - cargoBikeAPI: new CargoBikeAPI() - }) -}) + cargoBikeAPI: new CargoBikeAPI(), + userAPI + }), + context: (req: any) => { + return req; + } +}); + +const app = express(); + +app.post('/graphql', authenticate); +app.get(/\/graphql?&.*query=/, authenticate); +server.applyMiddleware({ app }); -console.log(__dirname) -server.listen() - .then(({ url }) => console.log(`Server ready at ${url} `)) +console.log(__dirname); +app.listen(4000, async () => { + console.log('Server listening on port 4000'); + await userAPI.createDefinedPermissions(); +}); diff --git a/src/model/CargoBike.ts b/src/model/CargoBike.ts index ddb2b9c..46de65c 100644 --- a/src/model/CargoBike.ts +++ b/src/model/CargoBike.ts @@ -1,4 +1,4 @@ -import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm' +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class CargoBike { diff --git a/src/resolvers/cargobikeResolver.ts b/src/resolvers/cargobikeResolver.ts index a7c89db..89d05df 100644 --- a/src/resolvers/cargobikeResolver.ts +++ b/src/resolvers/cargobikeResolver.ts @@ -1,10 +1,23 @@ +import { Permission } from '../datasources/userserver/permission'; +import { GraphQLError } from 'graphql'; + export default { Query: { - CargobikeById: (_: any, { id, token }:{id: any, token: string}, { dataSources }:{dataSources: any}) => - dataSources.cargoBikeAPI.findCargoBikeById({ id, token }) + CargobikeById: (_: any, { id, token }:{id: any, token: string}, { dataSources, req }:{dataSources: any, req: any }) => { + if (req.permissions.includes(Permission.ReadBike)) { + return dataSources.cargoBikeAPI.findCargoBikeById({ id, token }); + } else { + throw new GraphQLError('Insufficient Permissions'); + } + } }, Mutation: { - addBike: (_:any, { id, token, name }:{id: any, token: string, name:string}, { dataSources }:{dataSources: any }) => - dataSources.cargoBikeAPI.updateBike({ id, token, name }) + addBike: (_: any, { id, token, name }:{id: any, token: string, name:string}, { dataSources, req }:{dataSources: any, req: any }) => { + if (req.permissions.includes(Permission.WriteBike)) { + return dataSources.cargoBikeAPI.updateBike({ id, token, name }); + } else { + throw new GraphQLError('Insufficient Permissions'); + } + } } -} +}; diff --git a/src/schema/type-defs.ts b/src/schema/type-defs.ts index 517cbdf..a86a338 100644 --- a/src/schema/type-defs.ts +++ b/src/schema/type-defs.ts @@ -1,4 +1,4 @@ -import { gql } from 'apollo-server' +import { gql } from 'apollo-server'; export default gql` @@ -355,4 +355,4 @@ type Mutation { cargoBike(token:String!,cargoBike: CargoBikeInput): UpdateBikeResponse! } -` +`;