Compare commits

...

40 Commits

Author SHA1 Message Date
nikhilmantri0902
b46e536f3e Merge branch 'feat/fga-downtime' into feat/fga-rule-views 2026-10-29 18:35:34 +05:30
nikhilmantri0902
a07271dd6c Merge branch 'main' into feat/fga-downtime
# Conflicts:
#	pkg/ruler/authz.go
#	pkg/ruler/authz_test.go
#	pkg/signoz/provider.go
#	pkg/signoz/provider_test.go
#	pkg/signoz/signoz.go
#	tests/integration/tests/ruler/08_authz.py
2026-10-29 18:35:34 +05:30
nikhilmantri0902
7b3a63e11c chore(sqlmigration): renumber rule tuples migration to 142 2026-10-29 10:20:43 +05:30
nikhilmantri0902
d2e3c4a76f Merge branch 'feat/fga-downtime' into feat/fga-rule-views 2026-10-29 10:20:43 +05:30
nikhilmantri0902
3bac33d373 chore(sqlmigration): renumber downtime tuples migration to 141 2026-10-29 10:20:15 +05:30
nikhilmantri0902
c2069e6b56 Merge branch 'feat/fga-route-policies' into feat/fga-downtime 2026-10-29 10:20:15 +05:30
nikhilmantri0902
2047c24632 Merge branch 'main' into feat/fga-route-policies
# Conflicts:
#	pkg/signoz/provider.go
2026-10-29 10:20:15 +05:30
nikhilmantri0902
5a96a90071 chore(sqlmigration): renumber rule tuples migration to 141 2026-10-26 15:54:46 +05:30
nikhilmantri0902
816ea25eb9 Merge branch 'feat/fga-downtime' into feat/fga-rule-views 2026-10-26 15:54:45 +05:30
nikhilmantri0902
94bf33b1a5 chore(sqlmigration): renumber downtime tuples migration to 140 2026-10-26 15:54:24 +05:30
nikhilmantri0902
b03e1894da Merge branch 'feat/fga-route-policies' into feat/fga-downtime 2026-10-26 15:54:24 +05:30
nikhilmantri0902
a6b769cf89 Merge remote-tracking branch 'origin/main' into feat/fga-route-policies
# Conflicts:
#	pkg/signoz/provider.go
2026-10-26 15:53:46 +05:30
nikhilmantri0902
3542484942 test(authz): name the licensed rule view test by what it asserts 2026-10-26 15:28:46 +05:30
nikhilmantri0902
124504546f Merge branch 'feat/fga-downtime' into feat/fga-rule-views 2026-10-26 15:28:19 +05:30
nikhilmantri0902
42baa2bd1e test(authz): name the licensed downtime test by what it asserts 2026-10-26 15:28:18 +05:30
nikhilmantri0902
2e3239074b Merge branch 'feat/fga-route-policies' into feat/fga-downtime
# Conflicts:
#	pkg/ruler/authz_test.go
2026-10-26 15:28:01 +05:30
nikhilmantri0902
367b50d31e test(authz): name the licensed route policy test by what it asserts 2026-10-26 15:26:58 +05:30
nikhilmantri0902
b9577c2d60 refactor(alertmanager): resolve route policy channel names in the module
Route policy create and update now reject unknown channel names like rules
already do. The attach extractors call the same module method.
2026-10-26 15:26:58 +05:30
nikhilmantri0902
82a8dad4a3 test(authz): cover viewer rule views after license 2026-10-25 12:25:35 +05:30
nikhilmantri0902
4f3a87e1e5 refactor(sqlmigration): drop redundant managed role resync from rule tuple migration 2026-10-25 12:25:35 +05:30
nikhilmantri0902
bae259b730 Merge branch 'feat/fga-downtime' into feat/fga-rule-views 2026-10-25 12:24:54 +05:30
nikhilmantri0902
9ff51b89a1 test(authz): cover managed roles on downtime schedules after license 2026-10-25 12:24:43 +05:30
nikhilmantri0902
d995235bac refactor(authz): normalise downtime rule links once and use verb-first names 2026-10-25 12:24:43 +05:30
nikhilmantri0902
ca2545860d Merge branch 'feat/fga-route-policies' into feat/fga-downtime 2026-10-25 12:23:53 +05:30
nikhilmantri0902
ffc580265e test(authz): cover managed roles on route policies after license 2026-10-25 12:23:37 +05:30
nikhilmantri0902
9ddd1d000e refactor(authz): verb-first names for route policy helpers 2026-10-25 12:23:37 +05:30
nikhilmantri0902
c4f2b0a2ff Merge branch 'feat/fga-downtime' into feat/fga-rule-views 2026-10-24 23:44:50 +05:30
nikhilmantri0902
60befdff3a fix(authz): treat empty downtime rule set as a link to all rules 2026-10-24 22:33:12 +05:30
nikhilmantri0902
4f348519e6 Merge branch 'feat/fga-downtime' into feat/fga-rule-views 2026-10-24 18:53:15 +05:30
nikhilmantri0902
1d1a38309e test(authz): update managed role golden file for downtime links 2026-10-24 18:53:11 +05:30
nikhilmantri0902
34c2c20448 Merge branch 'feat/fga-route-policies' into feat/fga-downtime 2026-10-24 18:52:30 +05:30
nikhilmantri0902
2d2cd42320 test(authz): update managed role golden file for channel links 2026-10-24 18:52:21 +05:30
nikhilmantri0902
3bb1c9decd chore(openapi): regenerate spec for rule view scopes 2026-10-24 17:05:07 +05:30
nikhilmantri0902
8fc203934c feat(authz): rule view FGA
Rule view routes move from role gates to rule:list resource checks,
same as dashboard views.
2026-10-24 17:05:07 +05:30
nikhilmantri0902
253380995a chore(openapi): regenerate spec for downtime schedule scopes 2026-10-24 16:52:15 +05:30
nikhilmantri0902
82f57fd286 feat(authz): downtime schedule FGA
Downtime schedule routes move from role gates to resource checks.
Create and update check attach and detach on the rules in alertIds,
diffed against the stored schedule on update.
2026-10-24 16:52:14 +05:30
nikhilmantri0902
6f6f5abaaf fix(authz): reject unknown channel names on route policy links
A name that resolves to no channel was skipped, so the attach check
passed without covering it and the stored policy would route to a
channel created later under that name.
2026-10-24 16:33:25 +05:30
nikhilmantri0902
ac0f78eaf3 chore(authz): regenerate permissions for notification channel links 2026-10-24 16:17:29 +05:30
nikhilmantri0902
b32c4acc1f chore(openapi): regenerate spec for route policy scopes 2026-10-24 16:09:57 +05:30
nikhilmantri0902
694d8f6e53 feat(authz): route policy FGA
Route policy reads and writes move from role gates to resource checks.
Create and update also check attach and detach on the notification
channels the policy names, diffed against the stored policy on update.
2026-10-24 16:09:56 +05:30
14 changed files with 1315 additions and 125 deletions

