69 lines
3.4 KiB
Go
69 lines
3.4 KiB
Go
package callmemory
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"data-terminal-cloud/internal/identity"
|
||
|
|
"encoding/json"
|
||
|
|
"github.com/jackc/pgx/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
type AccessInput struct {
|
||
|
|
Elder string `json:"elder_id"`
|
||
|
|
User string `json:"user_id,omitempty"`
|
||
|
|
Read bool `json:"can_read"`
|
||
|
|
Write bool `json:"can_write"`
|
||
|
|
Sensitive bool `json:"sensitive_read"`
|
||
|
|
Version int64 `json:"expected_version"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) accessRequest(ctx context.Context, tx pgx.Tx, p identity.Principal, op string, raw []byte) (any, error) {
|
||
|
|
var in AccessInput
|
||
|
|
if e := StrictJSON(raw, &in); e != nil {
|
||
|
|
return nil, e
|
||
|
|
}
|
||
|
|
a, e := ResolveAccess(ctx, tx, p, in.Elder, false)
|
||
|
|
if e != nil {
|
||
|
|
return nil, e
|
||
|
|
}
|
||
|
|
if !a.Manage {
|
||
|
|
return nil, fault(403, "MEMORY_ACCESS_MANAGE_DENIED")
|
||
|
|
}
|
||
|
|
if op == "access/grant" {
|
||
|
|
if !validUUID(in.User) || in.Version < 0 || (!in.Read && (in.Write || in.Sensitive)) {
|
||
|
|
return nil, fault(400, "INVALID_MEMORY_GRANT")
|
||
|
|
}
|
||
|
|
var user, role string
|
||
|
|
e = tx.QueryRow(ctx, `SELECT ua.id::text,hm.role FROM user_account ua JOIN household_member hm ON hm.user_id=ua.id JOIN subject_profile sp ON sp.household_id=hm.household_id WHERE sp.id=$1::uuid AND ua.public_id=$2::uuid AND hm.status='active' AND ua.status='active' FOR UPDATE OF hm`, a.Subject, in.User).Scan(&user, &role)
|
||
|
|
if e != nil {
|
||
|
|
return nil, fault(403, "MEMORY_ACCESS_DENIED")
|
||
|
|
}
|
||
|
|
if role == "owner" {
|
||
|
|
return nil, fault(409, "OWNER_MEMORY_ACCESS_REQUIRED")
|
||
|
|
}
|
||
|
|
_, e = tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1,15))`, a.Subject+":"+user)
|
||
|
|
if e != nil {
|
||
|
|
return nil, e
|
||
|
|
}
|
||
|
|
var current int64
|
||
|
|
e = tx.QueryRow(ctx, `SELECT COALESCE((SELECT version FROM memory_access_grant WHERE subject_id=$1::uuid AND user_id=$2::uuid),0)`, a.Subject, user).Scan(¤t)
|
||
|
|
if e != nil {
|
||
|
|
return nil, e
|
||
|
|
}
|
||
|
|
if current != in.Version {
|
||
|
|
return nil, fault(409, "GRANT_VERSION_CONFLICT")
|
||
|
|
}
|
||
|
|
_, e = tx.Exec(ctx, `INSERT INTO memory_access_grant(subject_id,user_id,can_read,can_write,sensitive_read,version) VALUES($1::uuid,$2::uuid,$3,$4,$5,1) ON CONFLICT(subject_id,user_id) DO UPDATE SET can_read=excluded.can_read,can_write=excluded.can_write,sensitive_read=excluded.sensitive_read,version=memory_access_grant.version+1`, a.Subject, user, in.Read, in.Write, in.Sensitive)
|
||
|
|
if e != nil {
|
||
|
|
return nil, e
|
||
|
|
}
|
||
|
|
// Resolving a proposal does not preserve authority after this grant is revoked.
|
||
|
|
_, e = tx.Exec(ctx, `UPDATE memory_mutation SET confirmation_hash=NULL,proposal_digest=NULL,confirmation_expires_at=NULL,revision=revision+1 WHERE subject_id=$1::uuid AND actor_id=$2::uuid AND state='NEEDS_CONFIRMATION'`, a.Subject, user)
|
||
|
|
if e != nil {
|
||
|
|
return nil, e
|
||
|
|
}
|
||
|
|
}
|
||
|
|
var data []byte
|
||
|
|
e = tx.QueryRow(ctx, `SELECT COALESCE(jsonb_agg(jsonb_build_object('user_id',ua.public_id,'display_name',ua.display_name,'owner',hm.role='owner','can_read',hm.role='owner' OR COALESCE(g.can_read,false),'can_write',hm.role='owner' OR COALESCE(g.can_write,false),'sensitive_read',hm.role='owner' OR COALESCE(g.sensitive_read,false),'version',COALESCE(g.version,0)) ORDER BY ua.display_name),'[]'::jsonb) FROM subject_profile sp JOIN household_member hm ON hm.household_id=sp.household_id JOIN user_account ua ON ua.id=hm.user_id LEFT JOIN memory_access_grant g ON g.subject_id=sp.id AND g.user_id=ua.id WHERE sp.id=$1::uuid AND hm.status='active' AND ua.status='active'`, a.Subject).Scan(&data)
|
||
|
|
return map[string]any{"items": json.RawMessage(data)}, e
|
||
|
|
}
|