Initial import — snapshot from admin host /srv/gosec/gsc-ops-api

This repo had no version control prior to this commit. The import is a
straight snapshot of the working tree at 2026-05-03; the deployed
binary on fihelvop01 was being rebuilt from this source via `make
build` + scp into place, with no upstream review path.

The snapshot already includes one in-flight fix made on 2026-05-03 to
internal/service/persona.go:GetSelfModel — the handler queried
`source` and `strength` columns plus an `is_active = true` filter on
persona.persona_commitments, none of which exist on that table (its
shape is session-bound commitments with `status`, `commitment_meta`,
etc.). The query returned a 500 every time SynapseHub bootstrapped a
persona's self-model, dropping the IdentityConstraints / Commitments /
ConscienceStandards layer from the assembled prompt. The patched
query reads existing columns only (commitment_text, commitment_type),
filters on `status='active'`, and synthesises Source="learned" /
Strength=1.0 to keep the SelfModel response shape stable for callers.

Verified live: `GET /api/v1/personas/70f7cfd9-.../self-model` now
returns 200 with `{identityConstraints:[],commitments:[],
conscienceStandards:[]}` instead of 500.

Future changes go through PRs against this repo — no more bin-only
deploys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude (gsc-ops-api init)
2026-05-03 20:06:02 +02:00
commit 3847eb2036
68 changed files with 12982 additions and 0 deletions

449
internal/service/carddav.go Normal file
View File