View File

@@ -15064,9 +15064,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- planned-maintenance:list
- tokenizer:
- VIEWER
- planned-maintenance:list
summary: List downtime schedules
tags:
- downtimeschedules
@@ -15121,9 +15121,13 @@ paths:
description: Internal Server Error
security:
- api_key:
- EDITOR
- planned-maintenance:create
- planned-maintenance:attach
- rule:attach
- tokenizer:
- EDITOR
- planned-maintenance:create
- planned-maintenance:attach
- rule:attach
summary: Create downtime schedule
tags:
- downtimeschedules
@@ -15168,9 +15172,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- EDITOR
- planned-maintenance:delete
- tokenizer:
- EDITOR
- planned-maintenance:delete
summary: Delete downtime schedule
tags:
- downtimeschedules
@@ -15226,9 +15230,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- planned-maintenance:read
- tokenizer:
- VIEWER
- planned-maintenance:read
summary: Get downtime schedule by ID
tags:
- downtimeschedules
@@ -15283,9 +15287,17 @@ paths:
description: Internal Server Error
security:
- api_key:
- EDITOR
- planned-maintenance:update
- planned-maintenance:attach
- planned-maintenance:detach
- rule:attach
- rule:detach
- tokenizer:
- EDITOR
- planned-maintenance:update
- planned-maintenance:attach
- planned-maintenance:detach
- rule:attach
- rule:detach
summary: Update downtime schedule
tags:
- downtimeschedules
@@ -24931,9 +24943,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- rule:list
- tokenizer:
- VIEWER
- rule:list
summary: List rule saved views
tags:
- rules
@@ -24989,9 +25001,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- rule:list
- tokenizer:
- VIEWER
- rule:list
summary: Create rule saved view
tags:
- rules
@@ -25043,9 +25055,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- rule:list
- tokenizer:
- VIEWER
- rule:list
summary: Delete rule saved view
tags:
- rules
@@ -25112,9 +25124,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- rule:list
- tokenizer:
- VIEWER
- rule:list
summary: Update rule saved view
tags:
- rules

View File

