package postgres import ( "context" "database/sql" "git.gobha.me/xcaliber/chat-switchboard/models" "github.com/lib/pq" ) // ── ResourceGrantStore ────────────────────── type ResourceGrantStore struct{} func NewResourceGrantStore() *ResourceGrantStore { return &ResourceGrantStore{} } // Set creates or replaces the grant for a resource (upsert on unique resource_type+resource_id). func (s *ResourceGrantStore) Set(ctx context.Context, grant *models.ResourceGrant) error { // Coalesce nil to empty slice — pq.Array(nil) produces SQL NULL, but column expects array. groups := grant.GrantedGroups if groups == nil { groups = []string{} } return DB.QueryRowContext(ctx, ` INSERT INTO resource_grants (resource_type, resource_id, grant_scope, granted_groups, created_by) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (resource_type, resource_id) DO UPDATE SET grant_scope = EXCLUDED.grant_scope, granted_groups = EXCLUDED.granted_groups, created_by = EXCLUDED.created_by RETURNING id, created_at, updated_at`, grant.ResourceType, grant.ResourceID, grant.GrantScope, pq.Array(groups), grant.CreatedBy, ).Scan(&grant.ID, &grant.CreatedAt, &grant.UpdatedAt) } // Get retrieves the grant for a specific resource. func (s *ResourceGrantStore) Get(ctx context.Context, resourceType, resourceID string) (*models.ResourceGrant, error) { var rg models.ResourceGrant err := DB.QueryRowContext(ctx, ` SELECT id, resource_type, resource_id, grant_scope, granted_groups, created_by, created_at, updated_at FROM resource_grants WHERE resource_type = $1 AND resource_id = $2`, resourceType, resourceID, ).Scan(&rg.ID, &rg.ResourceType, &rg.ResourceID, &rg.GrantScope, pq.Array(&rg.GrantedGroups), &rg.CreatedBy, &rg.CreatedAt, &rg.UpdatedAt) if err != nil { return nil, err } return &rg, nil } // Delete removes the grant for a resource. func (s *ResourceGrantStore) Delete(ctx context.Context, resourceType, resourceID string) error { res, err := DB.ExecContext(ctx, "DELETE FROM resource_grants WHERE resource_type = $1 AND resource_id = $2", resourceType, resourceID) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return sql.ErrNoRows } return nil } // UserHasGroupAccess checks whether a user has group-based access to a resource. // Returns true if: // - The resource has grant_scope='global', OR // - The resource has grant_scope='groups' and the user is in at least one // of the granted_groups. func (s *ResourceGrantStore) UserHasGroupAccess(ctx context.Context, userID, resourceType, resourceID string) (bool, error) { var has bool err := DB.QueryRowContext(ctx, ` SELECT EXISTS( SELECT 1 FROM resource_grants rg WHERE rg.resource_type = $1 AND rg.resource_id = $2 AND ( rg.grant_scope = 'global' OR ( rg.grant_scope = 'groups' AND EXISTS ( SELECT 1 FROM group_members gm WHERE gm.user_id = $3 AND gm.group_id = ANY(rg.granted_groups) ) ) ) )`, resourceType, resourceID, userID).Scan(&has) return has, err }