258 lines
9.0 KiB
Go
258 lines
9.0 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/audit"
|
|
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/auth"
|
|
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/notification"
|
|
)
|
|
|
|
func (s *server) requireRecentAdmin(w http.ResponseWriter, r *http.Request, api bool) (auth.User, bool) {
|
|
var user auth.User
|
|
var ok bool
|
|
if api {
|
|
user, ok = s.requireAPIUser(w, r, true)
|
|
} else {
|
|
var err error
|
|
user, err = s.currentUser(r)
|
|
ok = err == nil && user.Role == "admin"
|
|
if ok {
|
|
session, e := r.Cookie(sessionCookie)
|
|
ok = e == nil && s.parseForm(w, r) && s.auth.ValidateCSRF(r.Context(), session.Value, r.FormValue("csrf_token"))
|
|
}
|
|
}
|
|
if !ok {
|
|
return auth.User{}, false
|
|
}
|
|
if time.Since(user.AuthenticatedAt) > 10*time.Minute {
|
|
if api {
|
|
s.apiProblem(w, http.StatusForbidden, "reauthentication_required", "Recent authentication is required.")
|
|
} else {
|
|
s.problem(w, http.StatusForbidden, "Recent authentication is required.")
|
|
}
|
|
return auth.User{}, false
|
|
}
|
|
return user, true
|
|
}
|
|
func (s *server) recordAudit(r *http.Request, actor auth.User, action, outcome string, summary map[string]string) {
|
|
if s.audit == nil {
|
|
return
|
|
}
|
|
if err := s.audit.Record(r.Context(), audit.Event{ActorID: actor.ID, ActorLabel: actor.Username, Action: action, Outcome: outcome, Summary: summary}); err != nil {
|
|
s.logger.Warn("audit recording failed", "event", "audit.record.failed")
|
|
}
|
|
}
|
|
|
|
func (s *server) auditList(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := s.requireAPIUser(w, r, true); !ok {
|
|
return
|
|
}
|
|
f := audit.Filter{ActorID: r.URL.Query().Get("actor_id"), InstanceID: r.URL.Query().Get("instance_id"), Action: r.URL.Query().Get("action"), Outcome: r.URL.Query().Get("outcome")}
|
|
events, err := s.audit.List(r.Context(), f)
|
|
if err != nil {
|
|
s.apiProblem(w, 500, "audit_unavailable", "Audit events are unavailable.")
|
|
return
|
|
}
|
|
s.apiJSON(w, 200, map[string]any{"events": events})
|
|
}
|
|
func (s *server) auditPolicyGet(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := s.requireAPIUser(w, r, true); !ok {
|
|
return
|
|
}
|
|
p, err := s.audit.Policy(r.Context())
|
|
if err != nil {
|
|
s.apiProblem(w, 500, "audit_unavailable", "Audit policy is unavailable.")
|
|
return
|
|
}
|
|
s.apiJSON(w, 200, p)
|
|
}
|
|
func (s *server) auditPolicyPut(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireRecentAdmin(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
var p audit.Policy
|
|
if !s.decodeAPIJSON(w, r, &p) {
|
|
return
|
|
}
|
|
if err := s.audit.SetPolicy(r.Context(), p); err != nil {
|
|
s.recordAudit(r, actor, "audit.policy.update", "failed", map[string]string{"reason_code": "invalid_policy"})
|
|
s.apiProblem(w, 422, "invalid_policy", "The audit policy is invalid.")
|
|
return
|
|
}
|
|
s.recordAudit(r, actor, "audit.policy.update", "allowed", nil)
|
|
s.apiJSON(w, 200, p)
|
|
}
|
|
func (s *server) auditPurge(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireRecentAdmin(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
var in struct {
|
|
Before string `json:"before"`
|
|
Confirm bool `json:"confirm"`
|
|
}
|
|
if !s.decodeAPIJSON(w, r, &in) || !in.Confirm {
|
|
s.apiProblem(w, 422, "confirmation_required", "Purge confirmation is required.")
|
|
return
|
|
}
|
|
before, err := time.Parse(time.RFC3339, in.Before)
|
|
if err != nil {
|
|
s.apiProblem(w, 422, "invalid_boundary", "The purge boundary is invalid.")
|
|
return
|
|
}
|
|
n, err := s.audit.Purge(r.Context(), before)
|
|
if err != nil {
|
|
s.apiProblem(w, 422, "purge_failed", "The audit purge failed.")
|
|
return
|
|
}
|
|
s.recordAudit(r, actor, "audit.purge", "allowed", map[string]string{"deleted_count": strconv.FormatInt(n, 10)})
|
|
s.apiJSON(w, 200, map[string]int64{"deleted_count": n})
|
|
}
|
|
|
|
func (s *server) notificationList(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := s.requireAPIUser(w, r, true); !ok {
|
|
return
|
|
}
|
|
values, err := s.notifications.List(r.Context())
|
|
if err != nil {
|
|
s.apiProblem(w, 500, "channels_unavailable", "Notification channels are unavailable.")
|
|
return
|
|
}
|
|
s.apiJSON(w, 200, map[string]any{"channels": values})
|
|
}
|
|
func (s *server) notificationCreate(w http.ResponseWriter, r *http.Request) {
|
|
s.notificationUpsert(w, r, "")
|
|
}
|
|
func (s *server) notificationUpdate(w http.ResponseWriter, r *http.Request) {
|
|
s.notificationUpsert(w, r, r.PathValue("id"))
|
|
}
|
|
func (s *server) notificationUpsert(w http.ResponseWriter, r *http.Request, id string) {
|
|
actor, ok := s.requireRecentAdmin(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
var in notification.Input
|
|
if !s.decodeAPIJSON(w, r, &in) {
|
|
return
|
|
}
|
|
value, err := s.notifications.Upsert(r.Context(), id, in)
|
|
if err != nil {
|
|
s.recordAudit(r, actor, "notification.channel.update", "failed", map[string]string{"channel_type": in.Type, "reason_code": "invalid_channel"})
|
|
s.apiProblem(w, 422, "invalid_channel", "The notification channel is invalid.")
|
|
return
|
|
}
|
|
s.recordAudit(r, actor, "notification.channel.update", "allowed", map[string]string{"target_id": value.ID, "channel_type": value.Type})
|
|
status := http.StatusOK
|
|
if id == "" {
|
|
status = http.StatusCreated
|
|
}
|
|
s.apiJSON(w, status, value)
|
|
}
|
|
func (s *server) notificationDelete(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireRecentAdmin(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := s.notifications.Delete(r.Context(), r.PathValue("id")); err != nil {
|
|
s.apiProblem(w, 500, "delete_failed", "The channel could not be deleted.")
|
|
return
|
|
}
|
|
s.recordAudit(r, actor, "notification.channel.delete", "allowed", map[string]string{"target_id": r.PathValue("id")})
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
func (s *server) notificationTest(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireRecentAdmin(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
if !s.requireEmptyBody(w, r) {
|
|
return
|
|
}
|
|
if err := s.notifications.Test(r.Context(), r.PathValue("id")); err != nil {
|
|
s.apiProblem(w, 404, "channel_not_found", "The channel was not found.")
|
|
return
|
|
}
|
|
s.recordAudit(r, actor, "notification.channel.test", "allowed", map[string]string{"target_id": r.PathValue("id"), "event_type": "notification.test"})
|
|
s.apiJSON(w, 202, map[string]string{"status": "queued"})
|
|
}
|
|
|
|
func (s *server) notificationForm(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireRecentAdmin(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
config := map[string]string{"url": r.FormValue("url"), "signing_secret": r.FormValue("signing_secret"), "host": r.FormValue("host"), "port": r.FormValue("port"), "username": r.FormValue("smtp_username"), "password": r.FormValue("smtp_password"), "from": r.FormValue("from"), "to": r.FormValue("to")}
|
|
value, err := s.notifications.Upsert(r.Context(), "", notification.Input{Name: r.FormValue("name"), Type: r.FormValue("type"), Enabled: true, Events: strings.Fields(r.FormValue("events")), Config: config})
|
|
if err != nil {
|
|
s.problem(w, 422, "Invalid notification channel.")
|
|
return
|
|
}
|
|
s.recordAudit(r, actor, "notification.channel.update", "allowed", map[string]string{"target_id": value.ID, "channel_type": value.Type})
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
func (s *server) notificationTestForm(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireRecentAdmin(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := s.notifications.Test(r.Context(), r.PathValue("id")); err != nil {
|
|
s.problem(w, 404, "Notification channel not found.")
|
|
return
|
|
}
|
|
s.recordAudit(r, actor, "notification.channel.test", "allowed", map[string]string{"target_id": r.PathValue("id"), "event_type": "notification.test"})
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
func (s *server) notificationDeleteForm(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireRecentAdmin(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := s.notifications.Delete(r.Context(), r.PathValue("id")); err != nil {
|
|
s.problem(w, 500, "Notification channel could not be deleted.")
|
|
return
|
|
}
|
|
s.recordAudit(r, actor, "notification.channel.delete", "allowed", map[string]string{"target_id": r.PathValue("id")})
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
func (s *server) auditPolicyForm(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireRecentAdmin(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
days, _ := strconv.Atoi(r.FormValue("retention_days"))
|
|
maximum, _ := strconv.Atoi(r.FormValue("maximum_count"))
|
|
if err := s.audit.SetPolicy(r.Context(), audit.Policy{RetentionDays: days, MaximumCount: maximum}); err != nil {
|
|
s.problem(w, 422, "Invalid audit policy.")
|
|
return
|
|
}
|
|
s.recordAudit(r, actor, "audit.policy.update", "allowed", nil)
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
func (s *server) auditPurgeForm(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := s.requireRecentAdmin(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
if r.FormValue("confirm") != "yes" {
|
|
s.problem(w, 422, "Purge confirmation is required.")
|
|
return
|
|
}
|
|
before, err := time.Parse("2006-01-02", r.FormValue("before"))
|
|
if err != nil {
|
|
s.problem(w, 422, "Invalid purge date.")
|
|
return
|
|
}
|
|
n, err := s.audit.Purge(r.Context(), before)
|
|
if err != nil {
|
|
s.problem(w, 422, "Audit purge failed.")
|
|
return
|
|
}
|
|
s.recordAudit(r, actor, "audit.purge", "allowed", map[string]string{"deleted_count": strconv.FormatInt(n, 10)})
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|