feat: create admin api (#5)

* feat: create admin api

Create admin api as a standalone application.
This commit is contained in:
Krzysztof Dryś 2023-09-22 15:19:04 +02:00 committed by GitHub
parent 538e23455a
commit 2e4f34e829
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1384 additions and 30 deletions

1068
api/openapi/admin.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1903,4 +1903,4 @@ components:
schema:
allOf:
- $ref: '#/components/schemas/Entity'
- $ref: '#/components/schemas/DebugLogsAuditCollection'
- $ref: '#/components/schemas/DebugLogsAuditCollection'

25
cmd/admin/main.go Normal file
View File

@ -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)
})
}

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,9 @@
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: "2fas-admin-api"
ContainerPort: 8082

View File

@ -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>/'$AWS_ACCOUNT_ID'/g' deployments/admin/taskdef.json
- sed -i 's/<IMAGE_NAME>/'$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

View File

@ -0,0 +1,76 @@
{
"executionRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/2fas-admin-api_ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "2fas-admin-api",
"image": "<IMAGE_NAME>",
"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:<AWS_ACCOUNT_ID>:secret:prod/mysql-wi9cyz:password::"
},
{
"name": "MYSQL_USERNAME",
"valueFrom": "arn:aws:secretsmanager:us-east-2:<AWS_ACCOUNT_ID>:secret:prod/mysql-wi9cyz:username::"
},
{
"name": "MYSQL_HOST",
"valueFrom": "arn:aws:secretsmanager:us-east-2:<AWS_ACCOUNT_ID>:secret:prod/mysql-wi9cyz:host::"
},
{
"name": "MOBILE_DEBUG_AWS_ACCESS_KEY_ID",
"valueFrom": "arn:aws:secretsmanager:us-east-2:<AWS_ACCOUNT_ID>:secret:prod/mobile-X5wmei:AWS_ACCESS_KEY_ID::"
},
{
"name": "MOBILE_DEBUG_AWS_SECRET_ACCESS_KEY",
"valueFrom": "arn:aws:secretsmanager:us-east-2:<AWS_ACCOUNT_ID>:secret:prod/mobile-X5wmei:AWS_SECRET_ACCESS_KEY::"
},
{
"name": "S3_USER_ACCESS_KEY_ID",
"valueFrom": "arn:aws:secretsmanager:us-east-2:<AWS_ACCOUNT_ID>:secret:prod/api-EaED00:2fas_api_access_key_id::"
},
{
"name": "S3_USER_ACCESS_SECRET_KEY",
"valueFrom": "arn:aws:secretsmanager:us-east-2:<AWS_ACCOUNT_ID>:secret:prod/api-EaED00:2fas_api_access_secret_key::"
},
{
"name": "ICONS_S3_ACCESS_KEY_ID",
"valueFrom": "arn:aws:secretsmanager:us-east-2:<AWS_ACCOUNT_ID>:secret:prod/api-EaED00:icons_s3_access_key_id::"
},
{
"name": "ICONS_S3_ACCESS_SECRET_KEY",
"valueFrom": "arn:aws:secretsmanager:us-east-2:<AWS_ACCOUNT_ID>: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"
}

View File

@ -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:
go-modules:

31
docker/admin/Dockerfile Normal file
View File

@ -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"]

View File

@ -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

View File

@ -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)
}
}

View File

@ -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) {
}

View File

@ -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) {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}
}