diff --git a/aws/README.md b/aws/README.md new file mode 100644 index 0000000000..042c437a47 --- /dev/null +++ b/aws/README.md @@ -0,0 +1,3 @@ +# AWS + +This folder contains some of the scripts that we will be using to provision the required infrastructure on AWS. diff --git a/aws/apigw/API Gateway-staging-oas30-apigateway.yaml b/aws/apigw/API Gateway-staging-oas30-apigateway.yaml new file mode 100644 index 0000000000..2697c726d2 --- /dev/null +++ b/aws/apigw/API Gateway-staging-oas30-apigateway.yaml @@ -0,0 +1,1038 @@ +openapi: "3.0.1" +info: + title: "API Gateway" + description: "Main API Gateway for PeerPrep" + version: "1.0.2" +servers: +- url: "https://hwu5j8znt5.execute-api.ap-southeast-1.amazonaws.com/{basePath}" + variables: + basePath: + default: "staging" +paths: + /api/question-service/id/{id}: + get: + operationId: "getQuestionById" + parameters: + - name: "id" + in: "path" + required: true + schema: + type: "string" + responses: + "404": + description: "404 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELe64a59" + "500": + description: "500 response" + content: {} + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELd08b88" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "GET" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8003/api/question-service/id/{id}" + responses: + default: + statusCode: "200" + requestParameters: + integration.request.path.id: "method.request.path.id" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + put: + operationId: "updateQuestionById" + parameters: + - name: "id" + in: "path" + required: true + schema: + type: "string" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Question" + required: true + responses: + "404": + description: "404 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELf1bbf4" + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL803475" + "500": + description: "500 response" + content: {} + "409": + description: "409 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL23b348" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "PUT" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8003/api/question-service/id/{id}" + responses: + default: + statusCode: "200" + requestParameters: + integration.request.path.id: "method.request.path.id" + passthroughBehavior: "when_no_templates" + timeoutInMillis: 29000 + type: "http_proxy" + delete: + operationId: "deleteQuestionById" + parameters: + - name: "id" + in: "path" + required: true + schema: + type: "string" + responses: + "404": + description: "404 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL44ef1f" + "500": + description: "500 response" + content: {} + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELc8696d" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "DELETE" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8003/api/question-service/id/{id}" + responses: + default: + statusCode: "200" + requestParameters: + integration.request.path.id: "method.request.path.id" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + /api/question-service: + get: + operationId: "getAllQuestions" + responses: + "500": + description: "500 response" + content: {} + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL46170a" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "GET" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8003/api/question-service" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + post: + operationId: "createQuestion" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Question" + required: true + responses: + "201": + description: "201 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL76146b" + "400": + description: "400 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELd69e46" + "500": + description: "500 response" + content: {} + "409": + description: "409 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELd4a771" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "POST" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8003/api/question-service" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + /api/question-service/random: + get: + operationId: "findQuestion" + parameters: + - name: "difficulty" + in: "query" + schema: + type: "string" + - name: "categories" + in: "query" + schema: + type: "string" + responses: + "400": + description: "400 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELacc33f" + "500": + description: "500 response" + content: {} + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL1bfe98" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "GET" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8003/api/question-service/random" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + /api/user-service/users/logout: + post: + operationId: "logout" + responses: + "500": + description: "500 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL52bf6e" + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL5d8fd9" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "POST" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8001/api/user-service/users/logout" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + /api/user-service/users/changeDisplayName: + put: + operationId: "changeDisplayName" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MODELde4138" + required: true + responses: + "404": + description: "404 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELc531a7" + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL47bcc1" + "400": + description: "400 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELc76a41" + "500": + description: "500 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL296230" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "PUT" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8001/api/user-service/users/changeDisplayName" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + timeoutInMillis: 29000 + type: "http_proxy" + /api/question-service/categories: + get: + operationId: "getCategories" + responses: + "500": + description: "500 response" + content: {} + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL24b211" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "GET" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8003/api/question-service/categories" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + /api/user-service/users/login: + post: + operationId: "login" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL0bebcf" + required: true + responses: + "200": + description: "200 response" + headers: + Set-Cookie: + schema: + type: "string" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELe6d2f1" + "400": + description: "400 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL638449" + "401": + description: "401 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELc4cc1a" + "500": + description: "500 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELa1e3b1" + security: + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "POST" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8001/api/user-service/users/login" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + /api/run-service: + post: + operationId: "executeCode" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CodeExecutionRequest" + required: true + responses: + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/CodeExecutionResult" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + credentials: "arn:aws:iam::730335480348:role/PeerPrepApiGatewayExecutionRole" + httpMethod: "POST" + uri: "arn:aws:apigateway:ap-southeast-1:states:action/StartSyncExecution" + responses: + default: + statusCode: "200" + responseTemplates: + application/json: "#set($string = $input.json('$.output'))\n#set($data\ + \ = $util.parseJson($string))\n\n$data" + requestTemplates: + application/json: "#set($data = $util.escapeJavaScript($input.json('$')))\n\ + {\n \"input\": \"$data\",\n \"stateMachineArn\": \"arn:aws:states:ap-southeast-1:730335480348:stateMachine:ExpressStateMachine\"\ + \n}" + passthroughBehavior: "when_no_templates" + type: "aws" + /api/user-service/users/changePassword: + put: + operationId: "changePassword" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MODELe9c20a" + required: true + responses: + "404": + description: "404 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODELca9eeb" + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL72b7fb" + "400": + description: "400 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL73c89a" + "401": + description: "401 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL1eb856" + "500": + description: "500 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL403433" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "PUT" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8001/api/user-service/users/changePassword" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + /api/question-service/filter: + get: + operationId: "getFilteredQuestions" + parameters: + - name: "difficulty" + in: "query" + schema: + type: "string" + - name: "categories" + in: "query" + schema: + type: "string" + responses: + "400": + description: "400 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL5d05cb" + "500": + description: "500 response" + content: {} + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL5b14d0" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "GET" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8003/api/question-service/filter" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + /api/user-service/users: + post: + operationId: "createUser" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/MODELc1639c" + required: true + responses: + "201": + description: "201 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL4dabfb" + "400": + description: "400 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL8b3086" + "500": + description: "500 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL57b4fe" + "409": + description: "409 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL3c1ad8" + security: + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "POST" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8001/api/user-service/users" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + type: "http_proxy" + delete: + operationId: "deleteUser" + responses: + "404": + description: "404 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL955a3a" + "200": + description: "200 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL9e0dcb" + "400": + description: "400 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL0ac28b" + "500": + description: "500 response" + content: + application/json: + schema: + $ref: "#/components/schemas/MODEL6b3f7b" + security: + - PeerPrepJWTAuthorizer: [] + - api_key: [] + x-amazon-apigateway-integration: + httpMethod: "DELETE" + uri: "http://PeerPrepALB-705702575.ap-southeast-1.elb.amazonaws.com:8001/api/user-service/users" + responses: + default: + statusCode: "200" + passthroughBehavior: "when_no_templates" + type: "http_proxy" +components: + schemas: + MODEL3c1ad8: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODEL4dabfb: + type: "object" + properties: + message: + type: "string" + description: "success message" + MODEL72b7fb: + type: "object" + properties: + message: + type: "string" + description: "success message" + MODEL5d05cb: + type: "object" + properties: + message: + type: "string" + description: "Error message detailing what went wrong." + MODEL0ac28b: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODEL44ef1f: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + message: + type: "string" + description: "Error message detailing what went wrong." + MODEL955a3a: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODELe9c20a: + type: "object" + properties: + password: + type: "string" + description: "password" + newPassword: + type: "string" + description: "newPassword" + MODELc4cc1a: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODELd69e46: + type: "object" + properties: + message: + type: "string" + description: "Error message detailing what went wrong." + MODEL1eb856: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODELc531a7: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODEL52bf6e: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODEL0bebcf: + type: "object" + properties: + email: + type: "string" + description: "email" + password: + type: "string" + description: "password" + MODEL296230: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODELde4138: + type: "object" + properties: + newDisplayName: + type: "string" + description: "newDisplayName" + MODEL46170a: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + default: 200 + data: + type: "array" + items: + $ref: "#/components/schemas/Question" + CodeExecutionResult: + required: + - "description" + - "details" + - "errors" + - "memory" + - "prints" + - "results" + - "statusCode" + - "time" + type: "object" + properties: + statusCode: + type: "string" + description: "HTTP status code of the response" + description: + type: "string" + description: "String elaborating details on the status code" + results: + type: "array" + description: "A list of strings that are returned from the execution of\ + \ your code. This can be empty if your code did not produce any outputs\ + \ or failed to compile." + items: + type: "string" + prints: + type: "array" + items: + type: "string" + errors: + type: "array" + description: "A list of strings that details errors that are encountered\ + \ when running your code. This may be empty if there are no errors." + items: + type: "string" + time: + type: "number" + description: "Time taken to run the Lambda function" + memory: + type: "number" + description: "Memory used to execute the Lambda function" + MODELc1639c: + type: "object" + properties: + email: + type: "string" + description: "email" + password: + type: "string" + description: "password" + displayName: + type: "string" + description: "displayName" + MODELc76a41: + type: "object" + properties: + message: + type: "string" + description: "error message" + Question: + required: + - "categories" + - "description" + - "difficulty" + - "title" + type: "object" + properties: + title: + type: "string" + description: "Title of the question (required)" + description: + type: "string" + description: "Description of the question (required and unique)" + image: + type: "string" + description: "Image associated with the question (optional)" + format: "binary" + categories: + type: "array" + description: "List of topics associated with the question (required, at\ + \ least one topic)" + items: + type: "string" + difficulty: + type: "string" + description: "Difficulty level of the question (required)" + enum: + - "EASY" + - "MEDIUM" + - "HARD" + isDeleted: + type: "boolean" + description: "Indicates if the question is deleted (default is false)" + default: false + MODEL6b3f7b: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODELe64a59: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + message: + type: "string" + description: "Error message detailing what went wrong." + MODEL5b14d0: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + default: 200 + data: + type: "array" + items: + $ref: "#/components/schemas/Question" + MODELca9eeb: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODEL73c89a: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODEL76146b: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + data: + $ref: "#/components/schemas/Question" + MODEL403433: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODEL5d8fd9: + type: "object" + properties: + message: + type: "string" + description: "success message" + MODEL23b348: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + message: + type: "string" + description: "Error message detailing what went wrong." + MODELa1e3b1: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODELc8696d: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + message: + type: "string" + description: "Verified question is deleted" + MODEL1bfe98: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + default: 200 + data: + $ref: "#/components/schemas/Question" + MODEL47bcc1: + type: "object" + properties: + message: + type: "string" + description: "success message" + MODEL57b4fe: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODELd08b88: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + data: + $ref: "#/components/schemas/Question" + MODEL24b211: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + default: 200 + data: + type: "array" + items: + type: "string" + MODELd4a771: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + message: + type: "string" + description: "Error message detailing what went wrong." + MODEL8b3086: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODEL803475: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + data: + $ref: "#/components/schemas/Question" + MODELf1bbf4: + type: "object" + properties: + success: + type: "boolean" + status: + type: "integer" + format: "int32" + message: + type: "string" + description: "Error message detailing what went wrong." + MODELacc33f: + type: "object" + properties: + message: + type: "string" + description: "Error message detailing what went wrong." + CodeExecutionRequest: + required: + - "code" + - "language" + type: "object" + properties: + language: + type: "string" + description: "Language to execute the code in" + enum: + - "python" + - "java" + - "javascript" + code: + type: "string" + description: "JSON string representing code to execute" + MODEL638449: + type: "object" + properties: + message: + type: "string" + description: "error message" + MODEL9e0dcb: + type: "object" + properties: + message: + type: "string" + description: "success message" + MODELe6d2f1: + type: "object" + properties: + message: + type: "string" + description: "success message" + securitySchemes: + PeerPrepJWTAuthorizer: + type: "apiKey" + name: "Authorization" + in: "header" + x-amazon-apigateway-authtype: "custom" + x-amazon-apigateway-authorizer: + authorizerUri: "arn:aws:apigateway:ap-southeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-southeast-1:730335480348:function:jwtAuthoriser/invocations" + authorizerResultTtlInSeconds: 300 + type: "token" + api_key: + type: "apiKey" + name: "x-api-key" + in: "header" diff --git a/aws/lambda/code_execution_statistics.py b/aws/lambda/code_execution_statistics.py new file mode 100644 index 0000000000..d5dc46bbb1 --- /dev/null +++ b/aws/lambda/code_execution_statistics.py @@ -0,0 +1,46 @@ +import json +import boto3 +import time +import re + +def lambda_handler(event, context): + returns = { + 'statusCode': event['statusCode'], + 'description': event['description'], + 'results': event['results'], + 'prints': event['prints'], + 'errors': event['errors'], + 'time': 0, + 'memory': 0, + } + + lambda_request_id = event['details']['request_id'] + log_group_name = event['details']['log_group_name'] + log_stream_name = event['details']['log_stream_name'] + + cloudwatch = boto3.client('logs') + events = cloudwatch.get_log_events( + logGroupName=log_group_name, + logStreamName=log_stream_name, + ) + + for event in events['events']: + current_log_message = event['message'] + + if not current_log_message.startswith('REPORT'): + continue + + run_id = re.search(f'RequestId: {lambda_request_id}', current_log_message) + + if not run_id: + continue + + memory = re.search(r'Max Memory Used: (\d+) MB', current_log_message) + runtime = re.search(r'Duration: ([\d\.]+) ms', current_log_message) + + if memory and runtime: + returns["time"] = float(runtime.group(1)) + returns["memory"] = float(memory.group(1)) + break + + return returns diff --git a/aws/lambda/images/runner/Dockerfile.lambda b/aws/lambda/images/runner/Dockerfile.lambda new file mode 100644 index 0000000000..1f217779d8 --- /dev/null +++ b/aws/lambda/images/runner/Dockerfile.lambda @@ -0,0 +1,10 @@ +FROM public.ecr.aws/lambda/python:3.12 + +# Install GCC for C++ support +RUN dnf install -y gcc-c++ + +# Copy the python handler +COPY handler.py /var/task + +# Entrypoint for Lambda +CMD ["handler.handler"] diff --git a/aws/lambda/images/runner/handler.py b/aws/lambda/images/runner/handler.py new file mode 100644 index 0000000000..0b71b5b4c2 --- /dev/null +++ b/aws/lambda/images/runner/handler.py @@ -0,0 +1,128 @@ +import json +import sys +import io +import os +import subprocess + +from contextlib import redirect_stdout + + +# STDOUT redirection reused from: https://stackoverflow.com/questions/16571150/how-to-capture-stdout-output-from-a-python-function-call +sys.path.append('/tmp') +os.chdir('/tmp') + +def handler(event, context): + if "language" in event and 'code' in event: + language = event['language'].strip().lower() + out = io.StringIO() + + if language == 'python': + __write_to_file(event['code'], 'script.py') + + try: + from script import Solution + + with redirect_stdout(out): + soln = Solution().main() + + return { + 'statusCode': 200, + 'description': 'Success', + 'results': [soln], + 'prints': [out.getvalue()] if len(out.getvalue()) > 0 else [], + 'errors': [], + 'details': { + 'arn': context.invoked_function_arn, + 'request_id': context.aws_request_id, + 'log_group_name': context.log_group_name, + 'log_stream_name': context.log_stream_name, + } + } + except ImportError as ex: + return { + 'statusCode': 400, + 'description': 'Import Error', + 'results': [], + 'prints': [out.getvalue()] if len(out.getvalue()) > 0 else [], + 'errors': [str(ex)], + 'details': { + 'arn': context.invoked_function_arn, + 'request_id': context.aws_request_id, + 'log_group_name': context.log_group_name, + 'log_stream_name': context.log_stream_name, + } + } + except Exception as ex: + return { + 'statusCode': 400, + 'description': 'Runtime Error', + 'results': [], + 'prints': [out.getvalue()] if len(out.getvalue()) > 0 else [], + 'errors': [str(ex)], + 'details': { + 'arn': context.invoked_function_arn, + 'request_id': context.aws_request_id, + 'log_group_name': context.log_group_name, + 'log_stream_name': context.log_stream_name, + } + } + elif language == 'cpp': + __write_to_file(event['code'], 'script.cpp') + results = subprocess.run(['g++', 'script.cpp'], capture_output=True, text=True) + if results.returncode != 0: + return { + 'statusCode': 400, + 'description': 'Compilation or Runtime Error', + 'results': [results.stdout], + 'prints': [results.stdout], + 'errors': [results.stderr], + 'details': { + 'arn': context.invoked_function_arn, + 'request_id': context.aws_request_id, + 'log_group_name': context.log_group_name, + 'log_stream_name': context.log_stream_name, + } + } + else: + results = subprocess.run(['./a.out'], capture_output=True, text=True) + return { + 'statusCode': 200 if not results.stderr else 400, + 'description': 'Success' if not results.stderr else "Runtime Error", + 'results': [results.stdout], + 'prints': [results.stdout], + 'errors': [results.stderr], + 'details': { + 'arn': context.invoked_function_arn, + 'request_id': context.aws_request_id, + 'log_group_name': context.log_group_name, + 'log_stream_name': context.log_stream_name, + } + } + else: + return { + 'statusCode': 400, + 'description': 'No Language or Code Specified', + 'results': [], + 'prints': [], + 'errors': ["Missing langauge or code parameter"], + 'details': { + 'arn': context.invoked_function_arn, + 'request_id': context.aws_request_id, + 'log_group_name': context.log_group_name, + 'log_stream_name': context.log_stream_name, + } + } + +def __write_to_file(code, filename): + """ + Writes the input string into a code file. + + Args: + code (string): String representing the code to write + filename (string): Name of the file to write the code string into + """ + + with open(filename, 'w') as f: + for line in code.split("\n"): + f.write(line) + f.write("\n") diff --git a/aws/lambda/jwt_authoriser.py b/aws/lambda/jwt_authoriser.py new file mode 100644 index 0000000000..8d74b16a96 --- /dev/null +++ b/aws/lambda/jwt_authoriser.py @@ -0,0 +1,257 @@ +import re +import os +import jwt + +""" +Adapted from: https://gist.github.com/bendog/44f21a921f3e4282c631a96051718619 +""" + +def decode_jwt(token: str): + """Decode the encoded JWT token and return the principalUser + if correctly encoded""" + + return jwt.decode( + token.replace("Bearer ", "").encode(), + os.environ["JWT_SECRET_KEY"], + algorithms='HS256' + ) + +def lambda_handler(event, context): + try: + authHeader = event["headers"].get("Authorization", event["headers"].get("authorization", None)) + cookieHeader = event["headers"].get("Cookie", event["headers"].get("cookie", None)) + methodArn = event["methodArn"] + + if authHeader: + principalUser = decode_jwt(authHeader) + elif cookieHeader: + cookieHeader = cookieHeader.split(";") + principalUser = None + + for header in cookieHeader: + key, value = header.split("=") + + if key == "accessToken": + principalUser = decode_jwt(value) + else: + raise Exception("Unauthorized") + + principalEmail = principalUser.get('email') + principalDisplayName = principalUser.get('displayName') + principalIsAdmin = principalUser.get('isAdmin') + + ''' + If the token is valid, a policy must be generated which will allow or deny + access to the client. If access is denied, the client will receive a 403 + Access Denied response. If access is allowed, API Gateway will proceed with + the backend integration configured on the method that was called. + + This function must generate a policy that is associated with the recognized + principal user identifier. Depending on your use case, you might store + policies in a DB, or generate them on the fly. + + Keep in mind, the policy is cached for 5 minutes by default (TTL is + configurable in the authorizer) and will apply to subsequent calls to any + method/resource in the RestApi made with the same token. + + The example policy below denies access to all resources in the RestApi. + ''' + tmp = methodArn.split(':') + apiGatewayArnTmp = tmp[5].split('/') + awsAccountId = tmp[4] + + policy = AuthPolicy(principalEmail, awsAccountId) + policy.restApiId = apiGatewayArnTmp[0] + policy.region = tmp[3] + policy.stage = apiGatewayArnTmp[1] + policy.allowAllMethods() + #policy.allowMethod(HttpVerb.GET, '/pets/*') + + # Finally, build the policy + authResponse = policy.build() + + # new! -- add additional key-value pairs associated with the authenticated principal + # these are made available by APIGW like so: $context.authorizer. + # additional context is cached + # context = { + # 'key': 'value', # $context.authorizer.key -> value + # 'number': 1, + # 'bool': True + # } + # context['arr'] = ['foo'] <- this is invalid, APIGW will not accept it + # context['obj'] = {'foo':'bar'} <- also invalid + + # authResponse['context'] = context + + return authResponse + except: + """ + You can send a 401 Unauthorized response to the client by failing like so: + + raise Exception('Unauthorized') + """ + + raise Exception("Unauthorized") + + +class HttpVerb: + GET = 'GET' + POST = 'POST' + PUT = 'PUT' + PATCH = 'PATCH' + HEAD = 'HEAD' + DELETE = 'DELETE' + OPTIONS = 'OPTIONS' + ALL = '*' + + +class AuthPolicy(object): + # The AWS account id the policy will be generated for. This is used to create the method ARNs. + awsAccountId = '' + # The principal used for the policy, this should be a unique identifier for the end user. + principalId = '' + # The policy version used for the evaluation. This should always be '2012-10-17' + version = '2012-10-17' + # The regular expression used to validate resource paths for the policy + pathRegex = '^[/.a-zA-Z0-9-\*]+$' + + '''Internal lists of allowed and denied methods. + + These are lists of objects and each object has 2 properties: A resource + ARN and a nullable conditions statement. The build method processes these + lists and generates the approriate statements for the final policy. + ''' + allowMethods = [] + denyMethods = [] + + """Replace the placeholder value with a default API Gateway API id to be used in the policy. + Beware of using '*' since it will not simply mean any API Gateway API id, because stars will greedily expand over '/' or other separators. + See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_resource.html for more details.""" + restApiId = "https://zg75c6kx20.execute-api.ap-southeast-1.amazonaws.com" + + """Replace the placeholder value with a default region to be used in the policy. + Beware of using '*' since it will not simply mean any region, because stars will greedily expand over '/' or other separators. + See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_resource.html for more details.""" + region = "ap-southeast-1" + + """Replace the placeholder value with a default stage to be used in the policy. + Beware of using '*' since it will not simply mean any stage, because stars will greedily expand over '/' or other separators. + See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_resource.html for more details.""" + stage = "main" + + def __init__(self, principal, awsAccountId): + self.awsAccountId = awsAccountId + self.principalId = principal + self.allowMethods = [] + self.denyMethods = [] + + def _addMethod(self, effect, verb, resource, conditions): + '''Adds a method to the internal lists of allowed or denied methods. Each object in + the internal list contains a resource ARN and a condition statement. The condition + statement can be null.''' + if verb != '*' and not hasattr(HttpVerb, verb): + raise NameError('Invalid HTTP verb ' + verb + '. Allowed verbs in HttpVerb class') + resourcePattern = re.compile(self.pathRegex) + if not resourcePattern.match(resource): + raise NameError('Invalid resource path: ' + resource + '. Path should match ' + self.pathRegex) + + if resource[:1] == '/': + resource = resource[1:] + + resourceArn = 'arn:aws:execute-api:{}:{}:{}/{}/{}/{}'.format(self.region, self.awsAccountId, self.restApiId, self.stage, verb, resource) + + if effect.lower() == 'allow': + self.allowMethods.append({ + 'resourceArn': resourceArn, + 'conditions': conditions + }) + elif effect.lower() == 'deny': + self.denyMethods.append({ + 'resourceArn': resourceArn, + 'conditions': conditions + }) + + def _getEmptyStatement(self, effect): + '''Returns an empty statement object prepopulated with the correct action and the + desired effect.''' + statement = { + 'Action': 'execute-api:Invoke', + 'Effect': effect[:1].upper() + effect[1:].lower(), + 'Resource': [] + } + + return statement + + def _getStatementForEffect(self, effect, methods): + '''This function loops over an array of objects containing a resourceArn and + conditions statement and generates the array of statements for the policy.''' + statements = [] + + if len(methods) > 0: + statement = self._getEmptyStatement(effect) + + for curMethod in methods: + if curMethod['conditions'] is None or len(curMethod['conditions']) == 0: + statement['Resource'].append(curMethod['resourceArn']) + else: + conditionalStatement = self._getEmptyStatement(effect) + conditionalStatement['Resource'].append(curMethod['resourceArn']) + conditionalStatement['Condition'] = curMethod['conditions'] + statements.append(conditionalStatement) + + if statement['Resource']: + statements.append(statement) + + return statements + + def allowAllMethods(self): + '''Adds a '*' allow to the policy to authorize access to all methods of an API''' + self._addMethod('Allow', HttpVerb.ALL, '*', []) + + def denyAllMethods(self): + '''Adds a '*' allow to the policy to deny access to all methods of an API''' + self._addMethod('Deny', HttpVerb.ALL, '*', []) + + def allowMethod(self, verb, resource): + '''Adds an API Gateway method (Http verb + Resource path) to the list of allowed + methods for the policy''' + self._addMethod('Allow', verb, resource, []) + + def denyMethod(self, verb, resource): + '''Adds an API Gateway method (Http verb + Resource path) to the list of denied + methods for the policy''' + self._addMethod('Deny', verb, resource, []) + + def allowMethodWithConditions(self, verb, resource, conditions): + '''Adds an API Gateway method (Http verb + Resource path) to the list of allowed + methods and includes a condition for the policy statement. More on AWS policy + conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition''' + self._addMethod('Allow', verb, resource, conditions) + + def denyMethodWithConditions(self, verb, resource, conditions): + '''Adds an API Gateway method (Http verb + Resource path) to the list of denied + methods and includes a condition for the policy statement. More on AWS policy + conditions here: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition''' + self._addMethod('Deny', verb, resource, conditions) + + def build(self): + '''Generates the policy document based on the internal lists of allowed and denied + conditions. This will generate a policy with two main statements for the effect: + one statement for Allow and one statement for Deny. + Methods that includes conditions will have their own statement in the policy.''' + if ((self.allowMethods is None or len(self.allowMethods) == 0) and + (self.denyMethods is None or len(self.denyMethods) == 0)): + raise NameError('No statements defined for the policy') + + policy = { + 'principalId': self.principalId, + 'policyDocument': { + 'Version': self.version, + 'Statement': [] + } + } + + policy['policyDocument']['Statement'].extend(self._getStatementForEffect('Allow', self.allowMethods)) + policy['policyDocument']['Statement'].extend(self._getStatementForEffect('Deny', self.denyMethods)) + + return policy