@@ -95,6 +95,7 @@ type provider struct {
prometheusHandler prometheus.Handler
traceDetailHandler tracedetail.Handler
rulerHandler ruler.Handler
rulerService ruler.Ruler
llmPricingRuleHandler llmpricingrule.Handler
statsHandler statsreporter.Handler
savedViewHandler savedview.Handler
@@ -142,6 +143,7 @@ func NewFactory(
llmPricingRuleHandler llmpricingrule.Handler,
traceDetailHandler tracedetail.Handler,
rulerHandler ruler.Handler,
rulerService ruler.Ruler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
globalConfig global.Config,
@@ -196,6 +198,7 @@ func NewFactory(
llmPricingRuleHandler,
traceDetailHandler,
rulerHandler,
rulerService,
statsHandler,
savedViewHandler,
globalConfig,
@@ -252,6 +255,7 @@ func newProvider(
llmPricingRuleHandler llmpricingrule.Handler,
traceDetailHandler tracedetail.Handler,
rulerHandler ruler.Handler,
rulerService ruler.Ruler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
globalConfig global.Config,
@@ -307,6 +311,7 @@ func newProvider(
prometheusHandler: prometheusHandler,
traceDetailHandler: traceDetailHandler,
rulerHandler: rulerHandler,
rulerService: rulerService,
llmPricingRuleHandler: llmPricingRuleHandler,
statsHandler: statsHandler,
savedViewHandler: savedViewHandler,

View File

@@ -4,8 +4,11 @@ import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/ruler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/gorilla/mux"
)
@@ -146,131 +149,260 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rule_views", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListRuleViews), handler.OpenAPIDef{
ID: "ListRuleViews",
Tags: []string{"rules"},
Summary: "List rule saved views",
Description: "Returns every saved view in the calling user's org. Saved views are shared org-wide.",
Response: new(ruletypes.ListableRuleViews),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
if err := router.Handle("/api/v2/rule_views", handler.New(
provider.authzMiddleware.CheckResources(provider.rulerHandler.ListRuleViews, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "ListRuleViews",
Tags: []string{"rules"},
Summary: "List rule saved views",
Description: "Returns every saved view in the calling user's org. Saved views are shared org-wide.",
Response: new(ruletypes.ListableRuleViews),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceRule,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryDataAccess,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rule_views", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.CreateRuleView), handler.OpenAPIDef{
ID: "CreateRuleView",
Tags: []string{"rules"},
Summary: "Create rule saved view",
Description: "Persists the calling user's rule listing state (query, states, sort, order) as a named, reusable view shared across the org.",
Request: new(ruletypes.PostableRuleView),
RequestContentType: "application/json",
Response: new(ruletypes.RuleView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v2/rule_views", handler.New(
provider.authzMiddleware.CheckResources(provider.rulerHandler.CreateRuleView, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "CreateRuleView",
Tags: []string{"rules"},
Summary: "Create rule saved view",
Description: "Persists the calling user's rule listing state (query, states, sort, order) as a named, reusable view shared across the org.",
Request: new(ruletypes.PostableRuleView),
RequestContentType: "application/json",
Response: new(ruletypes.RuleView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceRule,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryConfigurationChange,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.UpdateRuleView), handler.OpenAPIDef{
ID: "UpdateRuleView",
Tags: []string{"rules"},
Summary: "Update rule saved view",
Description: "Replaces a saved view's name and data. Saved views are shared org-wide.",
Request: new(ruletypes.UpdatableRuleView),
RequestContentType: "application/json",
Response: new(ruletypes.RuleView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.rulerHandler.UpdateRuleView, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "UpdateRuleView",
Tags: []string{"rules"},
Summary: "Update rule saved view",
Description: "Replaces a saved view's name and data. Saved views are shared org-wide.",
Request: new(ruletypes.UpdatableRuleView),
RequestContentType: "application/json",
Response: new(ruletypes.RuleView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceRule,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryConfigurationChange,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.DeleteRuleView), handler.OpenAPIDef{
ID: "DeleteRuleView",
Tags: []string{"rules"},
Summary: "Delete rule saved view",
Description: "Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.",
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.rulerHandler.DeleteRuleView, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "DeleteRuleView",
Tags: []string{"rules"},
Summary: "Delete rule saved view",
Description: "Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.",
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourceRule,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryConfigurationChange,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListDowntimeSchedules), handler.OpenAPIDef{
ID: "ListDowntimeSchedules",
Tags: []string{"downtimeschedules"},
Summary: "List downtime schedules",
Description: "This endpoint lists all planned maintenance / downtime schedules",
RequestQuery: new(alertmanagertypes.ListPlannedMaintenanceParams),
Response: make([]*alertmanagertypes.PlannedMaintenance, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
if err := router.Handle("/api/v1/downtime_schedules", handler.New(
provider.authzMiddleware.CheckResources(provider.rulerHandler.ListDowntimeSchedules, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "ListDowntimeSchedules",
Tags: []string{"downtimeschedules"},
Summary: "List downtime schedules",
Description: "This endpoint lists all planned maintenance / downtime schedules",
RequestQuery: new(alertmanagertypes.ListPlannedMaintenanceParams),
Response: make([]*alertmanagertypes.PlannedMaintenance, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcePlannedMaintenance.Scope(coretypes.VerbList)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourcePlannedMaintenance,
Verb: coretypes.VerbList,
Category: coretypes.ActionCategoryDataAccess,
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.GetDowntimeScheduleByID), handler.OpenAPIDef{
ID: "GetDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Get downtime schedule by ID",
Description: "This endpoint returns a downtime schedule by ID",
Response: new(alertmanagertypes.PlannedMaintenance),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.rulerHandler.GetDowntimeScheduleByID, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName),
handler.OpenAPIDef{
ID: "GetDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Get downtime schedule by ID",
Description: "This endpoint returns a downtime schedule by ID",
Response: new(alertmanagertypes.PlannedMaintenance),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcePlannedMaintenance.Scope(coretypes.VerbRead)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourcePlannedMaintenance,
Verb: coretypes.VerbRead,
Category: coretypes.ActionCategoryDataAccess,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.CreateDowntimeSchedule), handler.OpenAPIDef{
ID: "CreateDowntimeSchedule",
Tags: []string{"downtimeschedules"},
Summary: "Create downtime schedule",
Description: "This endpoint creates a new planned maintenance / downtime schedule",
Request: new(alertmanagertypes.PostablePlannedMaintenance),
RequestContentType: "application/json",
Response: new(alertmanagertypes.PlannedMaintenance),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
if err := router.Handle("/api/v1/downtime_schedules", handler.New(
provider.authzMiddleware.CheckResources(provider.rulerHandler.CreateDowntimeSchedule, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName),
handler.OpenAPIDef{
ID: "CreateDowntimeSchedule",
Tags: []string{"downtimeschedules"},
Summary: "Create downtime schedule",
Description: "This endpoint creates a new planned maintenance / downtime schedule",
Request: new(alertmanagertypes.PostablePlannedMaintenance),
RequestContentType: "application/json",
Response: new(alertmanagertypes.PlannedMaintenance),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newScopedSecuritySchemes([]string{
coretypes.ResourceMetaResourcePlannedMaintenance.Scope(coretypes.VerbCreate),
coretypes.ResourceMetaResourcePlannedMaintenance.Scope(coretypes.VerbAttach),
coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbAttach),
}),
},
handler.WithResourceDefs(
handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourcePlannedMaintenance,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.ResponseJSONPath("data.id"),
Selector: coretypes.WildcardSelector,
},
handler.AttachDetachSiblingResourceDef{
Verb: coretypes.VerbAttach,
Category: coretypes.ActionCategoryConfigurationChange,
SourceResource: coretypes.ResourceMetaResourcePlannedMaintenance,
SourceIDs: coretypes.OneID(coretypes.ResponseJSONPath("data.id")),
SourceSelector: coretypes.WildcardSelector,
TargetResource: coretypes.ResourceMetaResourceRule,
TargetIDs: ruler.DowntimeRuleIDsExtractor,
TargetSelector: ruler.DowntimeRuleSelector,
},
),
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.UpdateDowntimeScheduleByID), handler.OpenAPIDef{
ID: "UpdateDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Update downtime schedule",
Description: "This endpoint updates a downtime schedule by ID",
Request: new(alertmanagertypes.PostablePlannedMaintenance),
RequestContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPut).GetError(); err != nil {
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.rulerHandler.UpdateDowntimeScheduleByID, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName),
handler.OpenAPIDef{
ID: "UpdateDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Update downtime schedule",
Description: "This endpoint updates a downtime schedule by ID",
Request: new(alertmanagertypes.PostablePlannedMaintenance),
RequestContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newScopedSecuritySchemes([]string{
coretypes.ResourceMetaResourcePlannedMaintenance.Scope(coretypes.VerbUpdate),
coretypes.ResourceMetaResourcePlannedMaintenance.Scope(coretypes.VerbAttach),
coretypes.ResourceMetaResourcePlannedMaintenance.Scope(coretypes.VerbDetach),
coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbAttach),
coretypes.ResourceMetaResourceRule.Scope(coretypes.VerbDetach),
}),
},
handler.WithResourceDefs(
handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourcePlannedMaintenance,
Verb: coretypes.VerbUpdate,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
},
handler.AttachDetachSiblingResourceDef{
Verb: coretypes.VerbAttach,
Category: coretypes.ActionCategoryConfigurationChange,
SourceResource: coretypes.ResourceMetaResourcePlannedMaintenance,
SourceIDs: coretypes.OneID(coretypes.PathParam("id")),
SourceSelector: coretypes.IDSelector,
TargetResource: coretypes.ResourceMetaResourceRule,
TargetIDs: ruler.NewDowntimeRuleAttachExtractor(provider.rulerService),
TargetSelector: ruler.DowntimeRuleSelector,
},
handler.AttachDetachSiblingResourceDef{
Verb: coretypes.VerbDetach,
Category: coretypes.ActionCategoryConfigurationChange,
SourceResource: coretypes.ResourceMetaResourcePlannedMaintenance,
SourceIDs: coretypes.OneID(coretypes.PathParam("id")),
SourceSelector: coretypes.IDSelector,
TargetResource: coretypes.ResourceMetaResourceRule,
TargetIDs: ruler.NewDowntimeRuleDetachExtractor(provider.rulerService),
TargetSelector: ruler.DowntimeRuleSelector,
},
),
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authzMiddleware.EditAccess(provider.rulerHandler.DeleteDowntimeScheduleByID), handler.OpenAPIDef{
ID: "DeleteDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Delete downtime schedule",
Description: "This endpoint deletes a downtime schedule by ID",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodDelete).GetError(); err != nil {
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(
provider.authzMiddleware.CheckResources(provider.rulerHandler.DeleteDowntimeScheduleByID, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName),
handler.OpenAPIDef{
ID: "DeleteDowntimeScheduleByID",
Tags: []string{"downtimeschedules"},
Summary: "Delete downtime schedule",
Description: "This endpoint deletes a downtime schedule by ID",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceMetaResourcePlannedMaintenance.Scope(coretypes.VerbDelete)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceMetaResourcePlannedMaintenance,
Verb: coretypes.VerbDelete,
Category: coretypes.ActionCategoryConfigurationChange,
ID: coretypes.PathParam("id"),
Selector: coretypes.IDSelector,
}),
)).Methods(http.MethodDelete).GetError(); err != nil {
return err
}

View File

@@ -17,6 +17,73 @@ const (
type diffSide int
var DowntimeRuleIDsExtractor = coretypes.ResourceIDsExtractor{Phase: coretypes.PhaseRequest, Fn: func(ec coretypes.ExtractorContext) ([]string, error) {
ids, err := coretypes.BodyJSONArray("alertIds").Fn(ec)
if err != nil {
return nil, err
}
return defaultToAllRules(ids), nil
}}
var DowntimeRuleSelector coretypes.SelectorFunc = func(ctx context.Context, resource coretypes.Resource, id string, orgID valuer.UUID) ([]coretypes.Selector, error) {
if id == coretypes.WildCardSelectorString {
return coretypes.WildcardSelector(ctx, resource, id, orgID)
}
return coretypes.IDSelector(ctx, resource, id, orgID)
}
func NewDowntimeRuleAttachExtractor(rulerService Ruler) coretypes.ResourceIDsExtractor {
return newDowntimeDiffExtractor(rulerService, attachedSide)
}
func NewDowntimeRuleDetachExtractor(rulerService Ruler) coretypes.ResourceIDsExtractor {
return newDowntimeDiffExtractor(rulerService, detachedSide)
}
func newDowntimeDiffExtractor(rulerService Ruler, side diffSide) coretypes.ResourceIDsExtractor {
return coretypes.ResourceIDsExtractor{Phase: coretypes.PhaseRequest, Fn: func(ec coretypes.ExtractorContext) ([]string, error) {
if ec.Request == nil {
return nil, nil
}
scheduleID, err := valuer.NewUUID(mux.Vars(ec.Request)["id"])
if err != nil {
return nil, err
}
stored, err := rulerService.MaintenanceStore().GetPlannedMaintenanceByID(ec.Request.Context(), scheduleID)
if err != nil {
return nil, err
}
incoming, err := DowntimeRuleIDsExtractor.Fn(ec)
if err != nil {
return nil, err
}
attached, detached := diffDowntimeRuleLinks(stored.RuleIDs, incoming)
if side == detachedSide {
return detached, nil
}
return attached, nil
}}
}
func diffDowntimeRuleLinks(stored, incoming []string) (attached, detached []string) {
return diffLinks(defaultToAllRules(stored), incoming)
}
func defaultToAllRules(ids []string) []string {
if len(ids) == 0 {
return []string{coretypes.WildCardSelectorString}
}
return ids
}
func NewCreateRoutePolicyChannelAttachExtractor(alertmanagerService alertmanager.Alertmanager) coretypes.ResourceIDsExtractor {
return coretypes.ResourceIDsExtractor{Phase: coretypes.PhaseRequest, Fn: func(ec coretypes.ExtractorContext) ([]string, error) {
if ec.Request == nil {

View File

@@ -3,7 +3,9 @@ package ruler
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDiffLinks(t *testing.T) {
@@ -75,3 +77,84 @@ func TestDiffLinks(t *testing.T) {
})
}
}
func TestDiffDowntimeRuleLinks(t *testing.T) {
testCases := []struct {
name string
stored []string
incoming []string
wantAttached []string
wantDetached []string
}{
{
name: "AllToAll_NoChange",
stored: nil,
incoming: []string{"*"},
wantAttached: []string{},
wantDetached: []string{},
},
{
name: "NarrowFromAllToOne_DetachesAllAttachesOne",
stored: nil,
incoming: []string{"r1"},
wantAttached: []string{"r1"},
wantDetached: []string{"*"},
},
{
name: "WidenFromOneToAll_DetachesOneAttachesAll",
stored: []string{"r1"},
incoming: []string{"*"},
wantAttached: []string{"*"},
wantDetached: []string{"r1"},
},
{
name: "SwapRules_DiffsIDs",
stored: []string{"r1"},
incoming: []string{"r2"},
wantAttached: []string{"r2"},
wantDetached: []string{"r1"},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
attached, detached := diffDowntimeRuleLinks(testCase.stored, testCase.incoming)
assert.ElementsMatch(t, testCase.wantAttached, attached)
assert.ElementsMatch(t, testCase.wantDetached, detached)
})
}
}
func TestDowntimeRuleIDsExtractor(t *testing.T) {
testCases := []struct {
name string
body string
wantIDs []string
}{
{
name: "MissingAlertIds_AllRules",
body: `{"name": "window"}`,
wantIDs: []string{"*"},
},
{
name: "EmptyAlertIds_AllRules",
body: `{"alertIds": []}`,
wantIDs: []string{"*"},
},
{
name: "ExplicitAlertIds_Kept",
body: `{"alertIds": ["r1", "r2"]}`,
wantIDs: []string{"r1", "r2"},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ids, err := DowntimeRuleIDsExtractor.Fn(coretypes.ExtractorContext{RequestBody: []byte(testCase.body)})
require.NoError(t, err)
assert.Equal(t, testCase.wantIDs, ids)
})
}
}

View File

@@ -105,6 +105,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ llmpricingrule.Handler }{},
struct{ tracedetail.Handler }{},
struct{ ruler.Handler }{},
struct{ ruler.Ruler }{},
struct{ statsreporter.Handler }{},
struct{ savedview.Handler }{},
global.Config{},

View File

@@ -48,6 +48,7 @@ import (
"github.com/SigNoz/signoz/pkg/prometheus/clickhouseprometheusv2"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/querier/signozquerier"
"github.com/SigNoz/signoz/pkg/ruler"
"github.com/SigNoz/signoz/pkg/sharder"
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
"github.com/SigNoz/signoz/pkg/sharder/singlesharder"
@@ -267,6 +268,8 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddGCPConnectionMethodFactory(),
sqlmigration.NewDeleteOrphanSARolesFactory(),
sqlmigration.NewAddRoutePolicyTuplesFactory(sqlstore),
sqlmigration.NewAddDowntimeTuplesFactory(sqlstore),
sqlmigration.NewAddRuleTuplesFactory(sqlstore),
)
}
@@ -331,7 +334,7 @@ func NewQuerierProviderFactories(telemetryStore telemetrystore.TelemetryStore, p
)
}
func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.AuthZ, modules Modules, handlers Handlers, globalConfig global.Config, gatewayService gateway.Gateway, identNResolver identn.IdentNResolver, sharder sharder.Sharder, auditor auditor.Auditor, web web.Web, alertmanagerService alertmanager.Alertmanager) factory.NamedMap[factory.ProviderFactory[apiserver.APIServer, apiserver.Config]] {
func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.AuthZ, modules Modules, handlers Handlers, globalConfig global.Config, gatewayService gateway.Gateway, identNResolver identn.IdentNResolver, sharder sharder.Sharder, auditor auditor.Auditor, web web.Web, alertmanagerService alertmanager.Alertmanager, rulerService ruler.Ruler) factory.NamedMap[factory.ProviderFactory[apiserver.APIServer, apiserver.Config]] {
return factory.MustNewNamedMap(
signozapiserver.NewFactory(
orgGetter,
@@ -373,6 +376,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.LLMPricingRuleHandler,
handlers.TraceDetail,
handlers.RulerHandler,
rulerService,
handlers.StatsHandler,
handlers.SavedView,
globalConfig,

View File

@@ -107,6 +107,7 @@ func TestNewProviderFactories(t *testing.T) {
nil,
nil,
nil,
nil,
)
})
}

View File

@@ -622,7 +622,7 @@ func New(
ctx,
providerSettings,
config.APIServer,
NewAPIServerProviderFactories(orgGetter, authz, modules, handlers, config.Global, gateway, identNResolver, sharder, auditor, web, alertmanager),
NewAPIServerProviderFactories(orgGetter, authz, modules, handlers, config.Global, gateway, identNResolver, sharder, auditor, web, alertmanager, rulerInstance),
"signoz",
)
if err != nil {

View File

@@ -0,0 +1,174 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
)
type addDowntimeTuples struct {
sqlstore sqlstore.SQLStore
}
func NewAddDowntimeTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_downtime_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addDowntimeTuples{sqlstore: sqlstore}, nil
})
}
func (migration *addDowntimeTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addDowntimeTuples) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var storeID string
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
if err != nil {
return err
}
var orgIDs []string
err = tx.NewSelect().
Table("organizations").
Column("id").
Scan(ctx, &orgIDs)
if err != nil && err != sql.ErrNoRows {
return err
}
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
crudVerbs := []string{"create", "read", "update", "delete", "list"}
readVerbs := []string{"read", "list"}
linkVerbs := []string{"attach", "detach"}
tuples := make([]migrationTuple, 0, 20)
for _, verb := range crudVerbs {
tuples = append(tuples,
migrationTuple{authtypes.SigNozAdminRoleName, "metaresource", "planned-maintenance", verb},
migrationTuple{authtypes.SigNozEditorRoleName, "metaresource", "planned-maintenance", verb},
)
}
for _, verb := range readVerbs {
tuples = append(tuples, migrationTuple{authtypes.SigNozViewerRoleName, "metaresource", "planned-maintenance", verb})
}
for _, objectName := range []string{"planned-maintenance", "rule"} {
for _, verb := range linkVerbs {
tuples = append(tuples,
migrationTuple{authtypes.SigNozAdminRoleName, "metaresource", objectName, verb},
migrationTuple{authtypes.SigNozEditorRoleName, "metaresource", objectName, verb},
)
}
}
for _, orgID := range orgIDs {
for _, tuple := range tuples {
entropy := ulid.DefaultEntropy()
now := time.Now().UTC()
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
if isPG {
user := "role:" + roleSubject + "#assignee"
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
)
if err != nil {
return err
}
} else {
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
)
if err != nil {
return err
}
}
}
}
managedRoleGroups := make(map[string]string, len(coretypes.ManagedRoleToTransactions))
for roleName, transactions := range coretypes.ManagedRoleToTransactions {
data, err := json.Marshal(authtypes.NewTransactionGroupsFromTransactions(transactions))
if err != nil {
return err
}
managedRoleGroups[roleName] = string(data)
}
for _, orgID := range orgIDs {
for roleName, data := range managedRoleGroups {
if _, err := tx.NewUpdate().
Model(new(roles)).
Set("transaction_groups = ?", data).
Where("org_id = ?", orgID).
Where("type = ?", authtypes.RoleTypeManaged.StringValue()).
Where("name = ?", roleName).
Exec(ctx); err != nil {
return err
}
}
}
return tx.Commit()
}
func (migration *addDowntimeTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -0,0 +1,140 @@
package sqlmigration
import (
"context"
"database/sql"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/migrate"
)
type addRuleTuples struct {
sqlstore sqlstore.SQLStore
}
func NewAddRuleTuplesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_rule_tuples"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addRuleTuples{sqlstore: sqlstore}, nil
})
}
func (migration *addRuleTuples) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addRuleTuples) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var storeID string
err = tx.QueryRowContext(ctx, `SELECT id FROM store WHERE name = ? LIMIT 1`, "signoz").Scan(&storeID)
if err != nil {
return err
}
var orgIDs []string
err = tx.NewSelect().
Table("organizations").
Column("id").
Scan(ctx, &orgIDs)
if err != nil && err != sql.ErrNoRows {
return err
}
isPG := migration.sqlstore.BunDB().Dialect().Name() == dialect.PG
crudVerbs := []string{"create", "read", "update", "delete", "list"}
readVerbs := []string{"read", "list"}
tuples := make([]migrationTuple, 0, 12)
for _, verb := range crudVerbs {
tuples = append(tuples,
migrationTuple{authtypes.SigNozAdminRoleName, "metaresource", "rule", verb},
migrationTuple{authtypes.SigNozEditorRoleName, "metaresource", "rule", verb},
)
}
for _, verb := range readVerbs {
tuples = append(tuples, migrationTuple{authtypes.SigNozViewerRoleName, "metaresource", "rule", verb})
}
for _, orgID := range orgIDs {
for _, tuple := range tuples {
entropy := ulid.DefaultEntropy()
now := time.Now().UTC()
tupleID := ulid.MustNew(ulid.Timestamp(now), entropy).String()
objectID := "organization/" + orgID + "/" + tuple.objectName + "/*"
roleSubject := "organization/" + orgID + "/role/" + tuple.roleName
if isPG {
user := "role:" + roleSubject + "#assignee"
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, user, 0, tupleID, now,
)
if err != nil {
return err
}
} else {
result, err := tx.ExecContext(ctx, `
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
continue
}
_, err = tx.ExecContext(ctx, `
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, tuple.objectType, objectID, tuple.relation, "role", roleSubject, "assignee", 0, tupleID, now,
)
if err != nil {
return err
}
}
}
}
return tx.Commit()
}
func (migration *addRuleTuples) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -134,12 +134,16 @@ var ManagedRoleToTransactions = map[string][]Transaction{
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbAttach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbDetach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
// rule — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbAttach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbDetach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
// saved-view — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
@@ -245,12 +249,16 @@ var ManagedRoleToTransactions = map[string][]Transaction{
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbAttach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
{Verb: VerbDetach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindPlannedMaintenance}, WildCardSelectorString)},
// rule — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbDelete, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbList, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbAttach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
{Verb: VerbDetach, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindRule}, WildCardSelectorString)},
// saved-view — full CRUD
{Verb: VerbRead, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},
{Verb: VerbUpdate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindSavedView}, WildCardSelectorString)},

