package store import ( "context" "database/sql" "fmt" "strings" "atlas9.dev/c/core" "atlas9.dev/c/core/dbi" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/lib/access" ) type SqliteUserGrantStore struct { db dbi.DBI guard access.Guard } var _ iam.UserGrantStore = (*SqliteUserGrantStore)(nil) func NewSqliteUserGrantStore(db dbi.DBI, guard access.Guard) *SqliteUserGrantStore { return &SqliteUserGrantStore{db: db, guard: guard} } func (s *SqliteUserGrantStore) Add(ctx context.Context, g iam.UserGrant) error { if err := s.guard.Check(ctx, iam.CapGrantsAdd, g.Tenant, ""); err != nil { return err } _, err := s.db.Exec(ctx, ` INSERT INTO user_grants (tenant, user, role, path) VALUES ($1, $2, $3, $4) ON CONFLICT (tenant, user, role, path) DO NOTHING `, g.Tenant, g.User, g.Role, g.Path) // The FK to tenant_members enforces "member before grant"; a non-member // insert fails here rather than via an explicit precondition check. if err != nil && strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { return fmt.Errorf("user is not a member of this tenant") } return err } func (s *SqliteUserGrantStore) Remove(ctx context.Context, g iam.UserGrant) error { if err := s.guard.Check(ctx, iam.CapGrantsRemove, g.Tenant, ""); err != nil { return err } _, err := s.db.Exec(ctx, ` DELETE FROM user_grants WHERE tenant = $1 AND user = $2 AND role = $3 AND path = $4 `, g.Tenant, g.User, g.Role, g.Path) return err } func (s *SqliteUserGrantStore) ListByTenant(ctx context.Context, tenant core.ID, page core.PageReq) (core.Page[iam.UserGrant], error) { var out core.Page[iam.UserGrant] if err := s.guard.Check(ctx, iam.CapGrantsList, tenant, ""); err != nil { return out, err } limit := page.Limit if limit <= 0 { limit = 100 } rows, err := s.db.Query(ctx, ` SELECT tenant, user, role, path FROM user_grants WHERE tenant = $1 ORDER BY user, role, path LIMIT $2 `, tenant, limit) if err != nil { return out, err } defer rows.Close() err = scanUserGrants(rows, &out) return out, err } func (s *SqliteUserGrantStore) ListByUser(ctx context.Context, userID core.ID, page core.PageReq) (core.Page[iam.UserGrant], error) { var out core.Page[iam.UserGrant] // Deliberately a system-only gate — no self-service or per-tenant filtering // (unlike TenantMemberStore.ListByUser). A user's grants across all tenants // are operator-level information, so this is intentionally stricter than the // membership equivalent. if err := s.guard.System(ctx, iam.CapGrantsList); err != nil { return out, err } limit := page.Limit if limit <= 0 { limit = 100 } rows, err := s.db.Query(ctx, ` SELECT tenant, user, role, path FROM user_grants WHERE user = $1 ORDER BY tenant, role, path LIMIT $2 `, userID, limit) if err != nil { return out, err } defer rows.Close() err = scanUserGrants(rows, &out) return out, err } func scanUserGrants(rows *sql.Rows, out *core.Page[iam.UserGrant]) error { for rows.Next() { var g iam.UserGrant if err := rows.Scan(&g.Tenant, &g.User, &g.Role, &g.Path); err != nil { return err } out.Items = append(out.Items, g) } return rows.Err() }