package server import ( "context" "flag" "fmt" "net/http" "os" "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" "github.com/go-logr/logr" "github.com/peterbourgon/ff/v3" "edge-infra.dev/pkg/edge/api/middleware" "edge-infra.dev/pkg/lib/fog" "edge-infra.dev/pkg/sds/emergencyaccess/apierror" errorhandler "edge-infra.dev/pkg/sds/emergencyaccess/apierror/handler" "edge-infra.dev/pkg/sds/emergencyaccess/authservice" "edge-infra.dev/pkg/sds/emergencyaccess/authservice/setup" eamiddleware "edge-infra.dev/pkg/sds/emergencyaccess/middleware" "edge-infra.dev/pkg/sds/emergencyaccess/msgdata" "edge-infra.dev/pkg/sds/emergencyaccess/types" ) type Authservice interface { AuthorizeCommand(ctx context.Context, payload authservice.CommandAuthPayload) (authservice.Validation, error) AuthorizeRequest(ctx context.Context, payload authservice.AuthorizeRequestPayload) (msgdata.Request, error) AuthorizeTarget(ctx context.Context, target authservice.Target) error AuthorizeUser(ctx context.Context) error ResolveTarget(ctx context.Context, payload authservice.ResolveTargetPayload) (authservice.Target, error) } type Server struct { GinEngine *gin.Engine AuthService Authservice Log logr.Logger } func New(router *gin.Engine, log logr.Logger, authService Authservice, checks ...func() error) Server { server := Server{ GinEngine: router, Log: log, AuthService: authService, } server.newGinServer(checks...) return server } func (server *Server) newGinServer(checks ...func() error) { router := server.GinEngine router.ContextWithFallback = true router.Use(middleware.SetRequestContext()) router.Use(eamiddleware.SaveAuthToContext()) router.Use(gin.Recovery()) router.Any("/ready", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) router.Any("/health", eamiddleware.HealthCheck(checks...)) public := router.Group("/") public.Use(requestid.New(requestid.WithCustomHeaderStrKey(eamiddleware.CorrelationIDKey))) public.Use(eamiddleware.SetLoggerInContext(server.Log)) public.Use(eamiddleware.RequestBookendLogs()) public.Use(eamiddleware.VerifyUserDetailsInContext()) public.POST("/authorizeUser", server.authorizeUser) public.POST("/authorizeCommand", server.authorizeCommand) public.POST("/authorizeRequest", server.authorizeRequest) public.POST("/authorizeTarget", server.authorizeTarget) public.POST("/resolveTarget", server.resolveTarget) } func (server Server) authorizeRequest(c *gin.Context) { log := fog.FromContext(c) // Checks incoming payload is valid. var payload authservice.AuthorizeRequestPayload if err := c.ShouldBindJSON(&payload); err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrPayloadStructure, err)) return } if err := payload.Validate(); err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrPayloadProperties, err)) return } log = log.WithValues( "targetProjectID", payload.Target.ProjectID, "targetBannerUUID", payload.Target.BannerID, "targetStoreUUID", payload.Target.StoreID, "targetTerminalUUID", payload.Target.TerminalID, ) c.Request = c.Request.Clone(fog.IntoContext(c.Request.Context(), log)) req, err := server.AuthService.AuthorizeRequest(c.Request.Context(), payload) // If no user in context, use empty string user, _ := types.UserFromContext(c) // log regardless of result of call log.Info("Authorize Request Called", "request", payload.Request, "requestID", eamiddleware.GetCorrelationID(c), "userID", user.Username, "commandAuthorized", (err == nil), ) if err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrSendFailure, err)) return } c.JSON(http.StatusOK, map[string]msgdata.Request{ "request": req, }) } func (server Server) authorizeCommand(c *gin.Context) { log := fog.FromContext(c) // Checks incoming payload is valid. var payload authservice.CommandAuthPayload if err := c.ShouldBindJSON(&payload); err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrPayloadStructure, err)) return } if err := payload.Validate(); err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrPayloadProperties, err)) return } log = log.WithValues("command", payload.Command, "targetBannerUUID", payload.Target.BannerID) c.Request = c.Request.Clone(fog.IntoContext(c.Request.Context(), log)) // Currently the message commandID/requestID is set to the incoming correlationID requestID := eamiddleware.GetCorrelationID(c) val, err := server.AuthService.AuthorizeCommand(c.Request.Context(), payload) // If no user in context, use empty string user, _ := types.UserFromContext(c) userID := user.Username // log regardless of result of call log.Info("Authorize Command Called", "requestID", requestID, "userID", userID, "targetProjectID", payload.Target.ProjectID, "targetStoreUUID", payload.Target.StoreID, "targetTerminalUUID", payload.Target.TerminalID, "commandAuthorized", val, "darkmode", payload.AuthDetails.DarkMode, ) if err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrSendFailure, err)) return } c.JSON(http.StatusOK, val) } func (server *Server) authorizeTarget(c *gin.Context) { var payload authservice.AuthorizeTargetPayload if err := c.ShouldBindJSON(&payload); err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrPayloadStructure, err)) return } if err := payload.Validate(); err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrPayloadProperties, err)) return } log := fog.FromContext(c) // log user provided target target := payload.Target // If no user in context, use empty string user, _ := types.UserFromContext(c) userID := user.Username defer func() { log.Info("Authorize Target Called", "userID", userID, "targetProjectID", target.ProjectID, "targetBannerUUID", target.BannerID, "targetStoreUUID", target.StoreID, "targetTerminalUUID", target.TerminalID, ) }() err := server.AuthService.AuthorizeTarget(c, payload.Target) if err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrAuthFailure, err)) return } c.Status(http.StatusOK) } func (server *Server) resolveTarget(c *gin.Context) { var payload authservice.ResolveTargetPayload if err := c.ShouldBindJSON(&payload); err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrPayloadStructure, err)) return } if err := payload.Validate(); err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrPayloadProperties, err)) return } var err error target, err := server.AuthService.ResolveTarget(c.Request.Context(), payload) if err != nil { errorhandler.ErrorHandler(c, apierror.E(apierror.ErrAuthFailure, err)) return } payload.Target = target c.JSON(http.StatusOK, payload) } func (server Server) authorizeUser(c *gin.Context) { log := fog.FromContext(c) // If no user in context, use empty string user, _ := types.UserFromContext(c) userID := user.Username auditLog := log.WithValues( "userID", userID, ) if err := server.AuthService.AuthorizeUser(c); err != nil { auditLog.Info("Authorize User Called", "authorized", false) errorhandler.ErrorHandler(c, apierror.E(apierror.ErrAuthFailure, err)) return } auditLog.Info("Authorize User Called", "authorized", true) c.Status(http.StatusOK) } func Run() error { router := gin.New() log := newLogger() config := setup.Config{} flags := flag.NewFlagSet("ea-authservice", flag.ExitOnError) config.BindFlags(flags) // flags passed as --user-service-host from cli or USER_SERVICE_HOST in env will be parsed here. if err := ff.Parse(flags, os.Args[1:], ff.WithEnvVarNoPrefix(), ff.WithIgnoreUndefined(true)); err != nil { return err } if err := config.AuthService.Validate(); err != nil { return fmt.Errorf("invalid authservice configuration: %w", err) } authService, checks, err := setup.CreateAuthservice(log, config) if err != nil { return err } server := New(router, log, authService, checks...) return server.GinEngine.Run() }