package acl import ( "encoding/json" "errors" "net/http" "github.com/go-chi/chi/v5" "github.com/google/uuid" "aether/internal/audit" "aether/internal/auth" "aether/pkg/middleware" "aether/pkg/response" ) // Handler bedient den Freigabedialog. Die Zugriffsprüfung selbst passiert im // files-Paket; hier geht es nur ums Erteilen, Ansehen und Widerrufen. // // Freigeben darf ausschließlich der Eigentümer. Wer selbst nur eine // Schreibfreigabe hat, kann sie nicht weiterreichen – sonst verlöre der // Eigentümer die Kontrolle darüber, wer seine Daten sieht. type Handler struct { store *Store auditor *audit.Logger } func NewHandler(store *Store, auditor *audit.Logger) *Handler { return &Handler{store: store, auditor: auditor} } // logShare hält fest, wer wem Zugriff auf was gegeben oder genommen hat. Bei // Freigaben ist das die interessanteste Spur überhaupt: Sie erklärt, wie jemand // an Daten kam, die ihm nicht gehören. func (h *Handler) logShare(r *http.Request, action, resourceType, resourceID, detail string) { user := auth.UserFromContext(r.Context()) e := audit.Event{ Action: action, TargetType: resourceType, TargetID: resourceID, Detail: detail, IP: middleware.RealIP(r), } if user != nil { e.ActorID = &user.ID e.ActorName = user.Username } h.auditor.Log(r.Context(), e) } // ownedFolder / ownedFile liefern die Ressourcen-ID und den Anfragenden – // aber nur, wenn ihm die Ressource auch gehört. Alles andere ist bereits // beantwortet. func (h *Handler) ownedFolder(w http.ResponseWriter, r *http.Request) (uuid.UUID, uuid.UUID, bool) { user := auth.UserFromContext(r.Context()) id, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { response.Error(w, http.StatusBadRequest, "invalid folder id") return uuid.Nil, uuid.Nil, false } a, err := h.store.ResolveFolder(r.Context(), user.ID, id) if err != nil { response.Error(w, http.StatusNotFound, "folder not found") return uuid.Nil, uuid.Nil, false } if a.OwnerID != user.ID { response.Error(w, http.StatusForbidden, "only the owner can manage sharing") return uuid.Nil, uuid.Nil, false } return id, user.ID, true } func (h *Handler) ownedFile(w http.ResponseWriter, r *http.Request) (uuid.UUID, uuid.UUID, bool) { user := auth.UserFromContext(r.Context()) id, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { response.Error(w, http.StatusBadRequest, "invalid file id") return uuid.Nil, uuid.Nil, false } a, err := h.store.ResolveFile(r.Context(), user.ID, id) if err != nil { response.Error(w, http.StatusNotFound, "file not found") return uuid.Nil, uuid.Nil, false } if a.OwnerID != user.ID { response.Error(w, http.StatusForbidden, "only the owner can manage sharing") return uuid.Nil, uuid.Nil, false } return id, user.ID, true } func userFrom(r *http.Request) uuid.UUID { if u := auth.UserFromContext(r.Context()); u != nil { return u.ID } return uuid.Nil } type grantRequest struct { SubjectID string `json:"subject_id"` SubjectType string `json:"subject_type"` // "user" | "group" Permission Permission `json:"permission"` } // GET /api/v1/folders/{id}/acl func (h *Handler) ListFolderACL(w http.ResponseWriter, r *http.Request) { id, _, ok := h.ownedFolder(w, r) if !ok { return } entries, err := h.store.ListForFolder(r.Context(), id) if err != nil { response.Error(w, http.StatusInternalServerError, "could not load shares") return } response.JSON(w, http.StatusOK, entries) } // POST /api/v1/folders/{id}/acl func (h *Handler) GrantFolderACL(w http.ResponseWriter, r *http.Request) { id, owner, ok := h.ownedFolder(w, r) if !ok { return } req, ok := decodeGrant(w, r) if !ok { return } entry, err := h.store.GrantFolder(r.Context(), id, req.subjectID, req.isGroup, req.perm, owner) if err == nil { h.logShare(r, audit.ActionShareGrant, "folder", id.String(), string(req.perm)+" for "+req.subjectType()+" "+req.subjectID.String()) } writeGrantResult(w, entry, err) } // GET /api/v1/files/{id}/acl func (h *Handler) ListFileACL(w http.ResponseWriter, r *http.Request) { id, _, ok := h.ownedFile(w, r) if !ok { return } entries, err := h.store.ListForFile(r.Context(), id) if err != nil { response.Error(w, http.StatusInternalServerError, "could not load shares") return } response.JSON(w, http.StatusOK, entries) } // POST /api/v1/files/{id}/acl func (h *Handler) GrantFileACL(w http.ResponseWriter, r *http.Request) { id, owner, ok := h.ownedFile(w, r) if !ok { return } req, ok := decodeGrant(w, r) if !ok { return } entry, err := h.store.GrantFile(r.Context(), id, req.subjectID, req.isGroup, req.perm, owner) if err == nil { h.logShare(r, audit.ActionShareGrant, "file", id.String(), string(req.perm)+" for "+req.subjectType()+" "+req.subjectID.String()) } writeGrantResult(w, entry, err) } // DELETE /api/v1/acl/{id} func (h *Handler) RevokeACL(w http.ResponseWriter, r *http.Request) { user := userFrom(r) if user == uuid.Nil { response.Error(w, http.StatusUnauthorized, "unauthorized") return } entryID, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { response.Error(w, http.StatusBadRequest, "invalid id") return } if err := h.store.Revoke(r.Context(), entryID, user); errors.Is(err, ErrNotFound) { response.Error(w, http.StatusNotFound, "share not found") return } else if err != nil { response.Error(w, http.StatusInternalServerError, "could not revoke share") return } h.logShare(r, audit.ActionShareRevoke, "acl", entryID.String(), "") w.WriteHeader(http.StatusNoContent) } // GET /api/v1/shared-with-me func (h *Handler) SharedWithMe(w http.ResponseWriter, r *http.Request) { user := userFrom(r) if user == uuid.Nil { response.Error(w, http.StatusUnauthorized, "unauthorized") return } items, err := h.store.ListSharedWith(r.Context(), user) if err != nil { response.Error(w, http.StatusInternalServerError, "could not load shared items") return } response.JSON(w, http.StatusOK, items) } // GET /api/v1/share-recipients func (h *Handler) Recipients(w http.ResponseWriter, r *http.Request) { user := userFrom(r) if user == uuid.Nil { response.Error(w, http.StatusUnauthorized, "unauthorized") return } list, err := h.store.Recipients(r.Context(), user) if err != nil { response.Error(w, http.StatusInternalServerError, "could not load recipients") return } response.JSON(w, http.StatusOK, list) } // ── Hilfen ─────────────────────────────────────────────────────────────────── type parsedGrant struct { subjectID uuid.UUID isGroup bool perm Permission } func (g parsedGrant) subjectType() string { return subjectTypeName(g.isGroup) } func decodeGrant(w http.ResponseWriter, r *http.Request) (parsedGrant, bool) { var body grantRequest if err := json.NewDecoder(r.Body).Decode(&body); err != nil { response.Error(w, http.StatusBadRequest, "invalid request body") return parsedGrant{}, false } id, err := uuid.Parse(body.SubjectID) if err != nil { response.Error(w, http.StatusBadRequest, "invalid subject_id") return parsedGrant{}, false } if body.SubjectType != "user" && body.SubjectType != "group" { response.Error(w, http.StatusBadRequest, `subject_type must be "user" or "group"`) return parsedGrant{}, false } if !body.Permission.valid() { response.Error(w, http.StatusBadRequest, `permission must be "read" or "write"`) return parsedGrant{}, false } return parsedGrant{subjectID: id, isGroup: body.SubjectType == "group", perm: body.Permission}, true } func writeGrantResult(w http.ResponseWriter, entry *Entry, err error) { switch { case errors.Is(err, ErrSelfShare): response.Error(w, http.StatusBadRequest, "you already have access to this") case err != nil: response.Error(w, http.StatusInternalServerError, "could not share") default: response.JSON(w, http.StatusCreated, entry) } }