diff --git a/api/openapi/admin.yaml b/api/openapi/admin.yaml new file mode 100644 index 0000000..ebccd14 --- /dev/null +++ b/api/openapi/admin.yaml @@ -0,0 +1,1068 @@ +openapi: 3.0.3 +info: + title: 2FAS Admin API + version: 0.0.1 + +servers: + - url: https://admin.api.2fas.com/ + +paths: + /system/fake_error: + get: + summary: Simulate application error (admin only) + tags: + - system + responses: + '500': + $ref: '#/components/responses/InternalServerError' + + /system/fake_warning: + get: + summary: Simulate warning message during runtime (admin only) + tags: + - system + responses: + '200': + $ref: '#/components/responses/Success' + + /system/info: + get: + summary: Get system configuration (admin only) + tags: + - system + responses: + '200': + $ref: '#/components/responses/SystemInfo' + + /mobile/web_services/{serviceId}: + put: + summary: Update web service. + tags: + - mobile + parameters: + - name: serviceId + in: path + required: true + description: Web service ID. + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebServicePayload' + responses: + '200': + $ref: '#/components/responses/WebServiceResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '404': + $ref: '#/components/responses/NotFoundError' + + delete: + summary: Delete web service. + tags: + - mobile + parameters: + - name: serviceId + in: path + required: true + description: Web service ID. + schema: + type: string + format: uuid + responses: + '200': + $ref: '#/components/responses/Success' + '404': + $ref: '#/components/responses/NotFoundError' + get: + summary: Get Web Service. + tags: + - mobile + parameters: + - name: serviceId + in: path + required: true + description: Web service ID. + schema: + type: string + format: uuid + responses: + '200': + $ref: '#/components/responses/WebServiceResponse' + '404': + $ref: '#/components/responses/NotFoundError' + + /mobile/web_services: + post: + summary: Create web service. + description: Adding service with already occupied name cause 409 response. + tags: + - mobile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WebServicePayload' + responses: + '200': + $ref: '#/components/responses/WebServiceResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '409': + $ref: '#/components/responses/ConflictError' + + /mobile/icons/collections: + post: + summary: Create icons collection. + tags: + - mobile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/IconsCollectionPayload' + responses: + '200': + $ref: '#/components/responses/IconsCollectionResponse' + '400': + $ref: '#/components/responses/BadRequestError' + + /mobile/icons/collections/{collectionId}: + put: + summary: Update icons colletion. + tags: + - mobile + parameters: + - name: collectionId + in: path + required: true + description: Icons collection ID. + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/IconsCollectionPayload' + responses: + '200': + $ref: '#/components/responses/IconsCollectionResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '404': + $ref: '#/components/responses/NotFoundError' + + delete: + summary: Delete icons collection. + tags: + - mobile + parameters: + - name: collectionId + in: path + required: true + description: Web service ID. + schema: + type: string + format: uuid + responses: + '200': + $ref: '#/components/responses/Success' + '404': + $ref: '#/components/responses/NotFoundError' + + /mobile/icons/requests/{iconRequestId}: + get: + summary: Get icon request. + tags: + - icon_requests + parameters: + - name: iconRequestId + in: path + required: true + description: Icon request ID. + schema: + type: string + format: uuid + responses: + '200': + $ref: '#/components/responses/IconRequestResponse' + '404': + $ref: '#/components/responses/NotFoundError' + + delete: + summary: Delete icon request. + tags: + - icon_requests + parameters: + - name: iconRequestId + in: path + required: true + description: Icon request ID. + schema: + type: string + format: uuid + responses: + '200': + $ref: '#/components/responses/Success' + '404': + $ref: '#/components/responses/NotFoundError' + + /mobile/icons/requests/{iconRequestId}/commands/update_web_service: + post: + summary: Update web service according to icon request data. + tags: + - icon_requests + parameters: + - name: iconRequestId + in: path + required: true + description: Icon request ID. + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/IconRequestUpdateWebServicePayload' + responses: + '200': + $ref: '#/components/responses/WebServiceResponse' + '404': + $ref: '#/components/responses/NotFoundError' + + /mobile/icons/requests/{iconRequestId}/commands/transform_to_web_service: + post: + summary: Transform icon request into web service. + tags: + - icon_requests + parameters: + - name: iconRequestId + in: path + required: true + description: Icon request ID. + schema: + type: string + format: uuid + responses: + '200': + $ref: '#/components/responses/WebServiceResponse' + '404': + $ref: '#/components/responses/NotFoundError' + + /mobile/icons: + post: + summary: Create icon. + tags: + - mobile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/IconPayload' + responses: + '200': + $ref: '#/components/responses/IconResponse' + '400': + $ref: '#/components/responses/BadRequestError' + + /mobile/icons/{iconId}: + put: + summary: Update icon. + tags: + - mobile + parameters: + - name: iconId + in: path + required: true + description: Icon ID. + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/IconPayload' + responses: + '200': + $ref: '#/components/responses/IconResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '404': + $ref: '#/components/responses/NotFoundError' + + delete: + summary: Delete icon. + tags: + - mobile + parameters: + - name: iconId + in: path + required: true + description: Icon ID. + schema: + type: string + format: uuid + responses: + '200': + $ref: '#/components/responses/Success' + '404': + $ref: '#/components/responses/NotFoundError' + + /mobile/notifications: + post: + summary: Create notification for mobile devices. + tags: + - mobile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MobileNotification' + responses: + '200': + $ref: '#/components/responses/MobileNotificationResponse' + '400': + $ref: '#/components/responses/BadRequestError' + + /mobile/notifications/{notificationId}: + put: + summary: Update notification for mobile devices. + tags: + - mobile + parameters: + - name: notificationId + in: path + required: true + description: The ID of notification. + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MobileNotification' + responses: + '200': + $ref: '#/components/responses/MobileNotificationResponse' + '400': + $ref: '#/components/responses/BadRequestError' + + delete: + summary: Delete notification. + tags: + - mobile + parameters: + - name: notificationId + in: path + required: true + description: The ID of notification. + schema: + type: string + format: uuid + responses: + '200': + $ref: '#/components/responses/Success' + + /mobile/notifications/{notificationId}/commands/publish: + post: + summary: Publish notification. + tags: + - mobile + parameters: + - name: notificationId + in: path + required: true + description: The ID of notification. + schema: + type: string + format: uuid + responses: + '200': + $ref: '#/components/responses/MobileNotificationResponse' + +components: + schemas: + Entity: + properties: + id: + type: string + format: uuid + + MetaData: + properties: + created_at: + type: string + format: date-time + description: Creation time. + updated_at: + type: string + format: date-time + description: Update time. + + WebServices: + type: array + items: + allOf: + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/WebService' + + WebService: + properties: + name: + type: string + description: Web service name. + description: + type: string + description: Web service description. + issuers: + type: array + items: + format: string + description: Web service possible issuers. + tags: + type: array + items: + format: string + description: Additional web service description tags. + icons_collections: + type: array + items: + format: string + description: Pinned icons collections IDs. + match_rules: + type: array + items: + $ref: '#/components/schemas/WebServiceMatchRule' + created_at: + type: string + format: date-time + description: Creation date. + updated_at: + type: string + format: date-time + description: Creation date. + + WebServiceMatchRule: + properties: + field: + type: string + enum: + - issuer + - label + - account + text: + type: string + example: "@facebook.com" + matcher: + type: string + enum: + - contains + - starts_with + - ends_with + - equals + - regex + ignore_case: + type: boolean + default: true + + WebServicePayload: + properties: + name: + type: string + description: Web service name. + description: + type: string + description: Web service description. + issuers: + type: array + items: + format: string + description: Web service possible issuers. + tags: + type: array + items: + format: string + description: Additional web service description tags. + icons_collections: + type: array + items: + format: string + description: Pinned icons collections IDs. + match_rules: + type: array + items: + $ref: '#/components/schemas/WebServiceMatchRule' + + IconsCollections: + type: array + items: + allOf: + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/IconsCollection' + + IconsCollection: + properties: + name: + type: string + description: Web service name. + description: + type: string + description: Web service description. + icons: + type: array + items: + format: string + description: Pinned icons IDs. + created_at: + type: string + format: date-time + description: Creation date. + updated_at: + type: string + format: date-time + description: Creation date. + + IconsCollectionPayload: + properties: + name: + type: string + description: Web service name. + description: + type: string + description: Web service description. + icons: + type: array + items: + format: string + description: Icons IDs. + + Icons: + type: array + items: + allOf: + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/Icon' + + Icon: + properties: + name: + type: string + description: Web service name. + url: + type: string + description: Icon AWS s3 location. + type: + type: string + enum: + - light + - dark + description: Icon type for darkmode and lighmode. + width: + type: integer + description: Icon width. + height: + type: integer + description: Icon height. + created_at: + type: string + format: date-time + description: Creation date. + updated_at: + type: string + format: date-time + description: Creation date. + + IconPayload: + properties: + name: + type: string + description: Web service name. + icon: + type: string + description: Base64 encoded PNG image file. + type: + type: string + enum: + - light + - dark + description: Icon type for darkmode and lighmode. + + IconsRequests: + type: array + items: + allOf: + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/IconRequest' + + IconRequest: + properties: + caller_id: + type: string + description: Some caller identifier + service_name: + type: string + description: Web service name. + issuers: + type: array + items: + format: string + description: Web service possible issuers. + description: + type: string + description: Request description. + light_icon_url: + type: string + description: Base64 encoded PNG image file. + dark_icon_url: + type: string + description: Base64 encoded PNG image file. + created_at: + type: string + format: date-time + description: Creation date. + updated_at: + type: string + format: date-time + description: Creation date. + + IconRequestPayload: + properties: + caller_id: + type: string + description: Some caller identifier + service_name: + type: string + description: Web service name. + issuers: + type: array + items: + format: string + description: Web service possible issuers. + description: + type: string + description: Request description. + light_icon: + type: string + description: Base64 encoded PNG image file. + dark_icon: + type: string + description: Base64 encoded PNG image file. + + IconRequestUpdateWebServicePayload: + properties: + web_service_id: + type: string + description: Web service id. + + DebugLogsAuditCollection: + type: array + items: + allOf: + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/DebugLogsAudit' + + DebugLogsAudit: + properties: + username: + type: string + description: Mobile app username. + description: + type: string + description: Problem description. + file: + type: string + description: Problem description. + expire_at: + type: string + format: date-time + description: Expiration date. + created_at: + type: string + format: date-time + description: Creation date. + updated_at: + type: string + format: date-time + description: Creation date. + + DebugLogsAuditClaimPayload: + properties: + username: + type: string + description: Mobile app username. + description: + type: string + description: Problem description. + + DebugLogsAuditPayload: + properties: + file: + type: string + description: Problem description. + + MobileNotification: + properties: + icon: + type: string + enum: + - updates + - news + - features + - youtube + description: Notification icon (category). + link: + type: string + description: 2fas.com subpage with more detailed description. + message: + type: string + description: Notification message. + published_at: + type: string + format: date-time + description: Date of publishing. + platform: + type: string + enum: + - ios + - android + - huawei + version: + type: string + description: Platform version (semantic version format). + created_at: + type: string + format: date-time + description: Creation date. + + DeviceAndExtensionHaveBeenPaired: + properties: + extension_id: + type: string + extension_name: + type: string + extension_public_key: + type: string + + AlreadyRegisteredBrowserExtension: + properties: + id: + type: integer + name: + type: string + browser_name: + type: string + browser_version: + type: string + created_at: + type: string + updated_at: + type: string + + BrowserExtension: + properties: + id: + type: integer + name: + type: string + browser_name: + type: string + browser_version: + type: string + created_at: + type: string + updated_at: + type: string + paired_at: + type: string + + BrowserExtensionDevice: + properties: + id: + type: string + name: + type: string + platform: + type: string + paired_at: + type: string + format: date-time + + BrowserExtension2FaRequest: + properties: + extension_id: + type: string + format: uuid + token_request_id: + type: string + format: uuid + domain: + type: string + status: + type: string + enum: + - pending + - completed + created_at: + type: string + format: date-time + + ApiError: + type: object + properties: + code: + type: integer + type: + type: string + description: + type: string + reason: + type: string + + responses: + Success: + description: Standard response for successful HTTP requests. + content: + application/json: + schema: + type: object + properties: + code: + type: integer + type: + type: string + description: + type: string + message: + type: string + + BadRequestError: + description: The server cannot or will not process the request, (e.g., malformed request syntax, size too large etc) + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + ConflictError: + description: The request could not be completed due to a conflict with the current state of the target resource + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + NotFoundError: + description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + GoneError: + description: The requested resource is no longer available. + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + InternalServerError: + description: Unexpected condition was encountered. + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + MobileDevice: + description: Registered mobile device. + content: + application/json: + schema: + properties: + id: + type: string + format: uuid + name: + type: string + platform: + type: string + enum: + - ios + - android + - huawei + created_at: + type: string + updated_at: + type: string + + SystemInfo: + description: System configuration + content: + application/json: + schema: + properties: + environment: + type: object + description: System environment variables. + configuration: + type: object + description: Application configuration. + + BrowserExtension2FaRequest: + description: Browser extension 2FA token request (return only PENDING and no older than 2-3 minutes). + content: + application/json: + schema: + $ref: '#/components/schemas/BrowserExtension2FaRequest' + + PairingResponse: + description: Mobile device and extension have been paired. + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceAndExtensionHaveBeenPaired' + + MobileNotificationsCollectionResponse: + description: Mobile notifications collection. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MobileNotification' + + MobileNotificationResponse: + description: Mobile device and extension have been paired. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/MobileNotification' + - $ref: '#/components/schemas/MetaData' + + BrowserExtensionsCollectionResponse: + description: Browser extensions collection. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/BrowserExtension' + + BrowserExtensionsPairedDevicesCollectionResponse: + description: Mobile devices paired with browser extension. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/BrowserExtensionDevice' + + BrowserExtension2FaRequestsCollectionResponse: + description: Current (not older than few minutes) browser extension 2fa requests. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/BrowserExtension2FaRequest' + + WebServiceResponse: + description: Web service. + content: + application/json: + schema: + $ref: "#/components/schemas/WebService" + + WebServicesCollectionResponse: + description: Web services. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/WebServices' + + IconsCollectionResponse: + description: Icons collection. + content: + application/json: + schema: + $ref: "#/components/schemas/IconsCollection" + + IconsCollectionsResponse: + description: Icons collections. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/IconsCollections' + + IconResponse: + description: Icon. + content: + application/json: + schema: + $ref: "#/components/schemas/Icon" + + IconsResponse: + description: Icons. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/Icons' + + IconRequestResponse: + description: Icon request. + content: + application/json: + schema: + $ref: "#/components/schemas/IconRequest" + + IconsRequestsResponse: + description: Icons requests. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/IconsRequests' + + DebugLogsAuditClaimResponse: + description: Debug logs audit claim. + content: + application/json: + schema: + $ref: "#/components/schemas/DebugLogsAudit" + + DebugLogsAuditResponse: + description: Debug logs audit. + content: + application/json: + schema: + $ref: "#/components/schemas/DebugLogsAudit" + + DebugLogsAuditCollectionResponse: + description: Icons. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Entity' + - $ref: '#/components/schemas/DebugLogsAuditCollection' diff --git a/api/openapi/rest.yaml b/api/openapi/rest.yaml index e49573f..e350c51 100644 --- a/api/openapi/rest.yaml +++ b/api/openapi/rest.yaml @@ -1903,4 +1903,4 @@ components: schema: allOf: - $ref: '#/components/schemas/Entity' - - $ref: '#/components/schemas/DebugLogsAuditCollection' \ No newline at end of file + - $ref: '#/components/schemas/DebugLogsAuditCollection' diff --git a/cmd/admin/main.go b/cmd/admin/main.go new file mode 100644 index 0000000..a961a6a --- /dev/null +++ b/cmd/admin/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "github.com/gin-gonic/gin" + + "github.com/twofas/2fas-server/config" + "github.com/twofas/2fas-server/internal/api" + "github.com/twofas/2fas-server/internal/common/http" + "github.com/twofas/2fas-server/internal/common/logging" +) + +func main() { + logging.WithDefaultField("service_name", "admin_api") + + config.LoadConfiguration() + + application := api.NewApplication("admin-api", config.Config) + + logging.Infof("Initialize admin-api application: %q", config.Config.App.ListenAddr) + logging.Infof("Environment is: %q", config.Config.Env) + + http.RunHttpServer(config.Config.App.ListenAddr, func(engine *gin.Engine) { + application.RegisterAdminRoutes(engine) + }) +} diff --git a/cmd/api/main.go b/cmd/api/main.go index 00fa725..f80d997 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -13,7 +13,7 @@ func main() { config.LoadConfiguration() - application := api.NewApplication(config.Config) + application := api.NewApplication("api", config.Config) logging.Info("Initialize application ", config.Config.App.ListenAddr) logging.Info("Environment is: ", config.Config.Env) diff --git a/config/config.go b/config/config.go index 17ae31d..eb9d794 100644 --- a/config/config.go +++ b/config/config.go @@ -1,10 +1,11 @@ package config import ( - "github.com/spf13/viper" - "github.com/twofas/2fas-server/internal/common/logging" "os" "strings" + + "github.com/spf13/viper" + "github.com/twofas/2fas-server/internal/common/logging" ) var Config Configuration diff --git a/deployments/admin/appspec.yml b/deployments/admin/appspec.yml new file mode 100644 index 0000000..6ce209c --- /dev/null +++ b/deployments/admin/appspec.yml @@ -0,0 +1,9 @@ +version: 0.0 +Resources: + - TargetService: + Type: AWS::ECS::Service + Properties: + TaskDefinition: + LoadBalancerInfo: + ContainerName: "2fas-admin-api" + ContainerPort: 8082 diff --git a/deployments/admin/buildspec.yml b/deployments/admin/buildspec.yml new file mode 100644 index 0000000..e73151d --- /dev/null +++ b/deployments/admin/buildspec.yml @@ -0,0 +1,38 @@ +version: 0.2 + +env: + secrets-manager: + DOCKERHUB_USERNAME: hub.docker.com:username + DOCKERHUB_PASS: hub.docker.com:password + +phases: + pre_build: + commands: + - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) + - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME + - echo Logging in to Docker HUB to avoid rate limit + - echo "$DOCKERHUB_PASS" | docker login --username $DOCKERHUB_USERNAME --password-stdin + - echo Logging in to Amazon ECR + - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com + + build: + commands: + - echo Build started on `date` + - echo Building the Docker image + - docker build -f docker/admin/Dockerfile -t $REPOSITORY_URI:latest . + - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG + + post_build: + commands: + - echo Build completed on `date` + - echo Pushing the Docker images latest, $IMAGE_TAG + - docker push $REPOSITORY_URI:latest + - docker push $REPOSITORY_URI:$IMAGE_TAG + - sed -i 's//'$AWS_ACCOUNT_ID'/g' deployments/admin/taskdef.json + - sed -i 's//'$AWS_ACCOUNT_ID'\.dkr\.ecr\.'$AWS_DEFAULT_REGION'\.amazonaws.com\/'$IMAGE_REPO_NAME'\:'$IMAGE_TAG'/g' deployments/admin/taskdef.json + +artifacts: + files: + - imageDetail.json + - deployments/admin/appspec.yml + - deployments/admin/taskdef.json diff --git a/deployments/admin/taskdef.json b/deployments/admin/taskdef.json new file mode 100644 index 0000000..6f1fcd7 --- /dev/null +++ b/deployments/admin/taskdef.json @@ -0,0 +1,76 @@ +{ + "executionRoleArn": "arn:aws:iam:::role/2fas-admin-api_ecsTaskExecutionRole", + "containerDefinitions": [ + { + "name": "2fas-admin-api", + "image": "", + "essential": true, + "portMappings": [ + { + "hostPort": 8082, + "protocol": "tcp", + "containerPort": 8082 + } + ], + "environmentFiles": [ + { + "value": "arn:aws:s3:::2fas-production-env/admin_api.env", + "type": "s3" + } + ], + "secrets": [ + { + "name": "MYSQL_PASSWORD", + "valueFrom": "arn:aws:secretsmanager:us-east-2::secret:prod/mysql-wi9cyz:password::" + }, + { + "name": "MYSQL_USERNAME", + "valueFrom": "arn:aws:secretsmanager:us-east-2::secret:prod/mysql-wi9cyz:username::" + }, + { + "name": "MYSQL_HOST", + "valueFrom": "arn:aws:secretsmanager:us-east-2::secret:prod/mysql-wi9cyz:host::" + }, + { + "name": "MOBILE_DEBUG_AWS_ACCESS_KEY_ID", + "valueFrom": "arn:aws:secretsmanager:us-east-2::secret:prod/mobile-X5wmei:AWS_ACCESS_KEY_ID::" + }, + { + "name": "MOBILE_DEBUG_AWS_SECRET_ACCESS_KEY", + "valueFrom": "arn:aws:secretsmanager:us-east-2::secret:prod/mobile-X5wmei:AWS_SECRET_ACCESS_KEY::" + }, + { + "name": "S3_USER_ACCESS_KEY_ID", + "valueFrom": "arn:aws:secretsmanager:us-east-2::secret:prod/api-EaED00:2fas_api_access_key_id::" + }, + { + "name": "S3_USER_ACCESS_SECRET_KEY", + "valueFrom": "arn:aws:secretsmanager:us-east-2::secret:prod/api-EaED00:2fas_api_access_secret_key::" + }, + { + "name": "ICONS_S3_ACCESS_KEY_ID", + "valueFrom": "arn:aws:secretsmanager:us-east-2::secret:prod/api-EaED00:icons_s3_access_key_id::" + }, + { + "name": "ICONS_S3_ACCESS_SECRET_KEY", + "valueFrom": "arn:aws:secretsmanager:us-east-2::secret:prod/api-EaED00:icons_s3_access_secret_key::" + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group" : "/ecs/2fas-api", + "awslogs-region": "us-east-2", + "awslogs-stream-prefix": "ecs" + } + } + } + ], + "requiresCompatibilities": [ + "FARGATE" + ], + "networkMode": "awsvpc", + "family": "2fas-admin-api", + "cpu": "256", + "memory": "512" +} diff --git a/docker-compose.yml b/docker-compose.yml index cda4e67..01f6987 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,18 @@ services: env_file: - .env + admin: + build: + context: . + dockerfile: docker/admin/Dockerfile + depends_on: + mysql: + condition: service_healthy + ports: + - "8082:8080" + env_file: + - .env + websocket: build: context: . @@ -58,4 +70,4 @@ services: - ./api/openapi:/usr/share/nginx/html/doc volumes: - go-modules: \ No newline at end of file + go-modules: diff --git a/docker/admin/Dockerfile b/docker/admin/Dockerfile new file mode 100644 index 0000000..106d73a --- /dev/null +++ b/docker/admin/Dockerfile @@ -0,0 +1,31 @@ +FROM golang:1.19-alpine as build + +ENV GO111MODULE=on \ + CGO_ENABLED=0 \ + GOOS=linux \ + GOARCH=amd64 + +WORKDIR /go/src/2fas + +COPY go.mod go.sum ./ + +RUN go mod download -x + +COPY . . + +RUN mkdir -p bin + +RUN go build -trimpath -o bin/api ./cmd/admin/main.go + +FROM alpine:latest + +RUN adduser 2fas -D + +USER 2fas + +WORKDIR /home/2fas/ + +COPY --from=build /go/src/2fas/bin/* /usr/local/bin/ +COPY ./config/config.yml ./config.yml + +CMD ["api"] diff --git a/docker/swaggerui/Dockerfile b/docker/swaggerui/Dockerfile index 276f650..323be0b 100644 --- a/docker/swaggerui/Dockerfile +++ b/docker/swaggerui/Dockerfile @@ -1,6 +1,6 @@ FROM swaggerapi/swagger-ui ENV URLS_PRIMARY_NAME "RestAPI" -ENV URLS "[{url: 'doc/rest.yaml', name: 'RestAPI'}, {url: 'doc/websocket.yaml', name: 'WebsocketAPI'}]" +ENV URLS "[{url: 'doc/rest.yaml', name: 'RestAPI'}, {url: 'doc/websocket.yaml', name: 'WebsocketAPI'}, {url: 'doc/admin.yaml', name: 'Admin RestAPI'},]" COPY ./api/openapi /usr/share/nginx/html/doc diff --git a/internal/api/app.go b/internal/api/app.go index 0947956..3683593 100644 --- a/internal/api/app.go +++ b/internal/api/app.go @@ -2,6 +2,7 @@ package api import ( "errors" + "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/twofas/2fas-server/config" @@ -20,19 +21,18 @@ var validate *validator.Validate type Module interface { RegisterRoutes(router *gin.Engine) + RegisterAdminRoutes(g *gin.RouterGroup) } type Application struct { - Addr string - - Router *gin.Engine - - Config config.Configuration - - Modules []Module + Addr string + Router *gin.Engine + Config config.Configuration + Modules []Module + HealthModule *health.HealthModule } -func NewApplication(config config.Configuration) *Application { +func NewApplication(applicationName string, config config.Configuration) *Application { validate = validator.New() gorm := db.NewGormConnection(config) @@ -41,8 +41,10 @@ func NewApplication(config config.Configuration) *Application { validate.RegisterValidation("not_blank", validation.NotBlank) + h := health.NewHealthModule(applicationName, config, redisClient) + modules := []Module{ - health.NewHealthModule(config, redisClient), + h, support.NewSupportModule(config, gorm, database, validate), icons.NewIconsModule(config, gorm, database, validate), extension.NewBrowserExtensionModule(config, gorm, database, redisClient, validate), @@ -50,9 +52,10 @@ func NewApplication(config config.Configuration) *Application { } app := &Application{ - Addr: config.App.ListenAddr, - Config: config, - Modules: modules, + Addr: config.App.ListenAddr, + Config: config, + Modules: modules, + HealthModule: h, } return app @@ -67,3 +70,19 @@ func (a *Application) RegisterRoutes(router *gin.Engine) { module.RegisterRoutes(router) } } + +func (a *Application) RegisterAdminRoutes(router *gin.Engine) { + router.NoRoute(func(c *gin.Context) { + c.JSON(404, api.NotFoundError(errors.New("URI not found"))) + }) + + // The only route method is /health. Everything else + // is nested under /admin so that oAuth proxy can route to it. + a.HealthModule.RegisterHealth(router) + + g := router.Group("/admin") + + for _, module := range a.Modules { + module.RegisterAdminRoutes(g) + } +} diff --git a/internal/api/browser_extension/service/service.go b/internal/api/browser_extension/service/service.go index 035b780..8f522c1 100644 --- a/internal/api/browser_extension/service/service.go +++ b/internal/api/browser_extension/service/service.go @@ -2,6 +2,7 @@ package service import ( "database/sql" + "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/go-redis/redis/v8" @@ -153,3 +154,6 @@ func (m *BrowserExtensionModule) RegisterRoutes(router *gin.Engine) { } } +func (m *BrowserExtensionModule) RegisterAdminRoutes(g *gin.RouterGroup) { + +} diff --git a/internal/api/health/ports/http.go b/internal/api/health/ports/http.go index b828459..6c0e074 100644 --- a/internal/api/health/ports/http.go +++ b/internal/api/health/ports/http.go @@ -3,6 +3,9 @@ package ports import ( "context" "encoding/json" + "os" + "time" + "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" "github.com/twofas/2fas-server/config" @@ -11,17 +14,17 @@ import ( "github.com/twofas/2fas-server/internal/common/aws" "github.com/twofas/2fas-server/internal/common/clock" "github.com/twofas/2fas-server/internal/common/logging" - "os" - "time" ) type RoutesHandler struct { - redis *redis.Client + applicationName string + redis *redis.Client } -func NewRoutesHandler(redis *redis.Client) *RoutesHandler { +func NewRoutesHandler(applicationName string, redis *redis.Client) *RoutesHandler { return &RoutesHandler{ - redis: redis, + applicationName: applicationName, + redis: redis, } } @@ -84,10 +87,11 @@ type redisStatus struct { } type systemInfo struct { - LocalTime string `json:"local_time"` - Config configuration `json:"configuration"` - Environment []string `json:"environment"` - Redis redisStatus `json:"redis"` + ApplicationName string `json:"application_name"` + LocalTime string `json:"local_time"` + Config configuration `json:"configuration"` + Environment []string `json:"environment"` + Redis redisStatus `json:"redis"` } func (r *RoutesHandler) RedisInfo(c *gin.Context) { diff --git a/internal/api/health/service/service.go b/internal/api/health/service/service.go index f7a6f19..f92bce4 100644 --- a/internal/api/health/service/service.go +++ b/internal/api/health/service/service.go @@ -13,8 +13,8 @@ type HealthModule struct { Config config.Configuration } -func NewHealthModule(config config.Configuration, redis *redis.Client) *HealthModule { - routesHandler := ports.NewRoutesHandler(redis) +func NewHealthModule(applicationName string, config config.Configuration, redis *redis.Client) *HealthModule { + routesHandler := ports.NewRoutesHandler(applicationName, redis) return &HealthModule{ RoutesHandler: routesHandler, @@ -34,3 +34,16 @@ func (m *HealthModule) RegisterRoutes(router *gin.Engine) { internalFor2FasUsersOnly.GET("/system/fake_warning", m.RoutesHandler.FakeWarning) internalFor2FasUsersOnly.GET("/system/fake_security_warning", m.RoutesHandler.FakeSecurityWarning) } + +func (m *HealthModule) RegisterHealth(router *gin.Engine) { + router.GET("/health", m.RoutesHandler.CheckApplicationHealth) +} + +func (m *HealthModule) RegisterAdminRoutes(g *gin.RouterGroup) { + g.GET("/health", m.RoutesHandler.CheckApplicationHealth) + g.GET("/system/redis/info", m.RoutesHandler.RedisInfo) + g.GET("/system/info", m.RoutesHandler.GetApplicationConfiguration) + g.GET("/system/fake_error", m.RoutesHandler.FakeError) + g.GET("/system/fake_warning", m.RoutesHandler.FakeWarning) + g.GET("/system/fake_security_warning", m.RoutesHandler.FakeSecurityWarning) +} diff --git a/internal/api/icons/service/service.go b/internal/api/icons/service/service.go index db667cd..a742e4a 100644 --- a/internal/api/icons/service/service.go +++ b/internal/api/icons/service/service.go @@ -2,6 +2,7 @@ package service import ( "database/sql" + "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/twofas/2fas-server/config" @@ -179,3 +180,30 @@ func (m *IconsModule) RegisterRoutes(router *gin.Engine) { publicRouter.GET("/mobile/icons/requests", m.RoutesHandler.FindAllIconsRequests) } + +func (m *IconsModule) RegisterAdminRoutes(g *gin.RouterGroup) { + // internal/admin + g.POST("/mobile/web_services", m.RoutesHandler.CreateWebService) + g.PUT("/mobile/web_services/:service_id", m.RoutesHandler.UpdateWebService) + g.DELETE("/mobile/web_services/:service_id", m.RoutesHandler.RemoveWebService) + + if m.Config.IsTestingEnv() { + g.DELETE("/mobile/web_services", m.RoutesHandler.RemoveAllWebServices) + g.DELETE("/mobile/icons", m.RoutesHandler.RemoveAllIcons) + g.DELETE("/mobile/icons/collections", m.RoutesHandler.RemoveAllIconsCollections) + g.DELETE("/mobile/icons/requests", m.RoutesHandler.RemoveAllIconsRequests) + } + + g.POST("/mobile/icons/collections", m.RoutesHandler.CreateIconsCollection) + g.PUT("/mobile/icons/collections/:collection_id", m.RoutesHandler.UpdateIconsCollection) + g.DELETE("/mobile/icons/collections/:collection_id", m.RoutesHandler.RemoveIconsCollection) + + g.POST("/mobile/icons", m.RoutesHandler.CreateIcon) + g.PUT("/mobile/icons/:icon_id", m.RoutesHandler.UpdateIcon) + g.DELETE("/mobile/icons/:icon_id", m.RoutesHandler.RemoveIcon) + + g.DELETE("/mobile/icons/requests/:icon_request_id", m.RoutesHandler.RemoveIconRequest) + g.POST("/mobile/icons/requests/:icon_request_id/commands/update_web_service", m.RoutesHandler.UpdateWebServiceFromIconRequest) + g.POST("/mobile/icons/requests/:icon_request_id/commands/transform_to_web_service", m.RoutesHandler.TransformToWebService) + g.GET("/mobile/icons/requests/:icon_request_id", m.RoutesHandler.FindIconRequest) +} diff --git a/internal/api/mobile/service/service.go b/internal/api/mobile/service/service.go index 070ffc7..e647f8c 100644 --- a/internal/api/mobile/service/service.go +++ b/internal/api/mobile/service/service.go @@ -2,6 +2,7 @@ package service import ( "database/sql" + "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/go-redis/redis/v8" @@ -10,7 +11,7 @@ import ( "github.com/twofas/2fas-server/internal/api/mobile/adapters" "github.com/twofas/2fas-server/internal/api/mobile/app" "github.com/twofas/2fas-server/internal/api/mobile/app/command" - "github.com/twofas/2fas-server/internal/api/mobile/app/queries" + query "github.com/twofas/2fas-server/internal/api/mobile/app/queries" apisec "github.com/twofas/2fas-server/internal/api/mobile/app/security" "github.com/twofas/2fas-server/internal/api/mobile/ports" "github.com/twofas/2fas-server/internal/common/clock" @@ -153,3 +154,15 @@ func (m *MobileModule) RegisterRoutes(router *gin.Engine) { publicRouter.GET("/mobile/devices/:device_id/browser_extensions", m.RoutesHandler.FindAllMobileAppExtensions) publicRouter.GET("/mobile/devices/:device_id/browser_extensions/:extension_id", m.RoutesHandler.FindMobileAppExtensionById) } + +func (m *MobileModule) RegisterAdminRoutes(g *gin.RouterGroup) { + g.POST("/mobile/notifications", m.RoutesHandler.CreateMobileNotification) + g.PUT("/mobile/notifications/:notification_id", m.RoutesHandler.UpdateMobileNotification) + g.DELETE("/mobile/notifications/:notification_id", m.RoutesHandler.RemoveMobileNotification) + g.POST("/mobile/notifications/:notification_id/commands/publish", m.RoutesHandler.PublishMobileNotification) + + if m.Config.IsTestingEnv() { + g.DELETE("/mobile/notifications", m.RoutesHandler.RemoveAllMobileNotifications) + g.DELETE("/mobile/devices", m.RoutesHandler.RemoveAllMobileDevices) + } +} diff --git a/internal/api/support/service/service.go b/internal/api/support/service/service.go index 0c61598..6773006 100644 --- a/internal/api/support/service/service.go +++ b/internal/api/support/service/service.go @@ -2,6 +2,7 @@ package service import ( "database/sql" + "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/twofas/2fas-server/config" @@ -103,3 +104,15 @@ func (m *SupportModule) RegisterRoutes(router *gin.Engine) { publicRouter.POST("/mobile/support/debug_logs/audit/:audit_id", m.RoutesHandler.CreateDebugLogsAudit) } + +func (m *SupportModule) RegisterAdminRoutes(g *gin.RouterGroup) { + g.POST("/mobile/support/debug_logs/audit/claim", m.RoutesHandler.CreateDebugLogsAuditClaim) + g.PUT("/mobile/support/debug_logs/audit/claim/:audit_id", m.RoutesHandler.UpdateDebugLogsAuditClaim) + g.DELETE("/mobile/support/debug_logs/audit/:audit_id", m.RoutesHandler.DeleteDebugLogsAudit) + g.GET("/mobile/support/debug_logs/audit/:audit_id", m.RoutesHandler.GetDebugLogsAudit) + g.GET("/mobile/support/debug_logs/audit", m.RoutesHandler.GetDebugAllLogsAudit) + + if m.Config.IsTestingEnv() { + g.DELETE("/mobile/support/debug_logs/audit", m.RoutesHandler.DeleteAllDebugLogsAudit) + } +}