Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,33 @@ func (a *API) createRouter(cfg *config.Config) *fiber.App {
// PROTECTED ROUTES //
//////////////////////
// ORDER IS IMPORTANT: all routes applied AFTER the security middleware will require authn
protectedAPIRouter := apiRouter.Group("/")
if cfg.Security != nil {
if err := cfg.Security.RegisterHandlers(app); err != nil {
panic(err)
}
if err := cfg.Security.ApplySecurityMiddleware(protectedAPIRouter); err != nil {
panic(err)
if cfg.Security.IsGlobal() {
// Global auth: protect all API routes with middleware
protectedAPIRouter := apiRouter.Group("/")
if err := cfg.Security.ApplySecurityMiddleware(protectedAPIRouter); err != nil {
panic(err)
}
protectedAPIRouter.Get("/v1/endpoints/statuses", EndpointStatuses(cfg))
protectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus(cfg))
protectedAPIRouter.Get("/v1/suites/statuses", SuiteStatuses(cfg))
protectedAPIRouter.Get("/v1/suites/:key/statuses", SuiteStatus(cfg))
} else {
// Endpoint-level auth: routes are accessible but data is filtered
unprotectedAPIRouter.Get("/v1/endpoints/statuses", EndpointStatuses(cfg))
unprotectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus(cfg))
unprotectedAPIRouter.Get("/v1/suites/statuses", SuiteStatuses(cfg))
unprotectedAPIRouter.Get("/v1/suites/:key/statuses", SuiteStatus(cfg))
}
} else {
// No security: all routes accessible
unprotectedAPIRouter.Get("/v1/endpoints/statuses", EndpointStatuses(cfg))
unprotectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus(cfg))
unprotectedAPIRouter.Get("/v1/suites/statuses", SuiteStatuses(cfg))
unprotectedAPIRouter.Get("/v1/suites/:key/statuses", SuiteStatus(cfg))
}
protectedAPIRouter.Get("/v1/endpoints/statuses", EndpointStatuses(cfg))
protectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus(cfg))
protectedAPIRouter.Get("/v1/suites/statuses", SuiteStatuses(cfg))
protectedAPIRouter.Get("/v1/suites/:key/statuses", SuiteStatus(cfg))
return app
}
1 change: 1 addition & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func TestNew(t *testing.T) {
Username: "john.doe",
PasswordBcryptHashBase64Encoded: "JDJhJDA4JDFoRnpPY1hnaFl1OC9ISlFsa21VS09wOGlPU1ZOTDlHZG1qeTFvb3dIckRBUnlHUmNIRWlT",
},
Level: security.authLevelGlobal,
}
}
api := New(cfg)
Expand Down
5 changes: 3 additions & 2 deletions api/badge.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ func HealthBadge(c *fiber.Ctx) error {
return c.Status(400).SendString("invalid key encoding")
}
pagingConfig := paging.NewEndpointStatusParams()
status, err := store.Get().GetEndpointStatusByKey(key, pagingConfig.WithResults(1, 1))
status, err := store.Get().GetEndpointStatusByKey(key, true, pagingConfig.WithResults(1, 1))

