NOISSUE - Add access control listing in alarms, rules engine and reports (#3417)

Signed-off-by: Arvindh <arvindh91@gmail.com>
This commit is contained in:
Arvindh
2026-04-02 15:21:26 +05:30
committed by GitHub
parent cc84466e7d
commit 8e774b3398
6 changed files with 201 additions and 51 deletions
+36 -18
View File
@@ -21,7 +21,7 @@ import (
)
const alarmColumns = `alarms.id, alarms.rule_id, alarms.domain_id, alarms.channel_id, alarms.client_id, alarms.subtopic, alarms.measurement, alarms.value, alarms.unit,
alarms.threshold, alarms.cause, alarms.status, alarms.severity, alarms.assignee_id, alarms.created_at, alarms.updated_at, alarms.updated_by, alarms.assigned_at,
alarms.threshold, alarms.cause, alarms.status, alarms.severity, alarms.assignee_id, alarms.created_at, alarms.updated_at, alarms.updated_by, alarms.assigned_at,
alarms.assigned_by, alarms.acknowledged_at, alarms.acknowledged_by, alarms.resolved_at, alarms.resolved_by, alarms.metadata`
type repository struct {
@@ -134,8 +134,8 @@ func (r *repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarm
}
q := fmt.Sprintf(`UPDATE alarms SET %s updated_by = :updated_by, updated_at = :updated_at WHERE id = :id
RETURNING id, rule_id, domain_id, channel_id, client_id, subtopic, measurement, value, unit, threshold,
cause, status, severity, assignee_id, assigned_at, assigned_by, acknowledged_at, acknowledged_by,
RETURNING id, rule_id, domain_id, channel_id, client_id, subtopic, measurement, value, unit, threshold,
cause, status, severity, assignee_id, assigned_at, assigned_by, acknowledged_at, acknowledged_by,
resolved_by, resolved_at, metadata, created_at, updated_by, updated_at;`, upq)
dba, err := toDBAlarm(alarm)
@@ -199,17 +199,30 @@ func (r *repository) ListAllAlarms(ctx context.Context, pm alarms.PageMetadata)
}
func (r *repository) ListUserAlarms(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
query, err := pageQuery(pm)
if err != nil {
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
clauses := []string{
`(
EXISTS (
SELECT 1
FROM rules_roles rr
JOIN rules_role_members rrm ON rrm.role_id = rr.id
WHERE rr.entity_id = alarms.rule_id AND rrm.member_id = :user_id
)
OR EXISTS (
SELECT 1
FROM domains_roles dr
JOIN domains_role_members drm ON drm.role_id = dr.id
JOIN domains_role_actions dra ON dra.role_id = dr.id
WHERE dr.entity_id = alarms.domain_id
AND drm.member_id = :user_id
AND dra.action LIKE 'alarm%'
)
)`,
}
clauses = append(clauses, pageQueryConditions(pm)...)
query := fmt.Sprintf("WHERE %s", strings.Join(clauses, " AND "))
pm.UserID = userID
comQuery := fmt.Sprintf(`SELECT DISTINCT %s
FROM alarms
INNER JOIN rules_roles rr ON rr.entity_id = alarms.rule_id
INNER JOIN rules_role_members rrm ON rrm.role_id = rr.id AND rrm.member_id = :user_id
%s`, alarmColumns, query)
comQuery := fmt.Sprintf(`SELECT DISTINCT %s FROM alarms %s`, alarmColumns, query)
return r.alarmsPage(ctx, comQuery, pm)
}
@@ -462,6 +475,17 @@ func toAlarm(dbr dbAlarm) (alarms.Alarm, error) {
}
func pageQuery(pm alarms.PageMetadata) (string, error) {
query := pageQueryConditions(pm)
var emq string
if len(query) > 0 {
emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
}
return emq, nil
}
func pageQueryConditions(pm alarms.PageMetadata) []string {
var query []string
if pm.DomainID != "" {
query = append(query, "alarms.domain_id = :domain_id")
@@ -508,11 +532,5 @@ func pageQuery(pm alarms.PageMetadata) (string, error) {
if !pm.CreatedTo.IsZero() {
query = append(query, "alarms.created_at <= :created_to")
}
var emq string
if len(query) > 0 {
emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
}
return emq, nil
return query
}
+32 -1
View File
@@ -417,7 +417,15 @@ func TestListAlarms(t *testing.T) {
func TestListUserAlarms(t *testing.T) {
t.Cleanup(func() {
_, err := db.Exec("DELETE FROM alarms")
_, err := db.Exec("DELETE FROM domains_role_actions")
require.Nil(t, err, fmt.Sprintf("clean domains_role_actions unexpected error: %s", err))
_, err = db.Exec("DELETE FROM domains_role_members")
require.Nil(t, err, fmt.Sprintf("clean domains_role_members unexpected error: %s", err))
_, err = db.Exec("DELETE FROM domains_roles")
require.Nil(t, err, fmt.Sprintf("clean domains_roles unexpected error: %s", err))
_, err = db.Exec("DELETE FROM domains")
require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err))
_, err = db.Exec("DELETE FROM alarms")
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
_, err = db.Exec("DELETE FROM rules")
require.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err))
@@ -426,9 +434,14 @@ func TestListUserAlarms(t *testing.T) {
repo := postgres.NewAlarmsRepo(db)
domainID := generateUUID(t)
domainRoute := generateUUID(t)
userID := generateUUID(t)
otherUserID := generateUUID(t)
adminUserID := generateUUID(t)
domainUserID := generateUUID(t)
_, err := db.Exec(`INSERT INTO domains (id, name, route, status) VALUES ($1, $2, $3, $4)`, domainID, namegen.Generate(), domainRoute, 0)
require.Nil(t, err, fmt.Sprintf("insert domains unexpected error: %s", err))
// Create 10 rules and 10 alarms referencing them.
// Assign userID to the first 6 rules via role membership.
@@ -485,6 +498,14 @@ func TestListUserAlarms(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("insert rules_role_members unexpected error: %s", err))
}
domainRoleID := generateUUID(t)
_, err = db.Exec(`INSERT INTO domains_roles (id, name, entity_id) VALUES ($1, $2, $3)`, domainRoleID, "admin", domainID)
require.Nil(t, err, fmt.Sprintf("insert domains_roles unexpected error: %s", err))
_, err = db.Exec(`INSERT INTO domains_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, domainRoleID, domainUserID, domainID)
require.Nil(t, err, fmt.Sprintf("insert domains_role_members unexpected error: %s", err))
_, err = db.Exec(`INSERT INTO domains_role_actions (role_id, action) VALUES ($1, $2)`, domainRoleID, "alarm_read")
require.Nil(t, err, fmt.Sprintf("insert domains_role_actions unexpected error: %s", err))
_ = createdAlarms
cases := []struct {
@@ -566,6 +587,16 @@ func TestListUserAlarms(t *testing.T) {
count: 10,
err: nil,
},
{
desc: "list alarms for user with domain-level rule access returns all alarms",
userID: domainUserID,
pm: alarms.PageMetadata{
Offset: 0,
Limit: 100,
},
count: 10,
err: nil,
},
{
desc: "list user alarms ordered by created_at ascending",
userID: userID,
+33 -14
View File
@@ -404,22 +404,37 @@ func (repo *PostgresRepository) ListAllRules(ctx context.Context, pm re.PageMeta
func (repo *PostgresRepository) ListUserRules(ctx context.Context, userID string, pm re.PageMeta) (re.Page, error) {
pm.UserID = userID
pq := pageRulesQuery(pm)
clauses := []string{
`(
EXISTS (
SELECT 1
FROM rules_roles rr
JOIN rules_role_members rrm ON rrm.role_id = rr.id
WHERE rr.entity_id = r.id AND rrm.member_id = :user_id
)
OR EXISTS (
SELECT 1
FROM domains_roles dr
JOIN domains_role_members drm ON drm.role_id = dr.id
JOIN domains_role_actions dra ON dra.role_id = dr.id
WHERE dr.entity_id = r.domain_id
AND drm.member_id = :user_id
AND dra.action LIKE 'rule%'
)
)`,
}
clauses = append(clauses, pageRulesQueryConditions(pm)...)
orderClause := rulesOrderClause(pm)
pgData := rulesPageData(pm)
userJoin := `
INNER JOIN rules_roles rr ON rr.entity_id = r.id
INNER JOIN rules_role_members rrm ON rrm.role_id = rr.id AND rrm.member_id = :user_id
`
whereClause := fmt.Sprintf("WHERE %s", strings.Join(clauses, " AND "))
innerQ := fmt.Sprintf(`
SELECT DISTINCT r.id, r.name, r.domain_id, r.tags, r.input_channel, r.input_topic, r.logic_type, r.logic_value, r.outputs,
r.start_datetime, r.time, r.recurring, r.recurring_period, r.created_at, r.created_by, r.updated_at, r.updated_by, r.status
FROM rules r
%s
%s
`, userJoin, pq)
`, whereClause)
q := fmt.Sprintf(`
SELECT * FROM (%s) AS sub %s %s;
@@ -521,6 +536,16 @@ func rulesPageData(pm re.PageMeta) string {
}
func pageRulesQuery(pm re.PageMeta) string {
query := pageRulesQueryConditions(pm)
var q string
if len(query) > 0 {
q = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
}
return q
}
func pageRulesQueryConditions(pm re.PageMeta) []string {
var query []string
if pm.InputChannel != "" {
query = append(query, "r.input_channel = :input_channel")
@@ -546,11 +571,5 @@ func pageRulesQuery(pm re.PageMeta) string {
if pm.Scheduled != nil && !*pm.Scheduled {
query = append(query, "r.time IS NULL")
}
var q string
if len(query) > 0 {
q = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
}
return q
return query
}
+33 -1
View File
@@ -937,17 +937,30 @@ func TestListRules(t *testing.T) {
func TestListUserRules(t *testing.T) {
t.Cleanup(func() {
_, err := db.Exec("DELETE FROM rules")
_, err := db.Exec("DELETE FROM domains_role_actions")
assert.Nil(t, err, fmt.Sprintf("clean domains_role_actions unexpected error: %s", err))
_, err = db.Exec("DELETE FROM domains_role_members")
assert.Nil(t, err, fmt.Sprintf("clean domains_role_members unexpected error: %s", err))
_, err = db.Exec("DELETE FROM domains_roles")
assert.Nil(t, err, fmt.Sprintf("clean domains_roles unexpected error: %s", err))
_, err = db.Exec("DELETE FROM domains")
assert.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err))
_, err = db.Exec("DELETE FROM rules")
assert.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err))
})
repo := postgres.NewRepository(database)
domainID := generateUUID(t)
domainRoute := generateUUID(t)
userID := generateUUID(t)
domainUserID := generateUUID(t)
otherUserID := generateUUID(t)
channelID := generateUUID(t)
_, err := db.Exec(`INSERT INTO domains (id, name, route, status) VALUES ($1, $2, $3, $4)`, domainID, namegen.Generate(), domainRoute, 0)
assert.Nil(t, err, fmt.Sprintf("insert domains unexpected error: %s", err))
// Create 10 rules; assign the first 4 to userID via a role.
var allRules []re.Rule
for i := range 10 {
@@ -977,6 +990,14 @@ func TestListUserRules(t *testing.T) {
assert.Nil(t, err, fmt.Sprintf("insert rules_role_members unexpected error: %s", err))
}
domainRoleID := generateUUID(t)
_, err = db.Exec(`INSERT INTO domains_roles (id, name, entity_id) VALUES ($1, $2, $3)`, domainRoleID, "admin", domainID)
assert.Nil(t, err, fmt.Sprintf("insert domains_roles unexpected error: %s", err))
_, err = db.Exec(`INSERT INTO domains_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, domainRoleID, domainUserID, domainID)
assert.Nil(t, err, fmt.Sprintf("insert domains_role_members unexpected error: %s", err))
_, err = db.Exec(`INSERT INTO domains_role_actions (role_id, action) VALUES ($1, $2)`, domainRoleID, "rule_read")
assert.Nil(t, err, fmt.Sprintf("insert domains_role_actions unexpected error: %s", err))
cases := []struct {
desc string
userID string
@@ -1053,6 +1074,17 @@ func TestListUserRules(t *testing.T) {
count: 0,
err: nil,
},
{
desc: "list user rules via domain role returns all domain rules",
userID: domainUserID,
pm: re.PageMeta{
Offset: 0,
Limit: 100,
Status: re.AllStatus,
},
count: 10,
err: nil,
},
{
desc: "list rules for user with no role assignments returns 0",
userID: otherUserID,
+34 -16
View File
@@ -452,25 +452,38 @@ func (repo *PostgresRepository) ListAllReportsConfig(ctx context.Context, pm rep
}
func (repo *PostgresRepository) ListUserReportsConfig(ctx context.Context, userID string, pm reports.PageMeta) (reports.ReportConfigPage, error) {
pq := pageReportQuery(pm)
clauses := []string{
`(
EXISTS (
SELECT 1
FROM reports_roles rr
JOIN reports_role_members rrm ON rrm.role_id = rr.id
WHERE rr.entity_id = rc.id AND rrm.member_id = :user_id
)
OR EXISTS (
SELECT 1
FROM domains_roles dr
JOIN domains_role_members drm ON drm.role_id = dr.id
JOIN domains_role_actions dra ON dra.role_id = dr.id
WHERE dr.entity_id = rc.domain_id
AND drm.member_id = :user_id
AND dra.action LIKE 'report%'
)
)`,
}
clauses = append(clauses, pageReportQueryConditions(pm)...)
orderClause := reportsOrderClause(pm)
pgData := reportsPageData(pm)
pm.UserID = userID
userJoin := `
INNER JOIN reports_roles rr ON rr.entity_id = rc.id
INNER JOIN reports_role_members rrm ON rrm.role_id = rr.id AND rrm.member_id = :user_id
`
whereClause := pq
whereClause := fmt.Sprintf("WHERE %s", strings.Join(clauses, " AND "))
innerQ := fmt.Sprintf(`
SELECT DISTINCT rc.id, rc.name, rc.description, rc.domain_id, rc.metrics, rc.email, rc.config,
rc.start_datetime, rc.due, rc.recurring, rc.recurring_period, rc.created_at, rc.created_by, rc.updated_at, rc.updated_by, rc.status
FROM report_config rc
%s
%s
`, userJoin, whereClause)
`, whereClause)
q := fmt.Sprintf(`
SELECT * FROM (%s) AS sub %s %s;
@@ -637,6 +650,17 @@ func reportsPageData(pm reports.PageMeta) string {
}
func pageReportQuery(pm reports.PageMeta) string {
query := pageReportQueryConditions(pm)
var q string
if len(query) > 0 {
q = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
}
return q
}
func pageReportQueryConditions(pm reports.PageMeta) []string {
var query []string
if pm.Status != reports.AllStatus {
query = append(query, "rc.status = :status")
@@ -653,11 +677,5 @@ func pageReportQuery(pm reports.PageMeta) string {
if pm.Name != "" {
query = append(query, "rc.name ILIKE '%' || :name || '%'")
}
var q string
if len(query) > 0 {
q = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
}
return q
return query
}
+33 -1
View File
@@ -485,16 +485,29 @@ func TestListReportsConfig(t *testing.T) {
func TestListUserReportsConfig(t *testing.T) {
t.Cleanup(func() {
_, err := db.Exec("DELETE FROM report_config")
_, err := db.Exec("DELETE FROM domains_role_actions")
require.Nil(t, err, fmt.Sprintf("clean domains_role_actions unexpected error: %s", err))
_, err = db.Exec("DELETE FROM domains_role_members")
require.Nil(t, err, fmt.Sprintf("clean domains_role_members unexpected error: %s", err))
_, err = db.Exec("DELETE FROM domains_roles")
require.Nil(t, err, fmt.Sprintf("clean domains_roles unexpected error: %s", err))
_, err = db.Exec("DELETE FROM domains")
require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err))
_, err = db.Exec("DELETE FROM report_config")
require.Nil(t, err, fmt.Sprintf("clean report_config unexpected error: %s", err))
})
repo := postgres.NewRepository(database)
domainID := generateUUID(t)
domainRoute := generateUUID(t)
userID := generateUUID(t)
domainUserID := generateUUID(t)
otherUserID := generateUUID(t)
_, err := db.Exec(`INSERT INTO domains (id, name, route, status) VALUES ($1, $2, $3, $4)`, domainID, namegen.Generate(), domainRoute, 0)
require.Nil(t, err, fmt.Sprintf("insert domains unexpected error: %s", err))
num := 10
var allCfgs []reports.ReportConfig
for i := range num {
@@ -521,6 +534,14 @@ func TestListUserReportsConfig(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("insert reports_role_members unexpected error: %s", err))
}
domainRoleID := generateUUID(t)
_, err = db.Exec(`INSERT INTO domains_roles (id, name, entity_id) VALUES ($1, $2, $3)`, domainRoleID, "admin", domainID)
require.Nil(t, err, fmt.Sprintf("insert domains_roles unexpected error: %s", err))
_, err = db.Exec(`INSERT INTO domains_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, domainRoleID, domainUserID, domainID)
require.Nil(t, err, fmt.Sprintf("insert domains_role_members unexpected error: %s", err))
_, err = db.Exec(`INSERT INTO domains_role_actions (role_id, action) VALUES ($1, $2)`, domainRoleID, "report_read")
require.Nil(t, err, fmt.Sprintf("insert domains_role_actions unexpected error: %s", err))
cases := []struct {
desc string
userID string
@@ -583,6 +604,17 @@ func TestListUserReportsConfig(t *testing.T) {
size: 0,
err: nil,
},
{
desc: "list user reports via domain role returns all domain reports",
userID: domainUserID,
pageMeta: reports.PageMeta{
Domain: domainID,
Limit: 100,
Offset: 0,
},
size: 10,
err: nil,
},
{
desc: "list user reports with non-existing domain returns 0",
userID: userID,