package web import ( "context" "io/fs" "log" "os" "time" "code.crute.us/mcrute/cloud-identity-broker/app" "code.crute.us/mcrute/cloud-identity-broker/app/controllers" "code.crute.us/mcrute/cloud-identity-broker/app/middleware" "code.crute.us/mcrute/cloud-identity-broker/app/models" "code.crute.us/mcrute/cloud-identity-broker/auth" "code.crute.us/mcrute/cloud-identity-broker/auth/github" "code.crute.us/mcrute/golib/cli" "code.crute.us/mcrute/golib/clients/autocert/v2" "code.crute.us/mcrute/golib/clients/netbox/v3" "code.crute.us/mcrute/golib/db/mongodb/v2" glecho "code.crute.us/mcrute/golib/echo" glmw "code.crute.us/mcrute/golib/echo/middleware" "code.crute.us/mcrute/golib/secrets" echomw "github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4" "github.com/spf13/cobra" "golang.org/x/time/rate" ) func Register(root *cobra.Command, embeddedTemplates fs.FS, appVersion string) { webCmd := &cobra.Command{ Use: "web [options]", Short: "Run web server", Run: func(c *cobra.Command, args []string) { cfg := app.Config{} cli.MustGetConfig(c, &cfg) webMain(cfg, embeddedTemplates, appVersion) }, } cli.AddFlags(webCmd, &app.Config{}, app.DefaultConfig, "web") root.AddCommand(webCmd) } // webMain does the pretty standard golib/Echo setup func webMain(cfg app.Config, embeddedTemplates fs.FS, version string) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() s, err := glecho.NewEchoWrapper(ctx, cfg.Debug) if err != nil { log.Fatalf("Error building echo: %s", err) } vc, err := glecho.MakeVaultSecretsClient(ctx) if err != nil { log.Fatalf("Error making vault client %s", err) } if err = s.Configure(glecho.EchoConfig{ ApplicationName: "cloud-identity-broker", ApplicationVersion: version, BindAddresses: cfg.Bind, DiskTemplates: os.DirFS("templates/"), EmbeddedTemplates: embeddedTemplates, TrustedProxyIPRanges: cfg.TrustedIPRanges, CombinedHostLogFile: cfg.LogFile, ContentSecurityPolicy: &glmw.ContentSecurityPolicyConfig{ DefaultSrc: []glmw.CSPDirective{ glmw.CSPSelf, glmw.CSPUnsafeInline, }, StyleSrc: []glmw.CSPDirective{ glmw.CSPSelf, glmw.CSPData, glmw.CSPUnsafeInline, }, }, Autocert: autocert.MustNewAutocertWrapper(ctx, autocert.AutocertConfig{ ApiKey: secrets.MustGetApiKey(vc, ctx, cfg.DNSApiKeyVaultPath).Key, Hosts: cfg.Hostnames, Email: cfg.AutocertEmail, CertHost: cfg.AutocertHost, }), NetboxClient: &netbox.BasicNetboxClient{ Endpoint: cfg.NetboxHost, ApiKey: secrets.MustGetApiKey(vc, ctx, cfg.NetboxApiKeyVaultPath).Key, }, }); err != nil { log.Fatalf("Error building echo: %s", err) } glecho.AttachSecretsClient(vc, cancel, s.Runner(), s.Logger) mongo, err := mongodb.Connect(ctx, cfg.MongoDbUri, vc) if err != nil { log.Fatalf("Error connecting to mongodb: %s", err) } setupApplication(ctx, cfg, s, mongo, vc) s.RunForever(!cfg.DisableBackgroundJobs) } // setupApplication does the setup work that's specific to this // application func setupApplication(ctx context.Context, cfg app.Config, s *glecho.EchoWrapper, mongo *mongodb.Mongo, vc secrets.Client) { rateLimit := echomw.RateLimiter( echomw.NewRateLimiterMemoryStoreWithConfig( echomw.RateLimiterMemoryStoreConfig{ Rate: rate.Every(cfg.RateLimit), Burst: cfg.RateLimitBurst, ExpiresIn: time.Hour, }, ), ) // These admin prefixed stores have unsafe-by-default behavior that's // necessary for some admin workflows. They should never be used in // non-admin workflows. adminAccountStore := &models.MongoDbAccountStore{ Db: mongo, ReturnDeleted: true, } adminUserStore := &models.MongoDbUserStore{ Db: mongo, ReturnDeleted: true, } // Use these for non-admin workflows as := &models.MongoDbAccountStore{Db: mongo} us := &models.MongoDbUserStore{Db: mongo} aws := &controllers.AWSAPI{ Store: as, Secrets: vc, } if errs := aws.Preload(ctx); len(errs) > 0 { for _, err := range errs { log.Printf("Error preloading AWS accounts: %s", err) } log.Fatalf("Could not preload all AWS accounts") } ghCred := &app.GitHubOauthCreds{} if _, err := vc.Secret(ctx, cfg.GitHubOauthCreds, &ghCred); err != nil { log.Fatalf("Error retrieving GitHub credentials: %s", err) } am := &middleware.AuthenticationMiddleware{ Store: us, CookieDuration: cfg.AuthCookieDuration, GitHub: &github.GitHubAuthenticator{ ClientId: ghCred.ClientId, ClientSecret: ghCred.ClientSecret, }, JWTManager: &auth.JWTManager{ Store: us, Audience: cfg.JWTAudience, TokenExpires: cfg.AuthCookieDuration, }, } am.RegisterUrls(s) api := s.Group("/api") api.Use(glmw.VaryCookie()) api.Use(glmw.CacheNeverMiddleware) api.Use(am.Middleware) { api.GET("", controllers.APIIndexHandler) account := api.Group("/account") { account.GET("", controllers.NewAPIAccountListHandler(as)) account.GET( "/:provider/:account/credentials", controllers.NewAPIRegionListHandler(aws), ) account.GET( "/:provider/:account/console", controllers.NewAPIConsoleRedirectHandler(aws, cfg.IssuerEndpoint), rateLimit, ) account.GET( "/:provider/:account/credentials/:region", controllers.NewAPICredentialsHandler(aws), rateLimit, ) (&controllers.APIAccountHandler{ Store: as, AdminStore: adminAccountStore, }).Register("/:provider/:account", "", account) } user := api.Group("/user") user.Use(middleware.RequireAdminPrivileges) { user.GET("", controllers.NewAPIUserListHandler(us)) (&controllers.APIUserHandler{ Store: adminUserStore, }).Register("/:user", "", user) } } s.GET("/favicon.ico", echo.NotFoundHandler) s.GET("/logout", controllers.LogoutHandler) s.CachedStaticRoute("/assets", "assets") s.GET("/", controllers.IndexHandler, am.Middleware) }