diff --git a/cmd/server/main.go b/cmd/server/main.go index 59baf02f..8132e352 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -217,6 +217,7 @@ func run(ctx context.Context, cfg common.ConfigStore, stderr io.Writer, listener CheckInterval: cfg.Get(common.HealthCheckIntervalKey), Metrics: metrics, } + jobs := maintenance.NewJobs(businessDB) updateConfigFunc := func(ctx context.Context) { cfg.Update(ctx) @@ -225,6 +226,7 @@ func run(ctx context.Context, cfg common.ConfigStore, stderr io.Writer, listener businessDB.UpdateConfig(maintenanceMode) timeSeriesDB.UpdateConfig(maintenanceMode) portalServer.UpdateConfig(ctx, cfg) + jobs.UpdateConfig(cfg) verboseLogs := config.AsBool(cfg.Get(common.VerboseKey)) common.SetLogLevel(logLevel, verboseLogs) } @@ -308,7 +310,6 @@ func run(ctx context.Context, cfg common.ConfigStore, stderr io.Writer, listener }() // start maintenance jobs - jobs := maintenance.NewJobs(businessDB) jobs.Add(healthCheck) jobs.Add(&maintenance.SessionsCleanupJob{ Session: portalServer.Sessions, @@ -372,7 +373,7 @@ func run(ctx context.Context, cfg common.ConfigStore, stderr io.Writer, listener if localAddress := cfg.Get(common.LocalAddressKey).Value(); len(localAddress) > 0 { localRouter := http.NewServeMux() metrics.Setup(localRouter) - jobs.Setup(localRouter) + jobs.Setup(localRouter, cfg) localRouter.Handle(http.MethodGet+" /"+common.LiveEndpoint, common.Recovered(http.HandlerFunc(healthCheck.LiveHandler))) localRouter.Handle(http.MethodGet+" /"+common.ReadyEndpoint, common.Recovered(http.HandlerFunc(healthCheck.ReadyHandler))) localServer = &http.Server{ diff --git a/docker/pc.env.example b/docker/pc.env.example index 2931b89b..d85af797 100644 --- a/docker/pc.env.example +++ b/docker/pc.env.example @@ -1,5 +1,6 @@ STAGE=dev PC_LOCAL_ADDRESS=localhost:9090 +PC_LOCAL_API_KEY=BkdIDPmLPg6MDRbrr1TDLKMkhy PC_PORTAL_BASE_URL=portal.privatecaptcha.local PC_API_BASE_URL=api.privatecaptcha.local PC_CDN_BASE_URL=cdn.privatecaptcha.local diff --git a/pkg/common/config.go b/pkg/common/config.go index 5615013f..f5a1cf79 100644 --- a/pkg/common/config.go +++ b/pkg/common/config.go @@ -39,6 +39,7 @@ const ( UserFingerprintIVKey APISaltKey EnterpriseLicenseKeyKey + LocalAPIKeyKey // Add new fields _above_ COMMON_CONFIG_KEYS_COUNT ) diff --git a/pkg/config/env.go b/pkg/config/env.go index e797a107..b0f67506 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -60,6 +60,7 @@ func init() { configKeyToEnvName[common.EmailFromKey] = "PC_EMAIL_FROM" configKeyToEnvName[common.ReplyToEmailKey] = "PC_REPLY_TO_EMAIL" configKeyToEnvName[common.LocalAddressKey] = "PC_LOCAL_ADDRESS" + configKeyToEnvName[common.LocalAPIKeyKey] = "PC_LOCAL_API_KEY" configKeyToEnvName[common.MaintenanceModeKey] = "PC_MAINTENANCE_MODE" configKeyToEnvName[common.RegistrationAllowedKey] = "PC_REGISTRATION_ALLOWED" configKeyToEnvName[common.HealthCheckIntervalKey] = "PC_HEALTHCHECK_INTERVAL" diff --git a/pkg/maintenance/jobs.go b/pkg/maintenance/jobs.go index ac8b284a..3b72c31e 100644 --- a/pkg/maintenance/jobs.go +++ b/pkg/maintenance/jobs.go @@ -28,6 +28,7 @@ type jobs struct { oneOffJobs []common.OneOffJob maintenanceCancel context.CancelFunc maintenanceCtx context.Context + apiKey string mux sync.Mutex } @@ -71,10 +72,43 @@ func (j *jobs) Run() { } } -func (j *jobs) Setup(mux *http.ServeMux) { +func (j *jobs) UpdateConfig(cfg common.ConfigStore) { + j.apiKey = cfg.Get(common.LocalAPIKeyKey).Value() +} + +func (j *jobs) Setup(mux *http.ServeMux, cfg common.ConfigStore) { + j.apiKey = cfg.Get(common.LocalAPIKeyKey).Value() + const maxBytes = 256 * 1024 - mux.Handle(http.MethodPost+" /maintenance/periodic/{job}", common.Recovered(http.MaxBytesHandler(http.HandlerFunc(j.handlePeriodicJob), maxBytes))) - mux.Handle(http.MethodPost+" /maintenance/oneoff/{job}", common.Recovered(http.MaxBytesHandler(http.HandlerFunc(j.handleOneoffJob), maxBytes))) + mux.Handle(http.MethodPost+" /maintenance/periodic/{job}", common.Recovered(http.MaxBytesHandler(j.security(http.HandlerFunc(j.handlePeriodicJob)), maxBytes))) + mux.Handle(http.MethodPost+" /maintenance/oneoff/{job}", common.Recovered(http.MaxBytesHandler(j.security(http.HandlerFunc(j.handleOneoffJob)), maxBytes))) +} + +func (j *jobs) security(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if len(j.apiKey) == 0 { + slog.WarnContext(ctx, "Endpoint is not allowed without a configured API key") + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + + secret := r.Header.Get(common.HeaderAPIKey) + if len(secret) == 0 { + slog.WarnContext(ctx, "Request API key is empty") + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if secret != j.apiKey { + slog.WarnContext(ctx, "Request API key does not match", "value", secret) + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) } func (j *jobs) handlePeriodicJob(w http.ResponseWriter, r *http.Request) {