feat(calendar): Kalender an Nutzer und Gruppen freigeben
Nicht als eigene Tabelle, wie im Plan vorgesehen: acl_entries beantwortet fuer Ordner und Dateien laengst dieselbe Frage -- wer darf was, direkt oder ueber eine Gruppe. Ein zweites Rechtesystem daneben waere eine zweite Antwort auf dieselbe Frage, mit eigener Gruppenaufloesung, eigenem Widerruf, eigenem Pruefpfad; die Luecke dazwischen faellt erst auf, wenn jemand sieht, was er nicht sehen soll. Migration 25 haengt deshalb nur eine dritte Ressourcenspalte an. Der Gewinn: Freigeben an eine **Gruppe** kostete keine Zeile extra. Drei Entscheidungen, die nicht offensichtlich waren: * Ein fremder Kalender steht immer unter seiner UUID, nie unter dem Pfadsegment des Eigentuemers -- das hat der sich ausgesucht, und zwei Leute duerfen denselben Namen waehlen. Sonst laegen im Kalenderbestand des Empfaengers zwei Kalender auf derselben Adresse. * Alle Wege laufen durch RequireCalendar. CalDAV und Weboberflaeche sind zwei getrennte Zugaenge zu denselben Daten; ein Recht, das nur einer von beiden prueft, ist kein Recht. Im CalDAV-Teil genuegte, resolveCalendar umzustellen -- dort kommt jede Operation vorbei. * Wer ein Recht geliehen hat, darf es nicht weiterverleihen: Umbenennen, Loeschen und Freigeben bleiben auch mit Schreibrecht beim Eigentuemer. Oberflaeche: fremde Kalender tragen Eigentuemer und "(nur lesen)", das Menue zeigt nur, was auch geht, und ein Termin laesst sich gar nicht erst in einen Kalender ziehen, den der Server danach ablehnen wuerde. scripts/kalender-teilen-selbsttest.py, 41 Pruefungen, jede ueber beide Zugaenge. Er beginnt damit, dass der Empfaenger vorher nichts sieht -- ohne diesen Anfang beweist der Rest nichts. Dabei hatte der Test selbst eine Falle: Mit festem Testdatum lief die Terminliste aus ihrem Standardzeitraum, und die Pruefung "beim Eigentuemer ist nichts angekommen" bestand **leer**. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8fdde182b7
commit
5d90c0a2d2
21 files changed
+1031
-60
No files matched your search
@@ -0,0 +1,174 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"aether/internal/acl"
|
||||
)
|
||||
|
||||
// Wer darf was mit einem Kalender.
|
||||
//
|
||||
// **Alle Wege laufen hier zusammen.** CalDAV und die Weboberfläche sind zwei
|
||||
// völlig verschiedene Zugänge zu denselben Daten – ein Recht, das nur einer
|
||||
// von beiden prüft, ist kein Recht. Deshalb gibt es genau eine Antwort auf
|
||||
// „darf dieses Konto diesen Kalender", und beide Zugänge holen sie hier.
|
||||
|
||||
// Access ist ein Kalender samt dem, was der Anfragende damit darf.
|
||||
type Access struct {
|
||||
Calendar
|
||||
Permission acl.Permission `json:"permission"`
|
||||
// Shared: Der Kalender gehört jemand anderem.
|
||||
Shared bool `json:"shared"`
|
||||
// OwnerName steht in der Oberfläche neben dem Namen – ein fremder
|
||||
// Kalender ohne sichtbaren Absender ist ein Rätsel.
|
||||
OwnerName string `json:"owner_name,omitempty"`
|
||||
// ViaGroup nennt die Gruppe, über die der Zugriff kommt.
|
||||
ViaGroup string `json:"via_group,omitempty"`
|
||||
}
|
||||
|
||||
func (a *Access) CanWrite() bool { return a.Permission == acl.PermWrite }
|
||||
|
||||
// Ref ist die Adresse in CalDAV.
|
||||
//
|
||||
// Bei einem **fremden** Kalender immer die UUID, nie das Pfadsegment: Das
|
||||
// Segment hat sich der Eigentümer ausgesucht ("…/arbeit/"), und zwei Leute
|
||||
// dürfen denselben Namen wählen. Unter dem Kalenderbestand des Empfängers
|
||||
// stünden dann zwei Kalender an derselben Adresse – und welcher gemeint ist,
|
||||
// entschiede der Zufall.
|
||||
func (a *Access) Ref() string {
|
||||
if a.Shared {
|
||||
return a.Calendar.ID.String()
|
||||
}
|
||||
return a.Calendar.Ref()
|
||||
}
|
||||
|
||||
// ListAccessible liefert alles, was userID sehen darf: die eigenen Kalender
|
||||
// und die ihm freigegebenen. Eigene zuerst – sie sind die, die man sucht.
|
||||
func (s *Store) ListAccessible(ctx context.Context, userID uuid.UUID) ([]Access, error) {
|
||||
own, err := s.ListCalendars(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]Access, 0, len(own))
|
||||
for i := range own {
|
||||
out = append(out, Access{Calendar: own[i], Permission: acl.PermWrite})
|
||||
}
|
||||
|
||||
if s.rights == nil {
|
||||
return out, nil
|
||||
}
|
||||
geteilt, err := s.rights.SharedCalendars(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list shared calendars: %w", err)
|
||||
}
|
||||
for _, sc := range geteilt {
|
||||
cal, err := s.getCalendarAny(ctx, sc.CalendarID)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
continue // zwischenzeitlich gelöscht
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, Access{
|
||||
Calendar: *cal,
|
||||
Permission: sc.Permission,
|
||||
Shared: true,
|
||||
OwnerName: sc.OwnerName,
|
||||
ViaGroup: sc.ViaGroup,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ResolveRef findet den Kalender hinter einer CalDAV-Adresse – eigener oder
|
||||
// freigegebener.
|
||||
//
|
||||
// Die Reihenfolge ist Absicht: **erst die eigenen.** Sonst könnte ein fremder
|
||||
// Kalender, dessen UUID zufällig wie ein eigenes Pfadsegment aussieht, den
|
||||
// eigenen verdecken.
|
||||
func (s *Store) ResolveRef(ctx context.Context, userID uuid.UUID, ref string) (*Access, error) {
|
||||
cal, err := s.GetCalendarByRef(ctx, userID, ref)
|
||||
if err == nil {
|
||||
return &Access{Calendar: *cal, Permission: acl.PermWrite}, nil
|
||||
}
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ein freigegebener Kalender steht unter seiner UUID. Alles andere kann
|
||||
// hier nichts mehr treffen.
|
||||
id, parseErr := uuid.Parse(ref)
|
||||
if parseErr != nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return s.RequireCalendar(ctx, userID, id, false)
|
||||
}
|
||||
|
||||
// RequireCalendar ist die Prüfung, die jeder schreibende und lesende Zugriff
|
||||
// durchläuft – aus CalDAV wie aus der Weboberfläche.
|
||||
//
|
||||
// Ohne Zugriff kommt **ErrNotFound**, nicht „verboten": Ein 403 verriete, dass
|
||||
// es den Kalender gibt. Dieselbe Regel wie überall sonst in Aether.
|
||||
func (s *Store) RequireCalendar(ctx context.Context, userID, calendarID uuid.UUID, needWrite bool) (*Access, error) {
|
||||
cal, err := s.getCalendarAny(ctx, calendarID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cal.OwnerID == userID {
|
||||
return &Access{Calendar: *cal, Permission: acl.PermWrite}, nil
|
||||
}
|
||||
if s.rights == nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
perm, err := s.rights.ResolveCalendar(ctx, userID, calendarID)
|
||||
if errors.Is(err, acl.ErrNoAccess) || errors.Is(err, acl.ErrNotFound) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if needWrite && perm != acl.PermWrite {
|
||||
// Hier **darf** es ein 403 sein: Dass es den Kalender gibt, weiß der
|
||||
// Anfragende bereits – er darf ihn ja lesen.
|
||||
return nil, ErrReadOnly
|
||||
}
|
||||
|
||||
a := &Access{Calendar: *cal, Permission: perm, Shared: true}
|
||||
// Name des Eigentümers nur nachschlagen, wenn er auch angezeigt wird.
|
||||
if name, err := s.ownerName(ctx, cal.OwnerID); err == nil {
|
||||
a.OwnerName = name
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// ErrReadOnly: Der Kalender ist sichtbar, aber nur lesend freigegeben.
|
||||
var ErrReadOnly = errors.New("calendar: read-only share")
|
||||
|
||||
// getCalendarAny lädt einen Kalender **ohne** Rechteprüfung. Privat, und das
|
||||
// muss so bleiben: Jeder Aufruf von außen ginge an der Prüfung vorbei.
|
||||
func (s *Store) getCalendarAny(ctx context.Context, id uuid.UUID) (*Calendar, error) {
|
||||
var c Calendar
|
||||
err := s.db.QueryRow(ctx,
|
||||
`SELECT id, owner_id, name, color, description, supports_todo, timezone,
|
||||
COALESCE(path_segment, ''), ctag, sync_floor, created_at
|
||||
FROM cal_calendars WHERE id = $1`, id,
|
||||
).Scan(&c.ID, &c.OwnerID, &c.Name, &c.Color, &c.Description, &c.SupportsTodo,
|
||||
&c.TimeZone, &c.PathSegment, &c.CTag, &c.SyncFloor, &c.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (s *Store) ownerName(ctx context.Context, ownerID uuid.UUID) (string, error) {
|
||||
var name string
|
||||
err := s.db.QueryRow(ctx,
|
||||
`SELECT COALESCE(NULLIF(display_name, ''), username) FROM users WHERE id = $1`,
|
||||
ownerID).Scan(&name)
|
||||
return name, err
|
||||
}
|
||||
@@ -40,7 +40,10 @@ func (h *APIHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
response.Error(w, http.StatusInternalServerError, "list calendars failed")
|
||||
return
|
||||
}
|
||||
cals, err := h.store.ListCalendars(r.Context(), user.ID)
|
||||
// Eigene **und** freigegebene. Jeder Eintrag trägt sein Recht mit, damit
|
||||
// die Oberfläche einen nur lesbaren Kalender gar nicht erst zum
|
||||
// Bearbeiten anbietet.
|
||||
cals, err := h.store.ListAccessible(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusInternalServerError, "list calendars failed")
|
||||
return
|
||||
@@ -125,7 +128,8 @@ func (h *APIHandler) Export(w http.ResponseWriter, r *http.Request) {
|
||||
response.Error(w, http.StatusNotFound, "calendar not found")
|
||||
return
|
||||
}
|
||||
cal, err := h.store.GetCalendar(r.Context(), user.ID, id)
|
||||
// Ausgeben darf, wer lesen darf – auch bei einem freigegebenen Kalender.
|
||||
zugriff, err := h.store.RequireCalendar(r.Context(), user.ID, id, false)
|
||||
if err == ErrNotFound {
|
||||
response.Error(w, http.StatusNotFound, "calendar not found")
|
||||
return
|
||||
@@ -134,6 +138,7 @@ func (h *APIHandler) Export(w http.ResponseWriter, r *http.Request) {
|
||||
response.Error(w, http.StatusInternalServerError, "load calendar failed")
|
||||
return
|
||||
}
|
||||
cal := &zugriff.Calendar
|
||||
objs, err := h.store.ListObjects(r.Context(), cal.ID)
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusInternalServerError, "list objects failed")
|
||||
@@ -187,16 +192,22 @@ func (h *APIHandler) Import(w http.ResponseWriter, r *http.Request) {
|
||||
response.Error(w, http.StatusNotFound, "calendar not found")
|
||||
return
|
||||
}
|
||||
cal, err := h.store.GetCalendar(r.Context(), user.ID, id)
|
||||
// Hineinschreiben darf nur, wer schreiben darf.
|
||||
zugriff, err := h.store.RequireCalendar(r.Context(), user.ID, id, true)
|
||||
if err == ErrNotFound {
|
||||
response.Error(w, http.StatusNotFound, "calendar not found")
|
||||
return
|
||||
}
|
||||
if err == ErrReadOnly {
|
||||
response.Error(w, http.StatusForbidden, "calendar is shared read-only")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
response.Error(w, http.StatusInternalServerError, "load calendar failed")
|
||||
return
|
||||
}
|
||||
|
||||
cal := &zugriff.Calendar
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 20<<20) // 20 MiB – reicht für sehr große Kalender
|
||||
src, err := ical.NewDecoder(r.Body).Decode()
|
||||
if err != nil {
|
||||
|
||||
@@ -120,7 +120,10 @@ func parseCalendarPath(p string) (ref string, rest string, err error) {
|
||||
return parts[1], rest, nil
|
||||
}
|
||||
|
||||
func (b *Backend) resolveCalendar(ctx context.Context, p string) (*auth.User, *Calendar, error) {
|
||||
// resolveCalendar ist der **eine** Weg von einer Adresse zu einem Kalender –
|
||||
// und damit die Stelle, an der die Rechteprüfung sitzt. Jede CalDAV-Operation
|
||||
// läuft hier durch; eine zweite Auflösung daneben wäre eine Tür ohne Schloss.
|
||||
func (b *Backend) resolveCalendar(ctx context.Context, p string) (*auth.User, *Access, error) {
|
||||
u, err := userFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -129,7 +132,7 @@ func (b *Backend) resolveCalendar(ctx context.Context, p string) (*auth.User, *C
|
||||
if err != nil {
|
||||
return nil, nil, webdav.NewHTTPError(http.StatusNotFound, err)
|
||||
}
|
||||
cal, err := b.store.GetCalendarByRef(ctx, u.ID, ref)
|
||||
cal, err := b.store.ResolveRef(ctx, u.ID, ref)
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil, nil, webdav.NewHTTPError(http.StatusNotFound, err)
|
||||
}
|
||||
@@ -139,14 +142,36 @@ func (b *Backend) resolveCalendar(ctx context.Context, p string) (*auth.User, *C
|
||||
return u, cal, nil
|
||||
}
|
||||
|
||||
func toCalDAVCalendar(username string, c *Calendar) caldav.Calendar {
|
||||
// resolveWritable: dasselbe, aber der Aufrufer will schreiben. Ein nur lesend
|
||||
// freigegebener Kalender darf sichtbar bleiben und trotzdem ablehnen – 403
|
||||
// verrät hier nichts, was der Anfragende nicht ohnehin sieht.
|
||||
func (b *Backend) resolveWritable(ctx context.Context, p string) (*auth.User, *Access, error) {
|
||||
u, cal, err := b.resolveCalendar(ctx, p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !cal.CanWrite() {
|
||||
return nil, nil, webdav.NewHTTPError(http.StatusForbidden,
|
||||
errors.New("calendar is shared read-only"))
|
||||
}
|
||||
return u, cal, nil
|
||||
}
|
||||
|
||||
func toCalDAVCalendar(username string, c *Access) caldav.Calendar {
|
||||
comps := []string{"VEVENT"}
|
||||
if c.SupportsTodo {
|
||||
comps = append(comps, "VTODO")
|
||||
}
|
||||
// Ein fremder Kalender trägt den Eigentümer im Namen. Ohne das stehen in
|
||||
// der Liste des Clients zwei „Privat" nebeneinander, und welches welches
|
||||
// ist, weiß niemand.
|
||||
name := c.Name
|
||||
if c.Shared && c.OwnerName != "" {
|
||||
name = c.Name + " (" + c.OwnerName + ")"
|
||||
}
|
||||
return caldav.Calendar{
|
||||
Path: calendarPath(username, c.Ref()),
|
||||
Name: c.Name,
|
||||
Name: name,
|
||||
Description: c.Description,
|
||||
MaxResourceSize: 5 * 1024 * 1024,
|
||||
SupportedComponentSet: comps,
|
||||
@@ -194,13 +219,13 @@ func (b *Backend) ListCalendars(ctx context.Context) ([]caldav.Calendar, error)
|
||||
return nil, fmt.Errorf("ensure default calendar: %w", err)
|
||||
}
|
||||
|
||||
all, err := b.store.ListCalendars(ctx, u.ID)
|
||||
all, err := b.store.ListAccessible(ctx, u.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list calendars: %w", err)
|
||||
}
|
||||
out := make([]caldav.Calendar, len(all))
|
||||
for i, c := range all {
|
||||
out[i] = toCalDAVCalendar(u.Username, &c)
|
||||
for i := range all {
|
||||
out[i] = toCalDAVCalendar(u.Username, &all[i])
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -323,7 +348,7 @@ func etagFor(data string) string {
|
||||
}
|
||||
|
||||
func (b *Backend) PutCalendarObject(ctx context.Context, p string, cal *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (*caldav.CalendarObject, error) {
|
||||
u, calRow, err := b.resolveCalendar(ctx, p)
|
||||
u, calRow, err := b.resolveWritable(ctx, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -366,7 +391,7 @@ func (b *Backend) PutCalendarObject(ctx context.Context, p string, cal *ical.Cal
|
||||
}
|
||||
|
||||
func (b *Backend) DeleteCalendarObject(ctx context.Context, p string) error {
|
||||
_, calRow, err := b.resolveCalendar(ctx, p)
|
||||
_, calRow, err := b.resolveWritable(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -393,7 +393,8 @@ func (h *APIHandler) ListEvents(w http.ResponseWriter, r *http.Request) {
|
||||
response.Error(w, http.StatusNotFound, "calendar not found")
|
||||
return
|
||||
}
|
||||
if _, err := h.store.GetCalendar(r.Context(), user.ID, calID); err != nil {
|
||||
// Lesen genügt: Ein freigegebener Kalender soll sich ansehen lassen.
|
||||
if _, err := h.store.RequireCalendar(r.Context(), user.ID, calID, false); err != nil {
|
||||
response.Error(w, http.StatusNotFound, "calendar not found")
|
||||
return
|
||||
}
|
||||
@@ -552,13 +553,17 @@ func ruleFromRecurrence(body eventBody) *rrule.ROption {
|
||||
return opt
|
||||
}
|
||||
|
||||
// calendarOf steht vor **jedem schreibenden** Zugriff der Weboberfläche –
|
||||
// Anlegen, Ändern, Umhängen, Löschen. Deshalb verlangt es das Schreibrecht:
|
||||
// Ein nur lesend freigegebener Kalender darf sich ansehen lassen und trotzdem
|
||||
// nichts annehmen.
|
||||
func (h *APIHandler) calendarOf(r *http.Request) (uuid.UUID, bool) {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
calID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
return uuid.UUID{}, false
|
||||
}
|
||||
if _, err := h.store.GetCalendar(r.Context(), user.ID, calID); err != nil {
|
||||
if _, err := h.store.RequireCalendar(r.Context(), user.ID, calID, true); err != nil {
|
||||
return uuid.UUID{}, false
|
||||
}
|
||||
return calID, true
|
||||
@@ -658,10 +663,10 @@ func (h *APIHandler) MoveEvent(w http.ResponseWriter, r *http.Request) {
|
||||
response.Error(w, http.StatusBadRequest, "calendar_id is required")
|
||||
return
|
||||
}
|
||||
// Der Zielkalender muss demselben Konto gehören. Ohne diese Prüfung
|
||||
// In den Zielkalender muss geschrieben werden dürfen. Ohne diese Prüfung
|
||||
// schöbe man Termine in fremde Kalender – und ein fremder Kalender darf
|
||||
// sich nicht einmal als vorhanden verraten, deshalb 404 statt 403.
|
||||
if _, err := h.store.GetCalendar(r.Context(), user.ID, toCal); err != nil {
|
||||
if _, err := h.store.RequireCalendar(r.Context(), user.ID, toCal, true); err != nil {
|
||||
response.Error(w, http.StatusNotFound, "calendar not found")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -211,15 +211,19 @@ func extrasPropstat(cal *Calendar, want wantedExtras) string {
|
||||
// Eigenschaften an. Antworten für das Home-Set oder für einzelne Termine
|
||||
// bleiben unangetastet.
|
||||
func (h *Handler) injectExtras(ctx context.Context, user *auth.User, body []byte, want wantedExtras) []byte {
|
||||
cals, err := h.store.ListCalendars(ctx, user.ID)
|
||||
// **Accessible, nicht nur eigene**: Ein freigegebener Kalender steht mit
|
||||
// in der Liste und braucht dieselben Eigenschaften – ohne ctag und
|
||||
// sync-token hielte ihn der Client für unveränderlich.
|
||||
cals, err := h.store.ListAccessible(ctx, user.ID)
|
||||
if err != nil {
|
||||
return body // im Zweifel die Antwort der Bibliothek, unverändert
|
||||
}
|
||||
// Nach Pfadsegment, nicht nach UUID: Unter welcher Adresse ein Kalender
|
||||
// erreichbar ist, entscheidet der Client, wenn er ihn selbst angelegt hat.
|
||||
// Bei fremden Kalendern ist es immer die UUID – siehe Access.Ref.
|
||||
byRef := make(map[string]*Calendar, len(cals))
|
||||
for i := range cals {
|
||||
byRef[cals[i].Ref()] = &cals[i]
|
||||
byRef[cals[i].Ref()] = &cals[i].Calendar
|
||||
}
|
||||
|
||||
const closeTag = "</response>"
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"aether/internal/acl"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("calendar: not found")
|
||||
@@ -21,10 +23,14 @@ var ErrConflict = errors.New("calendar: uid already exists in target calendar")
|
||||
|
||||
type Store struct {
|
||||
db *pgxpool.Pool
|
||||
// rights beantwortet „wer darf was" – dieselbe Tabelle, die auch über
|
||||
// Ordner und Dateien entscheidet. Darf nil sein: Dann gibt es schlicht
|
||||
// keine freigegebenen Kalender, und alles Übrige funktioniert weiter.
|
||||
rights *acl.Store
|
||||
}
|
||||
|
||||
func NewStore(db *pgxpool.Pool) *Store {
|
||||
return &Store{db: db}
|
||||
func NewStore(db *pgxpool.Pool, rights *acl.Store) *Store {
|
||||
return &Store{db: db, rights: rights}
|
||||
}
|
||||
|
||||
func (s *Store) ListCalendars(ctx context.Context, ownerID uuid.UUID) ([]Calendar, error) {
|
||||
|
||||
@@ -99,11 +99,12 @@ func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request, user *aut
|
||||
http.Error(w, "sync-collection applies to a calendar collection", http.StatusForbidden)
|
||||
return true
|
||||
}
|
||||
cal, err := h.store.GetCalendarByRef(r.Context(), user.ID, ref)
|
||||
zugriff, err := h.store.ResolveRef(r.Context(), user.ID, ref)
|
||||
if err != nil {
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
return true
|
||||
}
|
||||
cal := &zugriff.Calendar
|
||||
|
||||
since, ok := parseSyncToken(req.SyncToken)
|
||||
if !ok {
|
||||
@@ -124,7 +125,7 @@ func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request, user *aut
|
||||
return true
|
||||
}
|
||||
|
||||
writeSyncResponse(w, user, cal, changes, req.wantsCalendarData())
|
||||
writeSyncResponse(w, user, ref, changes, req.wantsCalendarData())
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -175,8 +176,12 @@ func writeInvalidToken(w http.ResponseWriter) {
|
||||
`<error xmlns="DAV:"><valid-sync-token/></error>`)
|
||||
}
|
||||
|
||||
// ref ist die Adresse, unter der **dieser** Anfragende den Kalender sieht –
|
||||
// bei einem freigegebenen also dessen UUID, nicht das Pfadsegment des
|
||||
// Eigentümers. Die Antwort muss Adressen nennen, die der Client auch abrufen
|
||||
// kann.
|
||||
func writeSyncResponse(w http.ResponseWriter, user *auth.User,
|
||||
cal *Calendar, changes *Changes, withData bool) {
|
||||
ref string, changes *Changes, withData bool) {
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(xml.Header)
|
||||
@@ -189,7 +194,7 @@ func writeSyncResponse(w http.ResponseWriter, user *auth.User,
|
||||
// CalDAV-Clients vergleichen ETags als rohe Zeichenkette, und die
|
||||
// sähen dann bei jedem Durchgang eine Änderung, wo keine ist.
|
||||
fmt.Fprintf(&b, `<response><href>%s</href><propstat><prop><getetag>%s</getetag>`,
|
||||
xmlText(objectPath(user.Username, cal.Ref(), o.UID)), quoteETag(o.ETag))
|
||||
xmlText(objectPath(user.Username, ref, o.UID)), quoteETag(o.ETag))
|
||||
if withData {
|
||||
fmt.Fprintf(&b, `<C:calendar-data>%s</C:calendar-data>`, xmlText(o.Data))
|
||||
}
|
||||
@@ -201,7 +206,7 @@ func writeSyncResponse(w http.ResponseWriter, user *auth.User,
|
||||
// aber eine Eigenschaft nicht liefern".
|
||||
for _, uid := range changes.Deleted {
|
||||
fmt.Fprintf(&b, `<response><href>%s</href><status>HTTP/1.1 404 Not Found</status></response>`,
|
||||
xmlText(objectPath(user.Username, cal.Ref(), uid)))
|
||||
xmlText(objectPath(user.Username, ref, uid)))
|
||||
}
|
||||
|
||||
fmt.Fprintf(&b, `<sync-token>%s</sync-token></multistatus>`, syncToken(changes.Token))
|
||||
|
||||
Reference in new issue
Block a user