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/crypto/tls" "code.crute.us/mcrute/golib/db/mongodb" glecho "code.crute.us/mcrute/golib/echo" glmw "code.crute.us/mcrute/golib/echo/middleware" "code.crute.us/mcrute/golib/service" "github.com/labstack/echo/v4" echomw "github.com/labstack/echo/v4/middleware" "github.com/spf13/cobra" "golang.org/x/time/rate" ) func Register(root *cobra.Command, embeddedTemplates fs.FS, version string) { webCmd := &cobra.Command{ Use: "web [options]", Short: "Run web server", Run: func(c *cobra.Command, args []string) { webMain(app.NewConfigFromCmd(c), embeddedTemplates, version) }, } webCmd.Flags().StringSlice("bind", []string{":8169"}, "Addresses and ports to bind http server") webCmd.Flags().StringSlice("bind-tls", []string{":8170"}, "Addresses and ports to bind https server") webCmd.Flags().String("log-file", "", "Log file for combined host logs") webCmd.Flags().String("tls-cache-dir", "ssl/", "Cache directory for TLS certificates") webCmd.Flags().StringSlice("trusted-ip-ranges", []string{"172.19.0.0/22", "2602:803:4072::/48"}, "Comma separated list of IP ranges for trusted XFF proxies") webCmd.Flags().StringSlice("management-ip-ranges", []string{"127.0.0.1/32", "::1/128", "172.19.0.0/22", "2602:803:4072::/48"}, "IP ranges for management systems") webCmd.Flags().StringSlice("hostname", []string{"dev.aws-access.crute.us"}, "Hostname this server serves (can be specified multiple times)") webCmd.Flags().Duration("rate-limit", 30*time.Second, "Number seconds between requests for credential resources") webCmd.Flags().Duration("auth-cookie-duration", 24*time.Hour, "Expiration duration of the auth cookies") webCmd.Flags().Int("rate-limit-burst", 30, "Number of burst requests allowed to credential endpoints") webCmd.Flags().String("issuer-endpoint", "https://aws-access.crute.us", "Oauth issuer endpoint") webCmd.Flags().String("jwt-audience", "aws-access", "Audience for issued JWTs") webCmd.Flags().String("github-oauth-vault-path", "", "Vault material name for GitHub auth credentials") webCmd.MarkFlagRequired("github-oauth-vault-path") root.AddCommand(webCmd) } func webMain(cfg app.Config, embeddedTemplates fs.FS, version string) { ctx := context.Background() s, err := glecho.NewDefaultEchoWithConfig(glecho.EchoConfig{ Debug: cfg.Debug, Hostnames: cfg.Hostnames, BindAddresses: cfg.Bind, BindTLSAddresses: cfg.BindTLS, TLSCacheDir: cfg.TLSCacheDir, TrustedProxyIPRanges: cfg.TrustedIPRanges, ManagementIPRanges: cfg.ManagementIPRanges, DiskTemplates: os.DirFS(cfg.TemplatePath), EmbeddedTemplates: embeddedTemplates, TemplateGlob: &cfg.TemplateGlob, CombinedHostLogFile: cfg.LogFile, ContentSecurityPolicy: &glmw.ContentSecurityPolicyConfig{ DefaultSrc: []glmw.CSPDirective{ glmw.CSPSelf, glmw.CSPUnsafeInline, }, StyleSrc: []glmw.CSPDirective{ glmw.CSPSelf, glmw.CSPData, glmw.CSPUnsafeInline, }, }, }) if err != nil { log.Fatalf("Error building echo: %w", err) } if err = s.Init(); err != nil { log.Fatalf("Error initializing echo: %w", err) } s.Use(middleware.AddServerHeader(version)) mongo, err := mongodb.Connect(ctx, cfg.MongoDbUri, cfg.MongodbVaultPath) if err != nil { log.Fatalf("Error connecting to mongodb: %w", err) } rateLimit := echomw.RateLimiter( echomw.NewRateLimiterMemoryStoreWithConfig( echomw.RateLimiterMemoryStoreConfig{ Rate: rate.Every(cfg.RateLimit), Burst: cfg.RateLimitBurst, ExpiresIn: time.Hour, }, ), ) adminAccountStore := &models.MongoDbAccountStore{ Db: mongo, ReturnDeleted: true, } as := &models.MongoDbAccountStore{Db: mongo} us := &models.MongoDbUserStore{Db: mongo} aws := &controllers.AWSAPI{Store: as} am := &middleware.AuthenticationMiddleware{ Store: us, CookieDuration: cfg.AuthCookieDuration, GitHub: &github.GitHubAuthenticator{ ClientId: cfg.GitHubOauthCreds.ClientId, ClientSecret: cfg.GitHubOauthCreds.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( "/:account/credentials", controllers.NewAPIRegionListHandler(aws), ) account.GET( "/:account/console", controllers.NewAPIConsoleRedirectHandler(aws, cfg.IssuerEndpoint), rateLimit, ) account.GET( "/:account/credentials/:region", controllers.NewAPICredentialsHandler(aws), rateLimit, ) (&controllers.APIAccountHandler{ Store: as, AdminStore: adminAccountStore, }).Register("/:account", account) } } s.GET("/favicon.ico", echo.NotFoundHandler) s.GET("/logout", controllers.LogoutHandler) s.CachedStaticRoute("/assets", "assets") s.GET("/", controllers.IndexHandler, am.Middleware) runner := service.NewAppRunner(ctx, s.Logger) runner.AddJob(s.RunCertificateManager) // Cert manager has to start before server runner.AddJob(tls.OcspErrorLogger(s.Logger, s.OcspErrors())) runner.AddJobs(s.MakeServerJobs()) runner.RunForever(!cfg.DisableBackgroundJobs) }