if err != nil {
if errors.Is(err, common.ErrEndpointNotFound) {
return c.Status(404).SendString(err.Error())
Expand Down Expand Up @@ -148,7 +149,7 @@ func HealthBadgeShields(c *fiber.Ctx) error {
return c.Status(400).SendString("invalid key encoding")
}
pagingConfig := paging.NewEndpointStatusParams()
status, err := store.Get().GetEndpointStatusByKey(key, pagingConfig.WithResults(1, 1))
status, err := store.Get().GetEndpointStatusByKey(key, true, pagingConfig.WithResults(1, 1))
if err != nil {
if errors.Is(err, common.ErrEndpointNotFound) {
return c.Status(404).SendString(err.Error())
Expand Down
6 changes: 6 additions & 0 deletions api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@ type ConfigHandler struct {

func (handler ConfigHandler) GetConfig(c *fiber.Ctx) error {
hasOIDC := false
hasBasic := false
authLevel := ""
isAuthenticated := true // Default to true if no security config is set
if handler.securityConfig != nil {
hasOIDC = handler.securityConfig.OIDC != nil
hasBasic = handler.securityConfig.Basic != nil
authLevel = handler.securityConfig.Level
isAuthenticated = handler.securityConfig.IsAuthenticated(c)
}

// Prepare response with announcements
response := map[string]interface{}{
"oidc": hasOIDC,
"basic": hasBasic,
"authLevel": authLevel,
"authenticated": isAuthenticated,
}
// Add announcements if available, otherwise use empty slice
Expand Down
14 changes: 10 additions & 4 deletions api/endpoint_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ import (
// Due to how intensive this operation can be on the storage, this function leverages a cache.
func EndpointStatuses(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
// If no security is configured, user is considered authenticated (full access)
// If security is configured at endpoint level, check authentication status
userAuthenticated := cfg.Security == nil || cfg.Security.IsAuthenticated(c)
page, pageSize := extractPageAndPageSizeFromRequest(c, cfg.Storage.MaximumNumberOfResults)
value, exists := cache.Get(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize))
// Include authentication status in cache key to separate public/private cached results
cacheKey := fmt.Sprintf("endpoint-status-%d-%d-auth-%t", page, pageSize, userAuthenticated)
value, exists := cache.Get(cacheKey)
var data []byte
if !exists {
endpointStatuses, err := store.Get().GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(page, pageSize))
endpointStatuses, err := store.Get().GetAllEndpointStatuses(!userAuthenticated, paging.NewEndpointStatusParams().WithResults(page, pageSize))
if err != nil {
logr.Errorf("[api.EndpointStatuses] Failed to retrieve endpoint statuses: %s", err.Error())
return c.Status(500).SendString(err.Error())
Expand All @@ -42,7 +47,7 @@ func EndpointStatuses(cfg *config.Config) fiber.Handler {
logr.Errorf("[api.EndpointStatuses] Unable to marshal object to JSON: %s", err.Error())
return c.Status(500).SendString("unable to marshal object to JSON")
}
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize), data, cacheTTL)
cache.SetWithTTL(cacheKey, data, cacheTTL)
} else {
data = value.([]byte)
}
Expand Down Expand Up @@ -86,13 +91,14 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*end
// EndpointStatus retrieves a single endpoint.Status by group and endpoint name
func EndpointStatus(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
userAuthenticated := cfg.Security == nil || cfg.Security.IsAuthenticated(c)
page, pageSize := extractPageAndPageSizeFromRequest(c, cfg.Storage.MaximumNumberOfResults)
key, err := url.QueryUnescape(c.Params("key"))
if err != nil {
logr.Errorf("[api.EndpointStatus] Failed to decode key: %s", err.Error())
return c.Status(400).SendString("invalid key encoding")
}
endpointStatus, err := store.Get().GetEndpointStatusByKey(key, paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, cfg.Storage.MaximumNumberOfEvents))
endpointStatus, err := store.Get().GetEndpointStatusByKey(key, !userAuthenticated, paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, cfg.Storage.MaximumNumberOfEvents))
if err != nil {
if errors.Is(err, common.ErrEndpointNotFound) {
return c.Status(404).SendString(err.Error())
Expand Down
8 changes: 4 additions & 4 deletions api/endpoint_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,19 +180,19 @@ func TestEndpointStatuses(t *testing.T) {
Name: "no-pagination",
Path: "/api/v1/endpoints/statuses",
ExpectedCode: http.StatusOK,
ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}]}]`,
ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z","public":false},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z","public":false}]}]`,
},
{
Name: "pagination-first-result",
Path: "/api/v1/endpoints/statuses?page=1&pageSize=1",
ExpectedCode: http.StatusOK,
ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}]}]`,
ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z","public":false}]}]`,
},
{
Name: "pagination-second-result",
Path: "/api/v1/endpoints/statuses?page=2&pageSize=1",
ExpectedCode: http.StatusOK,
ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"}]}]`,
ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z","public":false}]}]`,
},
{
Name: "pagination-no-results",
Expand All @@ -204,7 +204,7 @@ func TestEndpointStatuses(t *testing.T) {
Name: "invalid-pagination-should-fall-back-to-default",
Path: "/api/v1/endpoints/statuses?page=INVALID&pageSize=INVALID",
ExpectedCode: http.StatusOK,
ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}]}]`,
ExpectedBody: `[{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z","public":false},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z","public":false}]}]`,
},
}

Expand Down
2 changes: 1 addition & 1 deletion api/external_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func TestCreateExternalEndpointResult(t *testing.T) {
})
}
t.Run("verify-end-results", func(t *testing.T) {
endpointStatus, err := store.Get().GetEndpointStatus("g", "n", paging.NewEndpointStatusParams().WithResults(1, 11))
endpointStatus, err := store.Get().GetEndpointStatus("g", "n", false, paging.NewEndpointStatusParams().WithResults(1, 11))
if err != nil {
t.Errorf("failed to get endpoint status: %s", err.Error())
return
Expand Down
20 changes: 15 additions & 5 deletions api/suite_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import (
// SuiteStatuses handles requests to retrieve all suite statuses
func SuiteStatuses(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
// Check authentication status - if no security or authenticated, user can see all
// If security exists at endpoint level and user is not authenticated, only show public
userAuthenticated := cfg.Security == nil || cfg.Security.IsAuthenticated(c)
page, pageSize := extractPageAndPageSizeFromRequest(c, 100)
params := paging.NewSuiteStatusParams().WithPagination(page, pageSize)
suiteStatuses, err := store.Get().GetAllSuiteStatuses(params)
suiteStatuses, err := store.Get().GetAllSuiteStatuses(!userAuthenticated, params)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": fmt.Sprintf("Failed to retrieve suite statuses: %v", err),
Expand All @@ -25,7 +28,10 @@ func SuiteStatuses(cfg *config.Config) fiber.Handler {
if len(suiteStatuses) == 0 {
for _, s := range cfg.Suites {
if s.IsEnabled() {
suiteStatuses = append(suiteStatuses, suite.NewStatus(s))
// Only include suites that match the visibility filter
if userAuthenticated || s.Public {
suiteStatuses = append(suiteStatuses, suite.NewStatus(s))
}
}
}
}
Expand All @@ -36,16 +42,20 @@ func SuiteStatuses(cfg *config.Config) fiber.Handler {
// SuiteStatus handles requests to retrieve a single suite's status
func SuiteStatus(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
userAuthenticated := cfg.Security == nil || cfg.Security.IsAuthenticated(c)
page, pageSize := extractPageAndPageSizeFromRequest(c, 100)
key := c.Params("key")
params := paging.NewSuiteStatusParams().WithPagination(page, pageSize)
status, err := store.Get().GetSuiteStatusByKey(key, params)
status, err := store.Get().GetSuiteStatusByKey(key, !userAuthenticated, params)
if err != nil || status == nil {
// Try to find the suite in config
for _, s := range cfg.Suites {
if s.Key() == key {
status = suite.NewStatus(s)
break
// Check visibility - only return if user is authenticated or suite is public
if userAuthenticated || s.Public {
status = suite.NewStatus(s)
break
}
}
}
if status == nil {
Expand Down
Loading