View File

@@ -188,7 +188,9 @@
"read",
"update",
"delete",
"list"
"list",
"attach",
"detach"
]
},
{
@@ -199,7 +201,9 @@
"read",
"update",
"delete",
"list"
"list",
"attach",
"detach"
]
},
{
@@ -420,7 +424,9 @@
"read",
"update",
"delete",
"list"
"list",
"attach",
"detach"
]
},
{
@@ -431,7 +437,9 @@
"read",
"update",
"delete",
"list"
"list",
"attach",
"detach"
]
},
{

View File

@@ -14,6 +14,10 @@ from fixtures.auth import (
from fixtures.role import find_role_by_name, transaction_group
from fixtures.types import Operation, SigNoz, TestContainerDocker
V2_RULES_URL = "/api/v2/rules"
V3_RULES_URL = "/api/v3/rules"
RULE_VIEWS_URL = "/api/v2/rule_views"
DOWNTIME_URL = "/api/v1/downtime_schedules"
ROUTE_POLICIES_URL = "/api/v1/route_policies"
_EDITOR_EMAIL = "editor+alertauthz@integration.test"
@@ -24,6 +28,10 @@ _ACTOR_EMAIL = "customrole+alertauthz@integration.test"
_ACTOR_PASSWORD = "password123Z$"
_ACTOR_ROLE_NAME = "alert-authz-actor"
_TARGET_RULE = "alert authz target"
_VIEW_NAME = "alert-authz-view"
_DOWNTIME_NAME = "alert-authz-downtime"
_ACTOR_DOWNTIME_NAME = "alert-authz-actor-downtime"
_ROUTE_POLICY_NAME = "alert-authz-route-policy"
_ACTOR_ROUTE_POLICY_NAME = "alert-authz-actor-route-policy"
@@ -32,6 +40,43 @@ SEED_CHANNEL_ALT = {"name": "alert-authz-channel-alt", "email_configs": [{"to":
ROUTE_POLICY_TEMPLATE = {"expression": 'threshold.name == "critical"', "channels": [SEED_CHANNEL["name"]], "description": "authz coverage policy"}
RULE_CONDITION = {
"thresholds": {
"kind": "basic",
"spec": [{"name": "critical", "target": 90, "matchType": "at_least_once", "op": "above", "channels": [SEED_CHANNEL["name"]]}],
},
"compositeQuery": {
"queryType": "builder",
"panelType": "graph",
"queries": [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "metrics",
"aggregations": [{"metricName": "alert_authz_cpu", "timeAggregation": "avg", "spaceAggregation": "max"}],
},
}
],
},
"selectedQueryName": "A",
}
RULE_TEMPLATE = {
"description": "authz coverage rule",
"alertType": "METRIC_BASED_ALERT",
"ruleType": "threshold_rule",
"condition": RULE_CONDITION,
"labels": {"severity": "critical"},
"annotations": {"summary": "s", "description": "d"},
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m0s", "frequency": "1m"}},
"notificationSettings": {"groupBy": [], "usePolicy": False, "renotify": {"enabled": False, "interval": "30m", "alertStates": []}},
"version": "v5",
"schemaVersion": "v2alpha1",
}
DOWNTIME_SCHEDULE = {"timezone": "UTC", "startTime": "2026-09-21T00:00:00Z", "endTime": "2026-09-22T00:00:00Z"}
_ACTOR_BASE_TRANSACTION_GROUPS = [
transaction_group("create", "metaresource", "route-policy", ["*"]),
transaction_group("read", "metaresource", "route-policy", ["*"]),
@@ -40,6 +85,14 @@ _ACTOR_BASE_TRANSACTION_GROUPS = [
transaction_group("delete", "metaresource", "route-policy", ["*"]),
transaction_group("attach", "metaresource", "route-policy", ["*"]),
transaction_group("detach", "metaresource", "route-policy", ["*"]),
transaction_group("create", "metaresource", "planned-maintenance", ["*"]),
transaction_group("read", "metaresource", "planned-maintenance", ["*"]),
transaction_group("list", "metaresource", "planned-maintenance", ["*"]),
transaction_group("update", "metaresource", "planned-maintenance", ["*"]),
transaction_group("delete", "metaresource", "planned-maintenance", ["*"]),
transaction_group("attach", "metaresource", "planned-maintenance", ["*"]),
transaction_group("detach", "metaresource", "planned-maintenance", ["*"]),
transaction_group("attach", "metaresource", "rule", ["*"]),
]
@@ -97,6 +150,29 @@ def test_setup_users_and_channels(
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.get(
signoz.self.host_configs["8080"].get(f"{V3_RULES_URL}?limit=200"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
for rule in response.json()["data"]["rules"]:
if rule["alert"] == _TARGET_RULE:
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{V2_RULES_URL}/{rule['id']}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.post(
signoz.self.host_configs["8080"].get(V2_RULES_URL),
json={**RULE_TEMPLATE, "alert": _TARGET_RULE},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, response.text
response = requests.post(
signoz.self.host_configs["8080"].get(ROUTE_POLICIES_URL),
json={**ROUTE_POLICY_TEMPLATE, "name": _ROUTE_POLICY_NAME},
@@ -207,6 +283,164 @@ def test_route_policy_naming_unknown_channel_rejected(
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
def test_editor_allowed_on_downtime_mutations(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
token = get_token(_EDITOR_EMAIL, _EDITOR_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(DOWNTIME_URL),
json={"name": _DOWNTIME_NAME, "description": "authz coverage window", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, response.text
downtime_id = response.json()["data"]["id"]
response = requests.put(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
json={"name": _DOWNTIME_NAME, "description": "authz coverage window updated", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
def test_editor_allowed_on_downtime_naming_rules(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
token = get_token(_EDITOR_EMAIL, _EDITOR_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get(f"{V3_RULES_URL}?limit=200"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
target_id = {rule["alert"]: rule["id"] for rule in response.json()["data"]["rules"]}[_TARGET_RULE]
response = requests.post(
signoz.self.host_configs["8080"].get(DOWNTIME_URL),
json={"name": _DOWNTIME_NAME, "description": "authz coverage window with rules", "schedule": DOWNTIME_SCHEDULE, "alertIds": [target_id]},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, response.text
downtime_id = response.json()["data"]["id"]
response = requests.put(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
json={"name": _DOWNTIME_NAME, "description": "authz coverage window without rules", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
def test_viewer_allowed_on_downtime_reads_and_forbidden_on_writes(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
token = get_token(_VIEWER_EMAIL, _VIEWER_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(DOWNTIME_URL),
json={"name": _DOWNTIME_NAME, "description": "authz coverage window", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, response.text
downtime_id = response.json()["data"]["id"]
for path in (DOWNTIME_URL, f"{DOWNTIME_URL}/{downtime_id}"):
response = requests.get(
signoz.self.host_configs["8080"].get(path),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, f"GET {path}: expected 200, got {response.status_code}: {response.text}"
for method, path in (
("post", DOWNTIME_URL),
("put", f"{DOWNTIME_URL}/{downtime_id}"),
("delete", f"{DOWNTIME_URL}/{downtime_id}"),
):
response = getattr(requests, method)(
signoz.self.host_configs["8080"].get(path),
json={"name": _DOWNTIME_NAME, "description": "authz coverage window", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.FORBIDDEN, f"{method.upper()} {path}: expected 403, got {response.status_code}: {response.text}"
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
def test_viewer_allowed_on_rule_views(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
token = get_token(_VIEWER_EMAIL, _VIEWER_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
response = requests.post(
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
json={"name": _VIEW_NAME, "data": {"version": "v1", "sort": "updated_at", "order": "desc"}},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, response.text
view_id = response.json()["data"]["id"]
response = requests.put(
signoz.self.host_configs["8080"].get(f"{RULE_VIEWS_URL}/{view_id}"),
json={"name": _VIEW_NAME, "data": {"version": "v1", "sort": "updated_at", "order": "asc"}},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{RULE_VIEWS_URL}/{view_id}"),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
def test_apply_license(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
@@ -256,6 +490,94 @@ def test_route_policy_managed_role_tuples_enforced_with_license(
assert response.status_code == HTTPStatus.NO_CONTENT, f"admin delete must pass on the migrated route-policy delete tuple: {response.text}"
def test_downtime_managed_role_tuples_enforced_with_license(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
editor_token = get_token(_EDITOR_EMAIL, _EDITOR_PASSWORD)
viewer_token = get_token(_VIEWER_EMAIL, _VIEWER_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get(f"{V3_RULES_URL}?limit=200"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, f"admin rule list must pass: {response.text}"
target_id = {rule["alert"]: rule["id"] for rule in response.json()["data"]["rules"]}[_TARGET_RULE]
response = requests.post(
signoz.self.host_configs["8080"].get(DOWNTIME_URL),
json={"name": _DOWNTIME_NAME, "description": "authz licensed window with rules", "schedule": DOWNTIME_SCHEDULE, "alertIds": [target_id]},
headers={"Authorization": f"Bearer {editor_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, f"editor create with a rule must pass on the migrated planned-maintenance create and rule attach tuples: {response.text}"
downtime_id = response.json()["data"]["id"]
response = requests.get(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
headers={"Authorization": f"Bearer {viewer_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, f"viewer read must pass on the migrated planned-maintenance read tuple: {response.text}"
response = requests.put(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
json={"name": _DOWNTIME_NAME, "description": "authz licensed window without rules", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {viewer_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.FORBIDDEN, f"viewer update must be forbidden, the migration grants viewers no planned-maintenance update tuple: {response.text}"
response = requests.put(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
json={"name": _DOWNTIME_NAME, "description": "authz licensed window without rules", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {editor_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, f"editor widen to all rules must pass on the migrated rule attach and detach tuples: {response.text}"
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
headers={"Authorization": f"Bearer {editor_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, f"editor delete must pass on the migrated planned-maintenance delete tuple: {response.text}"
def test_rule_view_managed_role_tuples_enforced_with_license(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
viewer_token = get_token(_VIEWER_EMAIL, _VIEWER_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
headers={"Authorization": f"Bearer {viewer_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, f"viewer list must pass on the migrated rule list tuple: {response.text}"
response = requests.post(
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
json={"name": _VIEW_NAME, "data": {"version": "v1", "sort": "updated_at", "order": "desc"}},
headers={"Authorization": f"Bearer {viewer_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, f"viewer create must pass, rule views are gated on rule list like dashboard views: {response.text}"
view_id = response.json()["data"]["id"]
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{RULE_VIEWS_URL}/{view_id}"),
headers={"Authorization": f"Bearer {viewer_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, f"viewer delete must pass on the migrated rule list tuple: {response.text}"
def test_setup_channel_scoped_actor(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
@@ -380,6 +702,202 @@ def test_route_policy_channel_changes_follow_channel_attach_and_detach(
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
def test_rule_views_forbidden_without_rule_list(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.FORBIDDEN, response.text
response = requests.post(
signoz.self.host_configs["8080"].get(RULE_VIEWS_URL),
json={"name": _VIEW_NAME, "data": {"version": "v1", "sort": "updated_at", "order": "desc"}},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.FORBIDDEN, response.text
def test_downtime_naming_rule_forbidden_without_rule_attach(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
role_id = find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)
response = requests.get(
signoz.self.host_configs["8080"].get(f"{V3_RULES_URL}?limit=200"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
target_id = {rule["alert"]: rule["id"] for rule in response.json()["data"]["rules"]}[_TARGET_RULE]
response = requests.put(
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{role_id}"),
json={"description": "", "transactionGroups": [group for group in _ACTOR_BASE_TRANSACTION_GROUPS if group != transaction_group("attach", "metaresource", "rule", ["*"])]},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
for alert_ids in ([target_id], []):
response = requests.post(
signoz.self.host_configs["8080"].get(DOWNTIME_URL),
json={"name": _ACTOR_DOWNTIME_NAME, "description": "scoped actor window", "schedule": DOWNTIME_SCHEDULE, "alertIds": alert_ids},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.FORBIDDEN, f"alertIds={alert_ids}: expected 403, got {response.status_code}: {response.text}"
response = requests.put(
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{role_id}"),
json={"description": "", "transactionGroups": _ACTOR_BASE_TRANSACTION_GROUPS},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
def test_downtime_rule_removal_forbidden_without_rule_detach(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get(f"{V3_RULES_URL}?limit=200"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
target_id = {rule["alert"]: rule["id"] for rule in response.json()["data"]["rules"]}[_TARGET_RULE]
response = requests.post(
signoz.self.host_configs["8080"].get(DOWNTIME_URL),
json={"name": _ACTOR_DOWNTIME_NAME, "description": "scoped actor window", "schedule": DOWNTIME_SCHEDULE, "alertIds": [target_id]},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, response.text
downtime_id = response.json()["data"]["id"]
response = requests.put(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
json={"name": _ACTOR_DOWNTIME_NAME, "description": "scoped actor window without rules", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.FORBIDDEN, response.text
response = requests.put(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
json={"name": _ACTOR_DOWNTIME_NAME, "description": "scoped actor window still with rules", "schedule": DOWNTIME_SCHEDULE, "alertIds": [target_id]},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.put(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
json={"name": _ACTOR_DOWNTIME_NAME, "description": "scoped actor window without rules", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
def test_downtime_widening_to_all_rules_forbidden_without_wildcard_attach(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
role_id = find_role_by_name(signoz, admin_token, _ACTOR_ROLE_NAME)
response = requests.get(
signoz.self.host_configs["8080"].get(f"{V3_RULES_URL}?limit=200"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
target_id = {rule["alert"]: rule["id"] for rule in response.json()["data"]["rules"]}[_TARGET_RULE]
response = requests.put(
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{role_id}"),
json={
"description": "",
"transactionGroups": [group for group in _ACTOR_BASE_TRANSACTION_GROUPS if group != transaction_group("attach", "metaresource", "rule", ["*"])]
+ [
transaction_group("attach", "metaresource", "rule", [target_id]),
transaction_group("detach", "metaresource", "rule", ["*"]),
],
},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
token = get_token(_ACTOR_EMAIL, _ACTOR_PASSWORD)
response = requests.post(
signoz.self.host_configs["8080"].get(DOWNTIME_URL),
json={"name": _ACTOR_DOWNTIME_NAME, "description": "scoped actor window", "schedule": DOWNTIME_SCHEDULE, "alertIds": [target_id]},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.CREATED, response.text
downtime_id = response.json()["data"]["id"]
response = requests.put(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
json={"name": _ACTOR_DOWNTIME_NAME, "description": "scoped actor window for all rules", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.FORBIDDEN, response.text
response = requests.put(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
json={"name": _ACTOR_DOWNTIME_NAME, "description": "scoped actor window for all rules", "schedule": DOWNTIME_SCHEDULE, "alertIds": []},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{downtime_id}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.put(
signoz.self.host_configs["8080"].get(f"/api/v1/roles/{role_id}"),
json={"description": "", "transactionGroups": _ACTOR_BASE_TRANSACTION_GROUPS},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
def test_alert_authz_cleanup(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
@@ -387,6 +905,43 @@ def test_alert_authz_cleanup(
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = requests.get(
signoz.self.host_configs["8080"].get(DOWNTIME_URL),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
for schedule in response.json()["data"] or []:
if schedule["name"] in (_DOWNTIME_NAME, _ACTOR_DOWNTIME_NAME):
response = requests.put(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{schedule['id']}"),
json={"name": schedule["name"], "description": schedule["description"], "schedule": schedule["schedule"], "alertIds": []},
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{DOWNTIME_URL}/{schedule['id']}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.get(
signoz.self.host_configs["8080"].get(f"{V3_RULES_URL}?limit=200"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, response.text
for rule in response.json()["data"]["rules"]:
if rule["alert"] == _TARGET_RULE:
response = requests.delete(
signoz.self.host_configs["8080"].get(f"{V2_RULES_URL}/{rule['id']}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.NO_CONTENT, response.text
response = requests.get(
signoz.self.host_configs["8080"].get(ROUTE_POLICIES_URL),
headers={"Authorization": f"Bearer {admin_token}"},