Compare commits

..

1 Commits

Author SHA1 Message Date
Grendgi
d0f8b48869 feat: add monitoring tg view-all scope
All checks were successful
CI / hygiene (push) Successful in 1s
Build and Deploy / build-and-deploy (push) Successful in 28s
CI / go (push) Successful in 21s
CI / python (push) Successful in 1s
2026-06-19 14:26:51 +03:00

View File

@@ -73,7 +73,9 @@ type app struct {
type accessScope struct { type accessScope struct {
IsAdmin bool IsAdmin bool
CanManage bool CanManage bool
CanManageAll bool
CanAuth bool CanAuth bool
CanViewAll bool
DeptID string DeptID string
DeptIDs []string DeptIDs []string
} }
@@ -638,7 +640,9 @@ func (a *app) handleAccessMe(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"is_admin": scope.IsAdmin, "is_admin": scope.IsAdmin,
"can_manage_department": scope.CanManage, "can_manage_department": scope.CanManage,
"can_manage_all": scope.CanManageAll,
"can_auth_telegram": scope.CanAuth, "can_auth_telegram": scope.CanAuth,
"can_view_all": scope.CanViewAll,
"department_id": nullableString(scope.DeptID), "department_id": nullableString(scope.DeptID),
"department_ids": scope.departmentIDs(), "department_ids": scope.departmentIDs(),
}) })
@@ -667,7 +671,7 @@ func (a *app) listSections(ctx context.Context, w http.ResponseWriter, r *http.R
args := []any{vertical} args := []any{vertical}
deptFilter := "" deptFilter := ""
if !scope.IsAdmin { if !scope.canReadAll() {
args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id") args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id")
} }
@@ -731,6 +735,7 @@ func (a *app) createSection(ctx context.Context, w http.ResponseWriter, r *http.
} }
var payload struct { var payload struct {
Vertical string `json:"vertical"` Vertical string `json:"vertical"`
DepartmentID *string `json:"department_id"`
Slug string `json:"slug"` Slug string `json:"slug"`
Title string `json:"title"` Title string `json:"title"`
Emoji *string `json:"emoji"` Emoji *string `json:"emoji"`
@@ -746,7 +751,11 @@ func (a *app) createSection(ctx context.Context, w http.ResponseWriter, r *http.
writeError(w, http.StatusBadRequest, "vertical, slug and title are required") writeError(w, http.StatusBadRequest, "vertical, slug and title are required")
return return
} }
dept := nullableString(scope.primaryDepartmentID()) deptID := scope.primaryDepartmentID()
if scope.CanManageAll && payload.DepartmentID != nil {
deptID = strings.TrimSpace(*payload.DepartmentID)
}
dept := nullableString(deptID)
row := a.db.QueryRow(ctx, ` row := a.db.QueryRow(ctx, `
INSERT INTO sections (vertical, department_id, slug, title, emoji, description) INSERT INTO sections (vertical, department_id, slug, title, emoji, description)
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6)
@@ -837,7 +846,7 @@ func (a *app) updateSection(ctx context.Context, w http.ResponseWriter, r *http.
} }
args = append(args, vertical, slug) args = append(args, vertical, slug)
where := fmt.Sprintf("vertical = $%d AND slug = $%d", len(args)-1, len(args)) where := fmt.Sprintf("vertical = $%d AND slug = $%d", len(args)-1, len(args))
if !scope.IsAdmin { if !scope.CanManageAll {
var deptFilter string var deptFilter string
args, deptFilter = appendDepartmentFilter(args, scope, "department_id") args, deptFilter = appendDepartmentFilter(args, scope, "department_id")
where += deptFilter where += deptFilter
@@ -865,7 +874,7 @@ func (a *app) deleteSection(ctx context.Context, w http.ResponseWriter, r *http.
if !ok { if !ok {
return return
} }
section, err := a.findSection(ctx, vertical, slug, scope) section, err := a.findSection(ctx, vertical, slug, scope.forManageLookup())
if err != nil { if err != nil {
writeDBError(w, err) writeDBError(w, err)
return return
@@ -889,7 +898,7 @@ func (a *app) deleteSection(ctx context.Context, w http.ResponseWriter, r *http.
func (a *app) findSection(ctx context.Context, vertical, slug string, scope accessScope) (sectionOut, error) { func (a *app) findSection(ctx context.Context, vertical, slug string, scope accessScope) (sectionOut, error) {
args := []any{vertical, slug} args := []any{vertical, slug}
where := "s.vertical = $1 AND s.slug = $2" where := "s.vertical = $1 AND s.slug = $2"
if !scope.IsAdmin { if !scope.canReadAll() {
var deptFilter string var deptFilter string
args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id") args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id")
where += deptFilter where += deptFilter
@@ -933,7 +942,7 @@ func (a *app) listChannels(ctx context.Context, w http.ResponseWriter, r *http.R
args = append(args, section) args = append(args, section)
where += fmt.Sprintf(" AND s.slug = $%d", len(args)) where += fmt.Sprintf(" AND s.slug = $%d", len(args))
} }
if !scope.IsAdmin { if !scope.canReadAll() {
var deptFilter string var deptFilter string
args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id") args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id")
where += deptFilter where += deptFilter
@@ -990,7 +999,7 @@ func (a *app) createChannel(ctx context.Context, w http.ResponseWriter, r *http.
writeError(w, http.StatusBadRequest, "identifier, vertical and section are required") writeError(w, http.StatusBadRequest, "identifier, vertical and section are required")
return return
} }
section, err := a.findSection(ctx, payload.Vertical, payload.Section, scope) section, err := a.findSection(ctx, payload.Vertical, payload.Section, scope.forManageLookup())
if err != nil { if err != nil {
writeDBError(w, err) writeDBError(w, err)
return return
@@ -1083,7 +1092,7 @@ func (a *app) updateChannel(ctx context.Context, w http.ResponseWriter, r *http.
if !ok { if !ok {
return return
} }
if _, err := a.findChannel(ctx, id, scope, r.URL.Query().Get("vertical"), r.URL.Query().Get("section")); err != nil { if _, err := a.findChannel(ctx, id, scope.forManageLookup(), r.URL.Query().Get("vertical"), r.URL.Query().Get("section")); err != nil {
writeDBError(w, err) writeDBError(w, err)
return return
} }
@@ -1110,7 +1119,7 @@ func (a *app) updateChannel(ctx context.Context, w http.ResponseWriter, r *http.
if payload.Vertical != nil && strings.TrimSpace(*payload.Vertical) != "" { if payload.Vertical != nil && strings.TrimSpace(*payload.Vertical) != "" {
vertical = strings.TrimSpace(*payload.Vertical) vertical = strings.TrimSpace(*payload.Vertical)
} }
section, err := a.findSection(ctx, vertical, strings.TrimSpace(*payload.Section), scope) section, err := a.findSection(ctx, vertical, strings.TrimSpace(*payload.Section), scope.forManageLookup())
if err != nil { if err != nil {
writeDBError(w, err) writeDBError(w, err)
return return
@@ -1144,7 +1153,7 @@ func (a *app) deleteChannel(ctx context.Context, w http.ResponseWriter, r *http.
if !ok { if !ok {
return return
} }
if _, err := a.findChannel(ctx, id, scope, r.URL.Query().Get("vertical"), r.URL.Query().Get("section")); err != nil { if _, err := a.findChannel(ctx, id, scope.forManageLookup(), r.URL.Query().Get("vertical"), r.URL.Query().Get("section")); err != nil {
writeDBError(w, err) writeDBError(w, err)
return return
} }
@@ -1166,7 +1175,7 @@ func (a *app) findChannel(ctx context.Context, id int64, scope accessScope, vert
args = append(args, strings.TrimSpace(section)) args = append(args, strings.TrimSpace(section))
where += fmt.Sprintf(" AND s.slug = $%d", len(args)) where += fmt.Sprintf(" AND s.slug = $%d", len(args))
} }
if !scope.IsAdmin { if !scope.canReadAll() {
var deptFilter string var deptFilter string
args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id") args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id")
where += deptFilter where += deptFilter
@@ -1234,7 +1243,7 @@ func (a *app) reanalyzeChannel(ctx context.Context, w http.ResponseWriter, r *ht
if !ok { if !ok {
return return
} }
ch, err := a.findChannel(ctx, id, scope, r.URL.Query().Get("vertical"), r.URL.Query().Get("section")) ch, err := a.findChannel(ctx, id, scope.forManageLookup(), r.URL.Query().Get("vertical"), r.URL.Query().Get("section"))
if err != nil { if err != nil {
writeDBError(w, err) writeDBError(w, err)
return return
@@ -1316,7 +1325,7 @@ func (a *app) handleMessages(ctx context.Context, w http.ResponseWriter, r *http
args = append(args, key, field) args = append(args, key, field)
where += fmt.Sprintf(" AND COALESCE(mc.verdict ->> $%d, m.extracted -> $%d ->> $%d) = 'true'", len(args), len(args)-1, len(args)) where += fmt.Sprintf(" AND COALESCE(mc.verdict ->> $%d, m.extracted -> $%d ->> $%d) = 'true'", len(args), len(args)-1, len(args))
} }
if !scope.IsAdmin { if !scope.canReadAll() {
var deptFilter string var deptFilter string
args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id") args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id")
where += deptFilter where += deptFilter
@@ -1385,7 +1394,7 @@ func (a *app) handleMessageItem(ctx context.Context, w http.ResponseWriter, r *h
} }
args := []any{id} args := []any{id}
where := "m.id = $1" where := "m.id = $1"
if !scope.IsAdmin { if !scope.canReadAll() {
var deptFilter string var deptFilter string
args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id") args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id")
where += deptFilter where += deptFilter
@@ -1508,7 +1517,7 @@ func (a *app) canReadChannelMedia(ctx context.Context, scope accessScope, channe
FROM channels c FROM channels c
JOIN sections s ON s.id = c.section_id JOIN sections s ON s.id = c.section_id
WHERE c.id = $1 OR c.source_channel_id = $1 WHERE c.id = $1 OR c.source_channel_id = $1
`, channelID, scope.departmentIDs(), scope.IsAdmin).Scan(&allowed) `, channelID, scope.departmentIDs(), scope.canReadAll()).Scan(&allowed)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return false, nil return false, nil
} }
@@ -1538,7 +1547,7 @@ func (a *app) handleStats(ctx context.Context, w http.ResponseWriter, r *http.Re
args = append(args, section) args = append(args, section)
where += fmt.Sprintf(" AND s.slug = $%d", len(args)) where += fmt.Sprintf(" AND s.slug = $%d", len(args))
} }
if !scope.IsAdmin { if !scope.canReadAll() {
var deptFilter string var deptFilter string
args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id") args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id")
where += deptFilter where += deptFilter
@@ -1657,7 +1666,7 @@ func (a *app) pendingLLM(ctx context.Context, scope accessScope, vertical, secti
args = append(args, section) args = append(args, section)
where += fmt.Sprintf(" AND s.slug = $%d", len(args)) where += fmt.Sprintf(" AND s.slug = $%d", len(args))
} }
if !scope.IsAdmin { if !scope.canReadAll() {
var deptFilter string var deptFilter string
args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id") args, deptFilter = appendDepartmentFilter(args, scope, "s.department_id")
where += deptFilter where += deptFilter
@@ -1748,7 +1757,7 @@ func (a *app) savePrompt(ctx context.Context, w http.ResponseWriter, r *http.Req
writeError(w, http.StatusBadRequest, "prompt is too long (max 30000 chars)") writeError(w, http.StatusBadRequest, "prompt is too long (max 30000 chars)")
return return
} }
deptID, err := a.promptDepartmentID(ctx, scope, vertical, section) deptID, err := a.promptDepartmentID(ctx, scope.forManageLookup(), vertical, section)
if err != nil { if err != nil {
writeDBError(w, err) writeDBError(w, err)
return return
@@ -1776,7 +1785,7 @@ func (a *app) resetPrompt(ctx context.Context, w http.ResponseWriter, r *http.Re
return return
} }
section := strings.TrimSpace(r.URL.Query().Get("section")) section := strings.TrimSpace(r.URL.Query().Get("section"))
deptID, err := a.promptDepartmentID(ctx, scope, vertical, section) deptID, err := a.promptDepartmentID(ctx, scope.forManageLookup(), vertical, section)
if err != nil { if err != nil {
writeDBError(w, err) writeDBError(w, err)
return return
@@ -1881,11 +1890,11 @@ func (a *app) readScope(w http.ResponseWriter, r *http.Request, manage bool) (ac
writeError(w, http.StatusNotFound, "not found") writeError(w, http.StatusNotFound, "not found")
return scope, false return scope, false
} }
} else if !scope.IsAdmin && len(scope.departmentIDs()) == 0 { } else if !scope.canReadAll() && len(scope.departmentIDs()) == 0 {
writeError(w, http.StatusForbidden, "department is required") writeError(w, http.StatusForbidden, "department is required")
return scope, false return scope, false
} }
if manage && !scope.IsAdmin && len(scope.departmentIDs()) == 0 { if manage && !scope.CanManageAll && len(scope.departmentIDs()) == 0 {
writeError(w, http.StatusForbidden, "department is required") writeError(w, http.StatusForbidden, "department is required")
return scope, false return scope, false
} }
@@ -1895,8 +1904,9 @@ func (a *app) readScope(w http.ResponseWriter, r *http.Request, manage bool) (ac
func readAccess(r *http.Request) accessScope { func readAccess(r *http.Request) accessScope {
admin := commonmw.HeaderBool(r, "X-User-Is-Admin") admin := commonmw.HeaderBool(r, "X-User-Is-Admin")
deptHead := commonmw.HeaderBool(r, "X-User-Is-Department-Head") deptHead := commonmw.HeaderBool(r, "X-User-Is-Department-Head")
canManage := commonmw.HeaderBool(r, "X-Monitoring-TG-Can-Manage") canManagePermission := commonmw.HeaderBool(r, "X-Monitoring-TG-Can-Manage")
canAuth := commonmw.HeaderBool(r, "X-Monitoring-TG-Can-Auth") canAuth := commonmw.HeaderBool(r, "X-Monitoring-TG-Can-Auth")
canViewAll := commonmw.HeaderBool(r, "X-Monitoring-TG-Can-View-All")
deptID := strings.TrimSpace(r.Header.Get("X-User-Department-Id")) deptID := strings.TrimSpace(r.Header.Get("X-User-Department-Id"))
deptIDs := commonmw.HeaderCSV(r, "X-User-Department-Ids") deptIDs := commonmw.HeaderCSV(r, "X-User-Department-Ids")
if deptID != "" { if deptID != "" {
@@ -1904,13 +1914,27 @@ func readAccess(r *http.Request) accessScope {
} }
return accessScope{ return accessScope{
IsAdmin: admin, IsAdmin: admin,
CanManage: admin || deptHead || canManage, CanManage: admin || deptHead || canManagePermission,
CanManageAll: admin || (canManagePermission && canViewAll),
CanAuth: admin || canAuth, CanAuth: admin || canAuth,
CanViewAll: admin || canViewAll,
DeptID: deptID, DeptID: deptID,
DeptIDs: deptIDs, DeptIDs: deptIDs,
} }
} }
func (s accessScope) canReadAll() bool {
return s.IsAdmin || s.CanViewAll
}
func (s accessScope) forManageLookup() accessScope {
if s.CanManageAll {
return s
}
s.CanViewAll = false
return s
}
func appendUniqueString(items []string, value string) []string { func appendUniqueString(items []string, value string) []string {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { if value == "" {