@@ -0,0 +1,449 @@
package service
import (
"context"
"crypto/md5"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// CardDAVService handles CardDAV principal, address book, and contact operations
type CardDAVService struct {
pool *pgxpool.Pool
logger zerolog.Logger
}
// NewCardDAVService creates a new CardDAV service
func NewCardDAVService(pool *pgxpool.Pool, logger zerolog.Logger) *CardDAVService {
return &CardDAVService{
pool: pool,
logger: logger.With().Str("service", "carddav").Logger(),
}
}
// --- Principals ---
// ListPrincipals lists all principals
func (s *CardDAVService) ListPrincipals(ctx context.Context) ([]types.CardDAVPrincipal, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, uri, email, displayname FROM principals ORDER BY id`)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
principals := make([]types.CardDAVPrincipal, 0)
for rows.Next() {
var p types.CardDAVPrincipal
var email, displayName *string
if err := rows.Scan(&p.ID, &p.URI, &email, &displayName); err != nil {
return nil, fmt.Errorf("scan failed: %w", err)
}
if email != nil {
p.Email = *email
}
if displayName != nil {
p.DisplayName = *displayName
}
principals = append(principals, p)
}
return principals, nil
}
// GetPrincipal gets a principal by username
func (s *CardDAVService) GetPrincipal(ctx context.Context, username string) (*types.CardDAVPrincipal, error) {
uri := "principals/" + username
var p types.CardDAVPrincipal
var email, displayName *string
err := s.pool.QueryRow(ctx,
`SELECT id, uri, email, displayname FROM principals WHERE uri = $1`, uri).
Scan(&p.ID, &p.URI, &email, &displayName)
if err != nil {
if err == pgx.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("query failed: %w", err)
}
if email != nil {
p.Email = *email
}
if displayName != nil {
p.DisplayName = *displayName
}
return &p, nil
}
// CreatePrincipal creates a new principal
func (s *CardDAVService) CreatePrincipal(ctx context.Context, req *types.CardDAVPrincipalCreate) (*types.CardDAVPrincipal, error) {
uri := "principals/" + req.Username
var id int
err := s.pool.QueryRow(ctx,
`INSERT INTO principals (uri, email, displayname) VALUES ($1, $2, $3) RETURNING id`,
uri, nilIfEmpty(req.Email), nilIfEmpty(req.DisplayName)).Scan(&id)
if err != nil {
return nil, fmt.Errorf("insert failed: %w", err)
}
s.logger.Info().Str("username", req.Username).Int("id", id).Msg("Created principal")
return s.GetPrincipal(ctx, req.Username)
}
// DeletePrincipal deletes a principal and cascades to address books and contacts
func (s *CardDAVService) DeletePrincipal(ctx context.Context, username string) error {
uri := "principals/" + username
tx, err := s.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx failed: %w", err)
}
defer tx.Rollback(ctx)
// Delete contacts and changes for all address books owned by this principal
_, err = tx.Exec(ctx,
`DELETE FROM cards WHERE addressbookid IN (SELECT id FROM addressbooks WHERE principaluri = $1)`, uri)
if err != nil {
return fmt.Errorf("delete contacts failed: %w", err)
}
_, err = tx.Exec(ctx,
`DELETE FROM addressbookchanges WHERE addressbookid IN (SELECT id FROM addressbooks WHERE principaluri = $1)`, uri)
if err != nil {
return fmt.Errorf("delete changes failed: %w", err)
}
// Delete address books
_, err = tx.Exec(ctx,
`DELETE FROM addressbooks WHERE principaluri = $1`, uri)
if err != nil {
return fmt.Errorf("delete addressbooks failed: %w", err)
}
// Delete principal
ct, err := tx.Exec(ctx, `DELETE FROM principals WHERE uri = $1`, uri)
if err != nil {
return fmt.Errorf("delete principal failed: %w", err)
}
if ct.RowsAffected() == 0 {
return fmt.Errorf("principal not found")
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit failed: %w", err)
}
s.logger.Info().Str("username", username).Msg("Deleted principal with cascade")
return nil
}
// --- Address Books ---
// ListAddressBooks lists address books, optionally filtered by principal
func (s *CardDAVService) ListAddressBooks(ctx context.Context, principal string) ([]types.AddressBook, error) {
query := `SELECT id, principaluri, displayname, uri, description, synctoken FROM addressbooks`
args := []interface{}{}
if principal != "" {
query += ` WHERE principaluri = $1`
args = append(args, "principals/"+principal)
}
query += ` ORDER BY id`
rows, err := s.pool.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
books := make([]types.AddressBook, 0)
for rows.Next() {
var ab types.AddressBook
var description *string
if err := rows.Scan(&ab.ID, &ab.PrincipalURI, &ab.DisplayName, &ab.URI, &description, &ab.SyncToken); err != nil {
return nil, fmt.Errorf("scan failed: %w", err)
}
if description != nil {
ab.Description = *description
}
books = append(books, ab)
}
return books, nil
}
// GetAddressBook gets an address book by ID
func (s *CardDAVService) GetAddressBook(ctx context.Context, id int) (*types.AddressBook, error) {
var ab types.AddressBook
var description *string
err := s.pool.QueryRow(ctx,
`SELECT id, principaluri, displayname, uri, description, synctoken FROM addressbooks WHERE id = $1`, id).
Scan(&ab.ID, &ab.PrincipalURI, &ab.DisplayName, &ab.URI, &description, &ab.SyncToken)
if err != nil {
if err == pgx.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("query failed: %w", err)
}
if description != nil {
ab.Description = *description
}
return &ab, nil
}
// CreateAddressBook creates a new address book
func (s *CardDAVService) CreateAddressBook(ctx context.Context, req *types.AddressBookCreate) (*types.AddressBook, error) {
var id int
err := s.pool.QueryRow(ctx,
`INSERT INTO addressbooks (principaluri, displayname, uri, description, synctoken)
VALUES ($1, $2, $3, $4, 1) RETURNING id`,
req.PrincipalURI, req.DisplayName, req.URI, nilIfEmpty(req.Description)).Scan(&id)
if err != nil {
return nil, fmt.Errorf("insert failed: %w", err)
}
s.logger.Info().Int("id", id).Str("uri", req.URI).Msg("Created address book")
return s.GetAddressBook(ctx, id)
}
// UpdateAddressBook updates an address book
func (s *CardDAVService) UpdateAddressBook(ctx context.Context, id int, req *types.AddressBookUpdate) (*types.AddressBook, error) {
setClauses := []string{}
args := []interface{}{}
argIdx := 1
if req.DisplayName != nil {
setClauses = append(setClauses, fmt.Sprintf("displayname = $%d", argIdx))
args = append(args, *req.DisplayName)
argIdx++
}
if req.Description != nil {
setClauses = append(setClauses, fmt.Sprintf("description = $%d", argIdx))
args = append(args, *req.Description)
argIdx++
}
if len(setClauses) == 0 {
return s.GetAddressBook(ctx, id)
}
args = append(args, id)
query := fmt.Sprintf("UPDATE addressbooks SET %s WHERE id = $%d",
join(setClauses, ", "), argIdx)
ct, err := s.pool.Exec(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("update failed: %w", err)
}
if ct.RowsAffected() == 0 {
return nil, nil
}
return s.GetAddressBook(ctx, id)
}
// DeleteAddressBook deletes an address book and its contacts
func (s *CardDAVService) DeleteAddressBook(ctx context.Context, id int) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx failed: %w", err)
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx, `DELETE FROM cards WHERE addressbookid = $1`, id)
if err != nil {
return fmt.Errorf("delete contacts failed: %w", err)
}
_, err = tx.Exec(ctx, `DELETE FROM addressbookchanges WHERE addressbookid = $1`, id)
if err != nil {
return fmt.Errorf("delete changes failed: %w", err)
}
ct, err := tx.Exec(ctx, `DELETE FROM addressbooks WHERE id = $1`, id)
if err != nil {
return fmt.Errorf("delete addressbook failed: %w", err)
}
if ct.RowsAffected() == 0 {
return fmt.Errorf("address book not found")
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit failed: %w", err)
}
s.logger.Info().Int("id", id).Msg("Deleted address book with contacts")
return nil
}
// --- Contacts ---
// ListContacts lists contacts in an address book (metadata only, no carddata)
func (s *CardDAVService) ListContacts(ctx context.Context, addressBookID int) ([]types.Contact, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, addressbookid, uri, lastmodified, etag, size
FROM cards WHERE addressbookid = $1 ORDER BY id`, addressBookID)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
contacts := make([]types.Contact, 0)
for rows.Next() {
var c types.Contact
if err := rows.Scan(&c.ID, &c.AddressBookID, &c.URI, &c.LastModified, &c.ETag, &c.Size); err != nil {
return nil, fmt.Errorf("scan failed: %w", err)
}
contacts = append(contacts, c)
}
return contacts, nil
}
// GetContact gets a contact by address book ID and URI (returns full carddata)
func (s *CardDAVService) GetContact(ctx context.Context, addressBookID int, uri string) (*types.Contact, error) {
var c types.Contact
var cardData []byte
err := s.pool.QueryRow(ctx,
`SELECT id, addressbookid, carddata, uri, lastmodified, etag, size
FROM cards WHERE addressbookid = $1 AND uri = $2`, addressBookID, uri).
Scan(&c.ID, &c.AddressBookID, &cardData, &c.URI, &c.LastModified, &c.ETag, &c.Size)
if err != nil {
if err == pgx.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("query failed: %w", err)
}
c.CardData = string(cardData)
return &c, nil
}
// CreateContact creates a new contact in an address book
func (s *CardDAVService) CreateContact(ctx context.Context, addressBookID int, req *types.ContactCreate) (*types.Contact, error) {
etag := computeETag(req.CardData)
size := len(req.CardData)
lastModified := int(time.Now().Unix())
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, fmt.Errorf("begin tx failed: %w", err)
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx,
`INSERT INTO cards (addressbookid, carddata, uri, lastmodified, etag, size)
VALUES ($1, $2, $3, $4, $5, $6)`,
addressBookID, []byte(req.CardData), req.URI, lastModified, etag, size)
if err != nil {
return nil, fmt.Errorf("insert failed: %w", err)
}
// Record change and bump sync token (operation 1 = add)
if err := addChange(ctx, tx, addressBookID, req.URI, 1); err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, fmt.Errorf("commit failed: %w", err)
}
s.logger.Info().Int("addressbookId", addressBookID).Str("uri", req.URI).Msg("Created contact")
return s.GetContact(ctx, addressBookID, req.URI)
}
// UpdateContact updates a contact's vCard data
func (s *CardDAVService) UpdateContact(ctx context.Context, addressBookID int, uri string, req *types.ContactUpdate) (*types.Contact, error) {
etag := computeETag(req.CardData)
size := len(req.CardData)
lastModified := int(time.Now().Unix())
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, fmt.Errorf("begin tx failed: %w", err)
}
defer tx.Rollback(ctx)
ct, err := tx.Exec(ctx,
`UPDATE cards SET carddata = $1, lastmodified = $2, etag = $3, size = $4
WHERE addressbookid = $5 AND uri = $6`,
[]byte(req.CardData), lastModified, etag, size, addressBookID, uri)
if err != nil {
return nil, fmt.Errorf("update failed: %w", err)
}
if ct.RowsAffected() == 0 {
return nil, nil
}
// Record change and bump sync token (operation 2 = modify)
if err := addChange(ctx, tx, addressBookID, uri, 2); err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, fmt.Errorf("commit failed: %w", err)
}
s.logger.Info().Int("addressbookId", addressBookID).Str("uri", uri).Msg("Updated contact")
return s.GetContact(ctx, addressBookID, uri)
}
// DeleteContact deletes a contact from an address book
func (s *CardDAVService) DeleteContact(ctx context.Context, addressBookID int, uri string) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx failed: %w", err)
}
defer tx.Rollback(ctx)
ct, err := tx.Exec(ctx,
`DELETE FROM cards WHERE addressbookid = $1 AND uri = $2`, addressBookID, uri)
if err != nil {
return fmt.Errorf("delete failed: %w", err)
}
if ct.RowsAffected() == 0 {
return fmt.Errorf("contact not found")
}
// Record change and bump sync token (operation 3 = delete)
if err := addChange(ctx, tx, addressBookID, uri, 3); err != nil {
return err
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit failed: %w", err)
}
s.logger.Info().Int("addressbookId", addressBookID).Str("uri", uri).Msg("Deleted contact")
return nil
}
// addChange records a change in addressbookchanges and bumps the sync token.
// This is critical for CardDAV sync — without it, clients won't see incremental changes.
// Operations: 1=add, 2=modify, 3=delete
func addChange(ctx context.Context, tx pgx.Tx, addressBookID int, uri string, operation int) error {
_, err := tx.Exec(ctx,
`INSERT INTO addressbookchanges (uri, synctoken, addressbookid, operation)
SELECT $1, synctoken, $2, $3 FROM addressbooks WHERE id = $2`,
uri, addressBookID, operation)
if err != nil {
return fmt.Errorf("record change failed: %w", err)
}
_, err = tx.Exec(ctx,
`UPDATE addressbooks SET synctoken = synctoken + 1 WHERE id = $1`, addressBookID)
if err != nil {
return fmt.Errorf("bump synctoken failed: %w", err)
}
return nil
}
// computeETag computes the ETag for card data (raw MD5 hex, matching sabre/dav DB format)
func computeETag(cardData string) string {
hash := md5.Sum([]byte(cardData))
return fmt.Sprintf("%x", hash)
}