Files
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

570 lines
31 KiB
Go

package sharing
import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/idempotency"
"data-terminal-cloud/internal/identity"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrAccessDenied = errors.New("sharing access denied")
ErrInvalid = errors.New("sharing grant is invalid")
ErrNotFound = errors.New("sharing resource not found")
ErrExpired = errors.New("sharing grant expired or revoked")
ErrVersionConflict = errors.New("shared resource version conflict")
)
type mediaSigner interface {
PresignMediaDownload(context.Context, string, time.Duration, string, string, bool) (string, error)
}
type Service struct {
pool *pgxpool.Pool
audit *audit.Repository
idem *idempotency.Executor
store mediaSigner
accessTTL time.Duration
tokenSecret []byte
}
type CreateInput struct {
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
GranteeUserID string `json:"grantee_user_id"`
Permission string `json:"permission"`
Purpose string `json:"purpose"`
ExpiresAt time.Time `json:"expires_at"`
LoginRequired bool `json:"login_required"`
}
type Grant struct {
ID string `json:"id"`
SubjectID string `json:"subject_id"`
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
GranteeUserID string `json:"grantee_user_id"`
Permission string `json:"permission"`
Purpose string `json:"purpose"`
LoginRequired bool `json:"login_required"`
GrantedBy string `json:"granted_by"`
GrantedAt time.Time `json:"granted_at"`
ExpiresAt time.Time `json:"expires_at"`
RevokedAt *time.Time `json:"revoked_at,omitempty"`
Version int64 `json:"version"`
AccessToken string `json:"access_token,omitempty"`
}
type SharedTurn struct {
SequenceNo int64 `json:"sequence_no"`
Speaker string `json:"speaker"`
StartAt time.Time `json:"start_at"`
EndAt *time.Time `json:"end_at,omitempty"`
Text string `json:"text"`
Interrupted bool `json:"interrupted"`
}
type SharedResource struct {
GrantID string `json:"grant_id"`
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
Permission string `json:"permission"`
Title string `json:"title,omitempty"`
Text string `json:"text,omitempty"`
Turns []SharedTurn `json:"turns,omitempty"`
AccessURL string `json:"access_url,omitempty"`
AccessUntil *time.Time `json:"access_until,omitempty"`
Version int64 `json:"version,omitempty"`
RevisionNo int64 `json:"revision_no,omitempty"`
}
type resourceBoundary struct{ tenantID, householdID, subjectID, subjectPublicID string }
func NewService(pool *pgxpool.Pool, auditRepository *audit.Repository, store mediaSigner, tokenSecret []byte, accessTTL time.Duration) (*Service, error) {
if pool == nil || auditRepository == nil || store == nil || len(tokenSecret) < 32 || accessTTL <= 0 {
return nil, errors.New("sharing database, audit, object store, token secret and access TTL are required")
}
return &Service{pool: pool, audit: auditRepository, idem: idempotency.NewExecutor(pool), store: store, accessTTL: accessTTL, tokenSecret: append([]byte(nil), tokenSecret...)}, nil
}
func normalizeInput(input CreateInput) (CreateInput, error) {
input.ResourceType = strings.ToUpper(strings.TrimSpace(input.ResourceType))
input.Permission = strings.ToUpper(strings.TrimSpace(input.Permission))
input.Purpose = strings.TrimSpace(input.Purpose)
valid := (input.ResourceType == "STORY" && (input.Permission == "VIEW_TEXT" || input.Permission == "EDIT")) ||
(input.ResourceType == "TRANSCRIPT" && input.Permission == "VIEW_TEXT") ||
(input.ResourceType == "RECORDING" && (input.Permission == "LISTEN" || input.Permission == "DOWNLOAD"))
if !valid || (!input.LoginRequired && input.Permission == "EDIT") || input.ResourceID == "" || input.GranteeUserID == "" || input.Purpose == "" || len([]rune(input.Purpose)) > 500 || input.ExpiresAt.Before(time.Now().UTC().Add(time.Minute)) {
return CreateInput{}, ErrInvalid
}
return input, nil
}
func (s *Service) Create(ctx context.Context, principal identity.Principal, key, requestID string, raw CreateInput) (Grant, bool, error) {
input, err := normalizeInput(raw)
if err != nil {
return Grant{}, false, err
}
boundary, grantorID, grantorPublicID, err := s.resolveGrantor(ctx, s.pool, principal, input.ResourceType, input.ResourceID, false)
if err != nil {
return Grant{}, false, err
}
canonical, _ := json.Marshal(input)
grantPublicID := s.deriveGrantID(principal.Issuer + "|" + principal.Subject + "|" + key + "|" + idempotency.Hash(canonical))
var tokenDigest any
if !input.LoginRequired {
tokenDigest = digest(s.deriveAccessToken(grantPublicID))
}
result, err := s.idem.Execute(ctx, idempotency.Request{TenantID: &boundary.tenantID, ActorKey: principal.Issuer + "|" + principal.Subject, Scope: "sharing-grant.create", Key: key, RequestHash: idempotency.Hash(canonical), ExpiresAfter: 24 * time.Hour}, func(ctx context.Context, tx pgx.Tx) (idempotency.Result, error) {
locked, actorID, actorPublicID, err := s.resolveGrantor(ctx, tx, principal, input.ResourceType, input.ResourceID, true)
if err != nil {
return idempotency.Result{}, err
}
if locked.tenantID != boundary.tenantID || actorID != grantorID {
return idempotency.Result{}, ErrAccessDenied
}
var granteeID string
if err := tx.QueryRow(ctx, `SELECT id::text FROM user_account WHERE tenant_id=$1::uuid AND public_id=$2::uuid AND status='active'`, locked.tenantID, input.GranteeUserID).Scan(&granteeID); errors.Is(err, pgx.ErrNoRows) {
return idempotency.Result{}, ErrAccessDenied
} else if err != nil {
return idempotency.Result{}, err
}
if _, err := tx.Exec(ctx, `UPDATE sharing_grant SET revoked_at=now(),revoked_by=$1::uuid,version=version+1,updated_at=now() WHERE tenant_id=$2::uuid AND resource_type=$3 AND resource_public_id=$4::uuid AND grantee_user_id=$5::uuid AND permission=$6 AND revoked_at IS NULL AND expires_at>now()`, actorID, locked.tenantID, input.ResourceType, input.ResourceID, granteeID, input.Permission); err != nil {
return idempotency.Result{}, err
}
var item Grant
err = tx.QueryRow(ctx, `INSERT INTO sharing_grant(public_id,tenant_id,household_id,subject_id,resource_type,resource_public_id,grantee_user_id,permission,purpose,login_required,access_token_digest,granted_by,expires_at) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5,$6::uuid,$7::uuid,$8,$9,$10,$11,$12::uuid,$13) RETURNING public_id::text,$14::text,resource_type,resource_public_id::text,$15::text,permission,purpose,login_required,$16::text,granted_at,expires_at,revoked_at,version`, grantPublicID, locked.tenantID, locked.householdID, locked.subjectID, input.ResourceType, input.ResourceID, granteeID, input.Permission, input.Purpose, input.LoginRequired, tokenDigest, actorID, input.ExpiresAt, locked.subjectPublicID, input.GranteeUserID, actorPublicID).Scan(&item.ID, &item.SubjectID, &item.ResourceType, &item.ResourceID, &item.GranteeUserID, &item.Permission, &item.Purpose, &item.LoginRequired, &item.GrantedBy, &item.GrantedAt, &item.ExpiresAt, &item.RevokedAt, &item.Version)
if err != nil {
return idempotency.Result{}, err
}
if err := s.audit.Record(ctx, tx, audit.Event{TenantID: &locked.tenantID, ActorType: "user", ActorPublicID: &actorPublicID, Action: "sharing.grant.create", ResourceType: "sharing_grant", ResourceID: &item.ID, RequestID: requestID, Purpose: input.Purpose, Result: "success", Detail: map[string]any{"resource_type": input.ResourceType, "resource_id": input.ResourceID, "grantee_user_id": input.GranteeUserID, "permission": input.Permission, "expires_at": input.ExpiresAt}}); err != nil {
return idempotency.Result{}, err
}
body, _ := json.Marshal(item)
return idempotency.Result{StatusCode: 201, Body: body, ResourceID: &item.ID}, nil
})
if err != nil {
return Grant{}, false, err
}
var item Grant
if err := json.Unmarshal(result.Body, &item); err != nil {
return Grant{}, false, err
}
if !item.LoginRequired {
item.AccessToken = s.deriveAccessToken(item.ID)
}
_ = grantorPublicID
return item, !result.Replayed, nil
}
func (s *Service) deriveGrantID(value string) string {
mac := hmac.New(sha256.New, s.tokenSecret)
_, _ = mac.Write([]byte("sharing-grant:" + value))
sum := mac.Sum(nil)
sum[6] = (sum[6] & 0x0f) | 0x40
sum[8] = (sum[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", sum[0:4], sum[4:6], sum[6:8], sum[8:10], sum[10:16])
}
func (s *Service) deriveAccessToken(grantID string) string {
mac := hmac.New(sha256.New, s.tokenSecret)
_, _ = mac.Write([]byte("sharing-access:" + grantID))
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
}
func digest(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])
}
func (s *Service) List(ctx context.Context, principal identity.Principal, subjectPublicID string) ([]Grant, error) {
var tenantID, subjectID string
err := s.pool.QueryRow(ctx, `SELECT sp.tenant_id::text,sp.id::text FROM subject_profile sp JOIN household_member hm ON hm.tenant_id=sp.tenant_id AND hm.household_id=sp.household_id AND hm.status='active' JOIN user_account ua ON ua.id=hm.user_id AND ua.tenant_id=sp.tenant_id AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND ua.status='active' WHERE sp.public_id=$1::uuid AND sp.status='active' AND hm.role IN('owner','admin')`, subjectPublicID, principal.Issuer, principal.Subject).Scan(&tenantID, &subjectID)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrAccessDenied
}
if err != nil {
return nil, err
}
rows, err := s.pool.Query(ctx, `SELECT sg.public_id::text,$3::text,sg.resource_type,sg.resource_public_id::text,grantee.public_id::text,sg.permission,sg.purpose,sg.login_required,grantor.public_id::text,sg.granted_at,sg.expires_at,sg.revoked_at,sg.version FROM sharing_grant sg JOIN user_account grantee ON grantee.id=sg.grantee_user_id AND grantee.tenant_id=sg.tenant_id JOIN user_account grantor ON grantor.id=sg.granted_by AND grantor.tenant_id=sg.tenant_id WHERE sg.tenant_id=$1::uuid AND sg.subject_id=$2::uuid ORDER BY sg.granted_at DESC LIMIT 200`, tenantID, subjectID, subjectPublicID)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]Grant, 0)
for rows.Next() {
var item Grant
if err := rows.Scan(&item.ID, &item.SubjectID, &item.ResourceType, &item.ResourceID, &item.GranteeUserID, &item.Permission, &item.Purpose, &item.LoginRequired, &item.GrantedBy, &item.GrantedAt, &item.ExpiresAt, &item.RevokedAt, &item.Version); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) Revoke(ctx context.Context, principal identity.Principal, grantPublicID, requestID string) (Grant, error) {
var item Grant
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return item, err
}
defer tx.Rollback(context.Background())
var tenantID, actorID, actorPublicID, role string
err = tx.QueryRow(ctx, `SELECT sg.tenant_id::text,ua.id::text,ua.public_id::text,hm.role,sg.public_id::text,sp.public_id::text,sg.resource_type,sg.resource_public_id::text,grantee.public_id::text,sg.permission,sg.purpose,sg.login_required,grantor.public_id::text,sg.granted_at,sg.expires_at,sg.revoked_at,sg.version FROM sharing_grant sg JOIN subject_profile sp ON sp.id=sg.subject_id AND sp.tenant_id=sg.tenant_id JOIN household_member hm ON hm.tenant_id=sg.tenant_id AND hm.household_id=sg.household_id AND hm.status='active' JOIN user_account ua ON ua.id=hm.user_id AND ua.tenant_id=sg.tenant_id AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND ua.status='active' JOIN user_account grantee ON grantee.id=sg.grantee_user_id JOIN user_account grantor ON grantor.id=sg.granted_by WHERE sg.public_id=$1::uuid FOR UPDATE OF sg`, grantPublicID, principal.Issuer, principal.Subject).Scan(&tenantID, &actorID, &actorPublicID, &role, &item.ID, &item.SubjectID, &item.ResourceType, &item.ResourceID, &item.GranteeUserID, &item.Permission, &item.Purpose, &item.LoginRequired, &item.GrantedBy, &item.GrantedAt, &item.ExpiresAt, &item.RevokedAt, &item.Version)
if errors.Is(err, pgx.ErrNoRows) {
return item, ErrNotFound
}
if err != nil {
return item, err
}
if role != "owner" && role != "admin" {
return item, ErrAccessDenied
}
if item.RevokedAt == nil {
err = tx.QueryRow(ctx, `UPDATE sharing_grant SET revoked_at=now(),revoked_by=$2::uuid,version=version+1,updated_at=now() WHERE public_id=$1::uuid RETURNING revoked_at,version`, grantPublicID, actorID).Scan(&item.RevokedAt, &item.Version)
if err != nil {
return item, err
}
}
if err = s.audit.Record(ctx, tx, audit.Event{TenantID: &tenantID, ActorType: "user", ActorPublicID: &actorPublicID, Action: "sharing.grant.revoke", ResourceType: "sharing_grant", ResourceID: &item.ID, RequestID: requestID, Result: "success"}); err != nil {
return item, err
}
if err = tx.Commit(ctx); err != nil {
return item, err
}
return item, nil
}
func (s *Service) Access(ctx context.Context, principal identity.Principal, grantPublicID, requestID string) (SharedResource, error) {
digestBytes := sha256.Sum256([]byte(principal.Issuer + "|" + principal.Subject))
actorDigest := hex.EncodeToString(digestBytes[:])
var grantID, tenantID, userID, resourceType, resourceID, permission string
var allowed, active bool
err := s.pool.QueryRow(ctx, `SELECT sg.id::text,sg.tenant_id::text,COALESCE(ua.id::text,''),sg.resource_type,sg.resource_public_id::text,sg.permission,COALESCE(ua.id=sg.grantee_user_id,false), (sg.revoked_at IS NULL AND sg.expires_at>now()) FROM sharing_grant sg LEFT JOIN user_account ua ON ua.tenant_id=sg.tenant_id AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND ua.status='active' WHERE sg.public_id=$1::uuid`, grantPublicID, principal.Issuer, principal.Subject).Scan(&grantID, &tenantID, &userID, &resourceType, &resourceID, &permission, &allowed, &active)
if errors.Is(err, pgx.ErrNoRows) {
return SharedResource{}, ErrNotFound
}
if err != nil {
return SharedResource{}, err
}
if !active {
if logErr := s.logAccess(ctx, tenantID, grantID, userID, actorDigest, resourceType, resourceID, permission, "DENIED", "GRANT_INACTIVE", requestID); logErr != nil {
return SharedResource{}, logErr
}
return SharedResource{}, ErrExpired
}
if !allowed {
if logErr := s.logAccess(ctx, tenantID, grantID, userID, actorDigest, resourceType, resourceID, permission, "DENIED", "RECIPIENT_MISMATCH", requestID); logErr != nil {
return SharedResource{}, logErr
}
return SharedResource{}, ErrAccessDenied
}
shared := SharedResource{GrantID: grantPublicID, ResourceType: resourceType, ResourceID: resourceID, Permission: permission}
switch resourceType {
case "STORY":
err = s.pool.QueryRow(ctx, `SELECT sd.title,sr.content,sd.version,sr.revision_no FROM story_draft sd JOIN story_revision sr ON sr.id=sd.current_revision_id WHERE sd.public_id=$1::uuid AND sd.status<>'SUPERSEDED' AND call_story_available(sd.id)`, resourceID).Scan(&shared.Title, &shared.Text, &shared.Version, &shared.RevisionNo)
case "TRANSCRIPT":
rows, qerr := s.pool.Query(ctx, `SELECT ct.sequence_no,ct.speaker,ct.start_at,ct.end_at,ct.final_text,ct.interrupted FROM conversation_turn ct JOIN conversation c ON c.id=ct.conversation_id WHERE c.public_id=$1::uuid ORDER BY ct.sequence_no LIMIT 1000`, resourceID)
if qerr != nil {
return SharedResource{}, qerr
}
defer rows.Close()
shared.Turns = []SharedTurn{}
for rows.Next() {
var turn SharedTurn
if err := rows.Scan(&turn.SequenceNo, &turn.Speaker, &turn.StartAt, &turn.EndAt, &turn.Text, &turn.Interrupted); err != nil {
return SharedResource{}, err
}
shared.Turns = append(shared.Turns, turn)
}
err = rows.Err()
case "RECORDING":
var objectRef, contentType, track, conversationPublicID string
var sequence int
var consent bool
err = s.pool.QueryRow(ctx, `SELECT ms.object_ref,ms.content_type,ms.track,ms.sequence_no,c.public_id::text,EXISTS(SELECT 1 FROM consent_record cr WHERE cr.tenant_id=ms.tenant_id AND cr.subject_id=c.subject_id AND cr.consent_type='raw_audio_storage' AND cr.withdrawn_at IS NULL AND (cr.expires_at IS NULL OR cr.expires_at>now())) FROM media_segment ms JOIN conversation c ON c.id=ms.conversation_id WHERE ms.public_id=$1::uuid AND ms.status='VERIFIED'`, resourceID).Scan(&objectRef, &contentType, &track, &sequence, &conversationPublicID, &consent)
if err == nil && !consent {
if logErr := s.logAccess(ctx, tenantID, grantID, userID, actorDigest, resourceType, resourceID, permission, "DENIED", "CONSENT_REQUIRED", requestID); logErr != nil {
return SharedResource{}, logErr
}
return SharedResource{}, ErrAccessDenied
}
if err == nil {
extension := ".bin"
if contentType == "audio/wav" {
extension = ".wav"
} else if contentType == "audio/opus" {
extension = ".opus"
} else if contentType == "audio/ogg" {
extension = ".ogg"
}
until := time.Now().UTC().Add(s.accessTTL)
shared.AccessURL, err = s.store.PresignMediaDownload(ctx, objectRef, s.accessTTL, fmt.Sprintf("%s-%s-%08d%s", conversationPublicID, track, sequence, extension), contentType, permission == "LISTEN")
shared.AccessUntil = &until
}
default:
return SharedResource{}, ErrInvalid
}
if errors.Is(err, pgx.ErrNoRows) {
if logErr := s.logAccess(ctx, tenantID, grantID, userID, actorDigest, resourceType, resourceID, permission, "DENIED", "RESOURCE_UNAVAILABLE", requestID); logErr != nil {
return SharedResource{}, logErr
}
return SharedResource{}, ErrNotFound
}
if err != nil {
return SharedResource{}, err
}
if logErr := s.logAccess(ctx, tenantID, grantID, userID, actorDigest, resourceType, resourceID, permission, "SUCCESS", "", requestID); logErr != nil {
return SharedResource{}, logErr
}
return shared, nil
}
func (s *Service) AccessByToken(ctx context.Context, grantPublicID, token, requestID string) (SharedResource, error) {
token = strings.TrimSpace(token)
actorDigest := digest(token)
var grantID, tenantID, resourceType, resourceID, permission, storedDigest string
var active, loginRequired bool
err := s.pool.QueryRow(ctx, `SELECT id::text,tenant_id::text,resource_type,resource_public_id::text,permission,COALESCE(access_token_digest,''),(revoked_at IS NULL AND expires_at>now()),login_required FROM sharing_grant WHERE public_id=$1::uuid`, grantPublicID).Scan(&grantID, &tenantID, &resourceType, &resourceID, &permission, &storedDigest, &active, &loginRequired)
if errors.Is(err, pgx.ErrNoRows) {
return SharedResource{}, ErrNotFound
}
if err != nil {
return SharedResource{}, err
}
validToken := len(token) >= 32 && subtle.ConstantTimeCompare([]byte(actorDigest), []byte(storedDigest)) == 1
if loginRequired || !validToken {
if logErr := s.logAccess(ctx, tenantID, grantID, "", actorDigest, resourceType, resourceID, permission, "DENIED", "TOKEN_INVALID", requestID); logErr != nil {
return SharedResource{}, logErr
}
return SharedResource{}, ErrAccessDenied
}
if !active {
if logErr := s.logAccess(ctx, tenantID, grantID, "", actorDigest, resourceType, resourceID, permission, "DENIED", "GRANT_INACTIVE", requestID); logErr != nil {
return SharedResource{}, logErr
}
return SharedResource{}, ErrExpired
}
shared, err := s.loadResource(ctx, grantPublicID, resourceType, resourceID, permission)
if errors.Is(err, pgx.ErrNoRows) || errors.Is(err, ErrNotFound) {
_ = s.logAccess(ctx, tenantID, grantID, "", actorDigest, resourceType, resourceID, permission, "DENIED", "RESOURCE_UNAVAILABLE", requestID)
return SharedResource{}, ErrNotFound
}
if errors.Is(err, ErrAccessDenied) {
if logErr := s.logAccess(ctx, tenantID, grantID, "", actorDigest, resourceType, resourceID, permission, "DENIED", "CONSENT_REQUIRED", requestID); logErr != nil {
return SharedResource{}, logErr
}
return SharedResource{}, ErrAccessDenied
}
if err != nil {
return SharedResource{}, err
}
if err = s.logAccess(ctx, tenantID, grantID, "", actorDigest, resourceType, resourceID, permission, "SUCCESS", "", requestID); err != nil {
return SharedResource{}, err
}
return shared, nil
}
func (s *Service) loadResource(ctx context.Context, grantPublicID, resourceType, resourceID, permission string) (SharedResource, error) {
shared := SharedResource{GrantID: grantPublicID, ResourceType: resourceType, ResourceID: resourceID, Permission: permission}
switch resourceType {
case "STORY":
err := s.pool.QueryRow(ctx, `SELECT sd.title,sr.content,sd.version,sr.revision_no FROM story_draft sd JOIN story_revision sr ON sr.id=sd.current_revision_id WHERE sd.public_id=$1::uuid AND sd.status<>'SUPERSEDED' AND call_story_available(sd.id)`, resourceID).Scan(&shared.Title, &shared.Text, &shared.Version, &shared.RevisionNo)
return shared, err
case "TRANSCRIPT":
rows, err := s.pool.Query(ctx, `SELECT ct.sequence_no,ct.speaker,ct.start_at,ct.end_at,ct.final_text,ct.interrupted FROM conversation_turn ct JOIN conversation c ON c.id=ct.conversation_id WHERE c.public_id=$1::uuid ORDER BY ct.sequence_no LIMIT 1000`, resourceID)
if err != nil {
return shared, err
}
defer rows.Close()
shared.Turns = []SharedTurn{}
for rows.Next() {
var turn SharedTurn
if err := rows.Scan(&turn.SequenceNo, &turn.Speaker, &turn.StartAt, &turn.EndAt, &turn.Text, &turn.Interrupted); err != nil {
return shared, err
}
shared.Turns = append(shared.Turns, turn)
}
return shared, rows.Err()
case "RECORDING":
var objectRef, contentType, track, conversationPublicID string
var sequence int
var consent bool
err := s.pool.QueryRow(ctx, `SELECT ms.object_ref,ms.content_type,ms.track,ms.sequence_no,c.public_id::text,EXISTS(SELECT 1 FROM consent_record cr WHERE cr.tenant_id=ms.tenant_id AND cr.subject_id=c.subject_id AND cr.consent_type='raw_audio_storage' AND cr.withdrawn_at IS NULL AND (cr.expires_at IS NULL OR cr.expires_at>now())) FROM media_segment ms JOIN conversation c ON c.id=ms.conversation_id WHERE ms.public_id=$1::uuid AND ms.status='VERIFIED'`, resourceID).Scan(&objectRef, &contentType, &track, &sequence, &conversationPublicID, &consent)
if err != nil {
return shared, err
}
if !consent {
return shared, ErrAccessDenied
}
extension := ".bin"
if contentType == "audio/wav" {
extension = ".wav"
} else if contentType == "audio/opus" {
extension = ".opus"
} else if contentType == "audio/ogg" {
extension = ".ogg"
}
until := time.Now().UTC().Add(s.accessTTL)
shared.AccessURL, err = s.store.PresignMediaDownload(ctx, objectRef, s.accessTTL, fmt.Sprintf("%s-%s-%08d%s", conversationPublicID, track, sequence, extension), contentType, permission == "LISTEN")
shared.AccessUntil = &until
return shared, err
default:
return shared, ErrInvalid
}
}
func (s *Service) EditStory(ctx context.Context, principal identity.Principal, grantPublicID, content, reason, requestID string, expectedVersion int64) (SharedResource, error) {
content = strings.TrimSpace(content)
reason = strings.TrimSpace(reason)
if content == "" || len([]rune(content)) > 50000 || reason == "" || len([]rune(reason)) > 500 || expectedVersion < 1 {
return SharedResource{}, ErrInvalid
}
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return SharedResource{}, err
}
defer tx.Rollback(context.Background())
var callSubject string
err = tx.QueryRow(ctx, `SELECT sd.subject_id::text FROM sharing_grant sg
JOIN user_account ua ON ua.id=sg.grantee_user_id AND ua.tenant_id=sg.tenant_id AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND ua.status='active'
JOIN story_draft sd ON sd.public_id=sg.resource_public_id AND sd.tenant_id=sg.tenant_id
WHERE sg.public_id=$1::uuid AND sg.resource_type='STORY' AND sg.permission='EDIT' AND sg.revoked_at IS NULL AND sg.expires_at>now() AND sd.call_session_id IS NOT NULL`, grantPublicID, principal.Issuer, principal.Subject).Scan(&callSubject)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return SharedResource{}, err
}
if callSubject != "" {
var epoch int64
if err = tx.QueryRow(ctx, `SELECT privacy_epoch FROM memory_user_state WHERE subject_id=$1::uuid FOR UPDATE`, callSubject).Scan(&epoch); err != nil {
return SharedResource{}, err
}
}
var grantID, tenantID, userID, userPublicID, resourceID, permission, storyID, currentRevisionID, title string
var active bool
var version, revisionNo int64
err = tx.QueryRow(ctx, `SELECT sg.id::text,sg.tenant_id::text,ua.id::text,ua.public_id::text,sg.resource_public_id::text,sg.permission,(sg.revoked_at IS NULL AND sg.expires_at>now()),sd.id::text,sd.current_revision_id::text,sd.title,sd.version,sr.revision_no FROM sharing_grant sg JOIN user_account ua ON ua.id=sg.grantee_user_id AND ua.tenant_id=sg.tenant_id AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND ua.status='active' JOIN story_draft sd ON sd.public_id=sg.resource_public_id AND sd.tenant_id=sg.tenant_id JOIN story_revision sr ON sr.id=sd.current_revision_id WHERE sg.public_id=$1::uuid AND sg.resource_type='STORY' AND call_story_available(sd.id) FOR UPDATE OF sg,sd`, grantPublicID, principal.Issuer, principal.Subject).Scan(&grantID, &tenantID, &userID, &userPublicID, &resourceID, &permission, &active, &storyID, &currentRevisionID, &title, &version, &revisionNo)
if errors.Is(err, pgx.ErrNoRows) {
return SharedResource{}, ErrAccessDenied
}
if err != nil {
return SharedResource{}, err
}
if !active {
return SharedResource{}, ErrExpired
}
if permission != "EDIT" {
return SharedResource{}, ErrAccessDenied
}
if version != expectedVersion {
return SharedResource{}, ErrVersionConflict
}
var available bool
if err = tx.QueryRow(ctx, `SELECT call_story_available(id) AND status NOT IN ('REJECTED','SUPERSEDED') FROM story_draft WHERE id=$1::uuid`, storyID).Scan(&available); err != nil {
return SharedResource{}, err
}
if !available {
return SharedResource{}, ErrAccessDenied
}
var newRevisionID string
err = tx.QueryRow(ctx, `INSERT INTO story_revision(story_id,revision_no,content,author_type,author_id,edit_reason,base_revision_id) VALUES($1::uuid,$2,$3,'user',$4::uuid,$5,$6::uuid) RETURNING id::text`, storyID, revisionNo+1, content, userID, reason, currentRevisionID).Scan(&newRevisionID)
if err != nil {
return SharedResource{}, err
}
if _, err = tx.Exec(ctx, `INSERT INTO story_source(story_revision_id,conversation_turn_id,call_session_id,call_utterance_id,call_source_revision,import_turn_id,source_start_offset,source_end_offset,source_role) SELECT $1::uuid,conversation_turn_id,call_session_id,call_utterance_id,call_source_revision,import_turn_id,source_start_offset,source_end_offset,source_role FROM story_source WHERE story_revision_id=$2::uuid`, newRevisionID, currentRevisionID); err != nil {
return SharedResource{}, err
}
if err = tx.QueryRow(ctx, `UPDATE story_draft SET current_revision_id=$2::uuid,status='IN_REVIEW',reviewed_by=$3::uuid,reviewed_at=now(),review_reason=$4,version=version+1,updated_at=now() WHERE id=$1::uuid RETURNING version`, storyID, newRevisionID, userID, reason).Scan(&version); err != nil {
return SharedResource{}, err
}
digestBytes := sha256.Sum256([]byte(principal.Issuer + "|" + principal.Subject))
actorDigest := hex.EncodeToString(digestBytes[:])
if _, err = tx.Exec(ctx, `INSERT INTO sharing_access_log(tenant_id,sharing_grant_id,actor_user_id,actor_identifier_digest,resource_type,resource_public_id,permission,result,request_id) VALUES($1::uuid,$2::uuid,$3::uuid,$4,'STORY',$5::uuid,'EDIT','SUCCESS',$6)`, tenantID, grantID, userID, actorDigest, resourceID, requestID); err != nil {
return SharedResource{}, err
}
if err = s.audit.Record(ctx, tx, audit.Event{TenantID: &tenantID, ActorType: "user", ActorPublicID: &userPublicID, Action: "sharing.story.edit", ResourceType: "story", ResourceID: &resourceID, RequestID: requestID, Purpose: reason, Result: "success", Detail: map[string]any{"sharing_grant_id": grantPublicID, "revision_no": revisionNo + 1}}); err != nil {
return SharedResource{}, err
}
if err = tx.Commit(ctx); err != nil {
return SharedResource{}, err
}
return SharedResource{GrantID: grantPublicID, ResourceType: "STORY", ResourceID: resourceID, Permission: "EDIT", Title: title, Text: content, Version: version, RevisionNo: revisionNo + 1}, nil
}
func (s *Service) logAccess(ctx context.Context, tenantID, grantID, userID, actorDigest, resourceType, resourceID, permission, result, reason, requestID string) error {
var actor any
if userID != "" {
actor = userID
}
_, err := s.pool.Exec(ctx, `INSERT INTO sharing_access_log(tenant_id,sharing_grant_id,actor_user_id,actor_identifier_digest,resource_type,resource_public_id,permission,result,reason_code,request_id) VALUES($1::uuid,$2::uuid,$3::uuid,$4,$5,$6::uuid,$7,$8,NULLIF($9,''),$10)`, tenantID, grantID, actor, actorDigest, resourceType, resourceID, permission, result, reason, requestID)
return err
}
type queryer interface {
QueryRow(context.Context, string, ...any) pgx.Row
}
func (s *Service) resolveGrantor(ctx context.Context, q queryer, principal identity.Principal, resourceType, resourcePublicID string, lock bool) (resourceBoundary, string, string, error) {
resourceTable := ""
joins := ""
subjectExpr := ""
householdExpr := ""
tenantExpr := ""
switch resourceType {
case "STORY":
resourceTable = "story_draft resource"
joins = "JOIN subject_profile sp ON sp.id=resource.subject_id AND sp.tenant_id=resource.tenant_id"
subjectExpr = "resource.subject_id"
householdExpr = "sp.household_id"
tenantExpr = "resource.tenant_id"
case "TRANSCRIPT":
resourceTable = "conversation resource"
joins = "JOIN subject_profile sp ON sp.id=resource.subject_id AND sp.tenant_id=resource.tenant_id"
subjectExpr = "resource.subject_id"
householdExpr = "resource.household_id"
tenantExpr = "resource.tenant_id"
case "RECORDING":
resourceTable = "media_segment resource"
joins = "JOIN conversation c ON c.id=resource.conversation_id AND c.tenant_id=resource.tenant_id JOIN subject_profile sp ON sp.id=c.subject_id AND sp.tenant_id=c.tenant_id"
subjectExpr = "c.subject_id"
householdExpr = "c.household_id"
tenantExpr = "resource.tenant_id"
default:
return resourceBoundary{}, "", "", ErrInvalid
}
lockClause := ""
if resourceType == "STORY" {
lockClause = " AND call_story_available(resource.id)"
}
if lock {
lockClause += " FOR SHARE OF resource"
}
query := fmt.Sprintf(`SELECT %s::text,%s::text,%s::text,sp.public_id::text,ua.id::text,ua.public_id::text,hm.role FROM %s %s JOIN household_member hm ON hm.tenant_id=%s AND hm.household_id=%s AND hm.status='active' JOIN user_account ua ON ua.id=hm.user_id AND ua.tenant_id=%s AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND ua.status='active' WHERE resource.public_id=$1::uuid%s`, tenantExpr, householdExpr, subjectExpr, resourceTable, joins, tenantExpr, householdExpr, tenantExpr, lockClause)
var b resourceBoundary
var actorID, actorPublicID, role string
err := q.QueryRow(ctx, query, resourcePublicID, principal.Issuer, principal.Subject).Scan(&b.tenantID, &b.householdID, &b.subjectID, &b.subjectPublicID, &actorID, &actorPublicID, &role)
if errors.Is(err, pgx.ErrNoRows) {
return b, "", "", ErrNotFound
}
if err != nil {
return b, "", "", err
}
if role != "owner" && role != "admin" {
return b, "", "", ErrAccessDenied
}
return b, actorID, actorPublicID, nil
}