fix(database): secure balance definers, least-privilege grants and destructive test isolation #42

Closed
opened 2026-10-14 03:11:18 +00:00 by PlasticDigits · 3 comments
Owner

Scope and source

Priority: P1. Gap coverage: GAP-015, GAP-019, GAP-039. Derived from GAP_1791945837.md, reviewed code commit f8dbe93df953a78b17cb3304b717dcf84c4cd517. Current e476a68da09645c2308cbf47b90211fcaffcb7af adds only the report, so implementation findings remain applicable. Report evidence is local reproduction/source inspection unless explicitly proposed; no production exploit or current live-readiness claim.

Coverage and sequencing: gap backlog tracker. Workstream 07. Dependencies: 05.

Current codebase

Balance SECURITY DEFINER functions reference unqualified tables without a pinned search_path; restricted-role temporary shadow tables returned forged balance 999. grants.sql uses ALL/default ALL on voting tables and a known login password. Startup does not validate actual privileges. Integration helpers fall back to DATABASE_URL then run migrations/TRUNCATE CASCADE and privilege DDL.

Why this implementation is needed

A restricted API role must not manufacture balances or mutate immutable votes/snapshots, and test tooling must never destroy an application database.

Relevant files

Add a forward migration schema-qualifying every definer relation and pinning a trusted search path with pg_temp explicitly last; audit ownership, public EXECUTE and schema CREATE/TEMP grants. Replace broad grants/default privileges with per-table/operation rights and narrow transactional routines where immutable inserts/freeze need mediation. Create roles without a usable default password; provision through authorized secret tooling and validate production effective privileges. Require explicit test-only configuration plus database identity/guard before any migration, truncate or role change; prefer ephemeral databases. Remove application-URL fallback and make required integration CI fail when its test DB is absent.

Constraints and guardrails

Preserve standalone offchain advisory voting, no new smart contracts or archive-node dependency, BSC as a core electorate, raw 18-decimal integer balances and separate Terra/BSC address identities. Keep Legal fail-closed transactional UI and the proven DEX Terra/Bridge EVM connectors; no browser BSC balance RPC, seed/key prompt or embedded admin/model credential. API must retain a restricted role separate from the ledger writer. Freeze at actual opening, never at draft creation or a client-supplied height. Agents own design, implementation, review and evidence under repository guidance; do not shift approvals/tests to the human or invent PASS evidence.

Acceptance criteria

Hostile search_path/temp tables cannot influence real balance queries. Restricted role cannot write ledger or UPDATE/DELETE/TRUNCATE immutable receipts/votes/frozen snapshots; legitimate lifecycle operations still work. Fresh and upgraded grants match the documented privilege matrix with no blanket future-table access. Destructive helpers reject unsafe/missing DB config before connecting or running DDL.

Test plan: supported paths, failure paths and concurrency

Fresh migration and populated upgrade; trusted/restricted/default/public roles; legal balance calls and all legitimate API mutations. Attempt temp/permanent shadow tables, hostile paths and direct mutation of each immutable table. Test guard parsing offline with production-like URLs, missing config, wrong database marker, correct ephemeral DB and required-job fail behavior. Repeat grants safely and exercise secure role rotation.

Test plan: attack, hack and abuse vectors

Temporary-table/function shadowing, schema hijack, inherited/public privileges, new-table privilege creep, known-password authentication, direct control-plane tampering and malicious/mistaken test URLs. No test may connect to production to prove rejection; use synthetic URLs and isolated fixtures.

Verification and completion criteria

Attach actual restricted-role permission matrix and original 999-shadow regression result, migration/grant upgrade evidence and guard refusal tests. Run all isolated DB suites after the guard lands; verify no working default password or credential-bearing logs.

Record commit and pinned toolchain, commands/results and sanitized artifacts. Relevant Rust changes require cargo test --workspace --lib plus isolated Postgres integration (explicit guarded LEDGER_TEST_DATABASE_URL); frontend changes require npm test and production build plus relevant browser cases. Run secret scanning for new fixtures/config. Exercise fresh/upgrade migrations when schema/grants change; report skipped/live-only checks explicitly.

<!-- voting-gap-1791945837:07 --> ## Scope and source **Priority:** P1. **Gap coverage:** GAP-015, GAP-019, GAP-039. Derived from [GAP_1791945837.md](https://git.cl8y.com/code/voting/src/branch/main/gaps/GAP_1791945837.md), reviewed code commit `f8dbe93df953a78b17cb3304b717dcf84c4cd517`. Current `e476a68da09645c2308cbf47b90211fcaffcb7af` adds only the report, so implementation findings remain applicable. Report evidence is local reproduction/source inspection unless explicitly proposed; no production exploit or current live-readiness claim. Coverage and sequencing: [gap backlog tracker](https://git.cl8y.com/code/voting/issues/35). Workstream **07**. Dependencies: [05](https://git.cl8y.com/code/voting/issues/40). ## Current codebase Balance SECURITY DEFINER functions reference unqualified tables without a pinned search_path; restricted-role temporary shadow tables returned forged balance 999. grants.sql uses ALL/default ALL on voting tables and a known login password. Startup does not validate actual privileges. Integration helpers fall back to DATABASE_URL then run migrations/TRUNCATE CASCADE and privilege DDL. ## Why this implementation is needed A restricted API role must not manufacture balances or mutate immutable votes/snapshots, and test tooling must never destroy an application database. ## Relevant files - [`ledger/migrations/20260825000001_ledger.sql`](https://git.cl8y.com/code/voting/src/branch/main/ledger/migrations/20260825000001_ledger.sql) - [`ledger/migrations/20260825000003_qualify_balance_fns.sql`](https://git.cl8y.com/code/voting/src/branch/main/ledger/migrations/20260825000003_qualify_balance_fns.sql) - [`deploy/grants.sql`](https://git.cl8y.com/code/voting/src/branch/main/deploy/grants.sql) - [`operator-voting/src/config.rs`](https://git.cl8y.com/code/voting/src/branch/main/operator-voting/src/config.rs) - [`operator-voting/src/balance_query.rs`](https://git.cl8y.com/code/voting/src/branch/main/operator-voting/src/balance_query.rs) - [`operator-voting/tests/privilege.rs`](https://git.cl8y.com/code/voting/src/branch/main/operator-voting/tests/privilege.rs) - [`operator-voting/tests/api_flow.rs`](https://git.cl8y.com/code/voting/src/branch/main/operator-voting/tests/api_flow.rs) - [`ledger/tests/ledger_integration.rs`](https://git.cl8y.com/code/voting/src/branch/main/ledger/tests/ledger_integration.rs) - [`docker-compose.test.yml`](https://git.cl8y.com/code/voting/src/branch/main/docker-compose.test.yml) - [`docs/OPS.md`](https://git.cl8y.com/code/voting/src/branch/main/docs/OPS.md) ## Recommended solution Add a forward migration schema-qualifying every definer relation and pinning a trusted search path with pg_temp explicitly last; audit ownership, public EXECUTE and schema CREATE/TEMP grants. Replace broad grants/default privileges with per-table/operation rights and narrow transactional routines where immutable inserts/freeze need mediation. Create roles without a usable default password; provision through authorized secret tooling and validate production effective privileges. Require explicit test-only configuration plus database identity/guard before any migration, truncate or role change; prefer ephemeral databases. Remove application-URL fallback and make required integration CI fail when its test DB is absent. ## Constraints and guardrails Preserve standalone offchain advisory voting, no new smart contracts or archive-node dependency, BSC as a core electorate, raw 18-decimal integer balances and separate Terra/BSC address identities. Keep Legal fail-closed transactional UI and the proven DEX Terra/Bridge EVM connectors; no browser BSC balance RPC, seed/key prompt or embedded admin/model credential. API must retain a restricted role separate from the ledger writer. Freeze at actual opening, never at draft creation or a client-supplied height. Agents own design, implementation, review and evidence under repository guidance; do not shift approvals/tests to the human or invent PASS evidence. ## Acceptance criteria Hostile search_path/temp tables cannot influence real balance queries. Restricted role cannot write ledger or UPDATE/DELETE/TRUNCATE immutable receipts/votes/frozen snapshots; legitimate lifecycle operations still work. Fresh and upgraded grants match the documented privilege matrix with no blanket future-table access. Destructive helpers reject unsafe/missing DB config before connecting or running DDL. ## Test plan: supported paths, failure paths and concurrency Fresh migration and populated upgrade; trusted/restricted/default/public roles; legal balance calls and all legitimate API mutations. Attempt temp/permanent shadow tables, hostile paths and direct mutation of each immutable table. Test guard parsing offline with production-like URLs, missing config, wrong database marker, correct ephemeral DB and required-job fail behavior. Repeat grants safely and exercise secure role rotation. ## Test plan: attack, hack and abuse vectors Temporary-table/function shadowing, schema hijack, inherited/public privileges, new-table privilege creep, known-password authentication, direct control-plane tampering and malicious/mistaken test URLs. No test may connect to production to prove rejection; use synthetic URLs and isolated fixtures. ## Verification and completion criteria Attach actual restricted-role permission matrix and original 999-shadow regression result, migration/grant upgrade evidence and guard refusal tests. Run all isolated DB suites after the guard lands; verify no working default password or credential-bearing logs. Record commit and pinned toolchain, commands/results and sanitized artifacts. Relevant Rust changes require `cargo test --workspace --lib` plus isolated Postgres integration (explicit guarded LEDGER_TEST_DATABASE_URL); frontend changes require `npm test` and production build plus relevant browser cases. Run secret scanning for new fixtures/config. Exercise fresh/upgrade migrations when schema/grants change; report skipped/live-only checks explicitly.
Author
Owner

Implementation status after main merge PR #53, commit cd07f00f32f04ad02f8b760ed34588fd9645919e:

  • Restricted SQL privileges, trusted paths and guarded test databases implemented with regression tests and crosslinked invariants/agent guidance.
  • Combined Rust1.88.0, isolated PostgreSQL, frontend/build/browser, OpenAPI and secret checks passed; see evidence.
  • Live release verification is still in progress. The first Coolify attempts failed before healthy acceptance. PR #54 fixes native-RPC entrypoint wiring, frontend image mode, liveness probes and pooled schema selection, with additional process/fault/privilege tests.

Services are paused while existing storage is audited and the corrected pinned release is prepared. No production wallet transaction or hardware-wallet/Legal QA is claimed. This is an interim report; the issue remains open and the final comment will map the original criteria to actual live results and any unverified cases.

Implementation status after main merge [PR #53](https://git.cl8y.com/code/voting/pulls/53), commit `cd07f00f32f04ad02f8b760ed34588fd9645919e`: - [x] Restricted SQL privileges, trusted paths and guarded test databases implemented with regression tests and crosslinked invariants/agent guidance. - [x] Combined Rust1.88.0, isolated PostgreSQL, frontend/build/browser, OpenAPI and secret checks passed; see [evidence](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-issues-36-43.md). - [ ] Live release verification is still in progress. The first Coolify attempts failed before healthy acceptance. [PR #54](https://git.cl8y.com/code/voting/pulls/54) fixes native-RPC entrypoint wiring, frontend image mode, liveness probes and pooled schema selection, with additional process/fault/privilege tests. Services are paused while existing storage is audited and the corrected pinned release is prepared. No production wallet transaction or hardware-wallet/Legal QA is claimed. This is an interim report; the issue remains open and the final comment will map the original criteria to actual live results and any unverified cases.
Author
Owner

The bounded read-only deployment o47uvv9xxkwhmmf6xcutmkfy completed from main 1562434c4237367f828ed71748ef71f116a5ff82. Production inspection confirmed15registrations plus2BSCtransfers/2balance checkpoints under the legacy voting schema; corresponding canonical public electorate/history tables are empty. The new public migration tracker contains11migrations while the legacy tracker contains5. This explains the undefined baseline_height failures and proves that switching pool paths alone would omit the existing electorate.

Accounting remains paused in read-only inspection mode. A forward import is being implemented and tested to preserve legacy registrations/history and frozen weights, retain the original tables for audit, reject conflicting data, and require explicit canonical baseline recovery. No issue is closed and no healthy voting deployment is claimed yet. The full latest isolated suite passed73unit/supervisor tests,92integration tests,245validated API responses,146frontend tests, production build/nginx/Gitleaks.

The bounded read-only deployment `o47uvv9xxkwhmmf6xcutmkfy` completed from main `1562434c4237367f828ed71748ef71f116a5ff82`. Production inspection confirmed15registrations plus2BSCtransfers/2balance checkpoints under the legacy `voting` schema; corresponding canonical `public` electorate/history tables are empty. The new public migration tracker contains11migrations while the legacy tracker contains5. This explains the undefined `baseline_height` failures and proves that switching pool paths alone would omit the existing electorate. Accounting remains paused in read-only inspection mode. A forward import is being implemented and tested to preserve legacy registrations/history and frozen weights, retain the original tables for audit, reject conflicting data, and require explicit canonical baseline recovery. No issue is closed and no healthy voting deployment is claimed yet. The full latest isolated suite passed73unit/supervisor tests,92integration tests,245validated API responses,146frontend tests, production build/nginx/Gitleaks.
Author
Owner

Implemented trusted balance definers, explicit SQL privileges and fail-closed destructive-test isolation. Changes are in PR #53, #54, #55, #56, and the throughput/test followup #59. PR #59 is merged with passing CI. Ledger/API/frontend deployments finished and are running healthy at final code 5cd9363307dc1607de952ac3243193c1a16304c7. Final live evidence.

Original acceptance and test-plan checklist:

  • Forward migrations qualify definer relations and pin trusted paths with pg_temp last. The original 999 temporary-table attack returns real missing-wallet balance 0. Permanent same-name tables/functions and hostile startup/session paths cannot change canonical balances; six physical API connections are tested.
  • The actual restricted permission matrix denies ledger writes and UPDATE/DELETE/TRUNCATE of immutable receipts/votes/revisions/signatures/frozen snapshots. Forged snapshot/freeze metadata, processed queue fields, opened-content/identity changes, schema CREATE and database TEMP reject. All legitimate dual-chain signed lifecycle operations still pass through the restricted SQL pool.
  • Fresh/populated upgrades and repeated grants match the documented per-table/operation/column matrix; no future-table blanket rights remain. PUBLIC/inherited column writes, NOINHERIT/SET-role escape, elevated/owner roles, protected definer ownership and writer-authenticated masking fail startup validation.
  • Real SCRAM TCP rejects fresh NULL/empty and historical default passwords. Memory-only random provisioning enables genuine restricted login; rotation to a second credential rejects the former with 28P01 and the replacement passes current_user=session_user/effective privileges. The deployed API also passed restricted startup; memory-only inspection confirmed its credential differs from the historical default without printing/persisting it.
  • Missing/unsafe/production-like test URLs fail offline before connection; no DATABASE_URL fallback exists. Explicit test name/host/destructive opt-in precede connection, then actual database identity/catalog marker precedes migrations/TRUNCATE/role DDL. Wrong markers refuse, marked disposable databases succeed, required jobs fail when configuration is missing, and CI service access requires its explicit exact-host opt-in. No rejection test contacts production.
  • All isolated suites run after those guards with fresh SCRAM PostgreSQL and durability retained. Production-default INFO/public-error sentinel regressions and secret scanning show no credential-bearing output. Shadow/schema hijack, direct immutable tampering, privilege creep and malicious URL regressions pass.
  • Promotion005 archives full originals, compares natural keys/IDs before import, rolls back nonidentical conflicts, accepts identical mixed storage and repeats safely. Ambiguous CW20 identities remain archived; legacy tables/tracker and frozen poll history are preserved. Three isolated promotion tests cover these cases.
  • The preservation comment is satisfied by the post-recovery inventory and final storage audit: all 15 original registrations retain every original column; the canonical 17 contain only two additional unfunded live probes, with no missing legacy registration. Both BSC transfer rows match and both old balance rows remain. Legacy tracker stays 5 records; canonical tracker has 12 through 005. Original tables/history are retained and runtime recovery/inspection settings have been checked.
  • Pinned Rust 1.88.0 CARGO_BUILD_JOBS=2 scripts/verify-release.sh release13 passes 74 unit/supervisor tests, 121 integrations (66 ledger), 245 schema validations and 146 frontend tests, production build, both nginx checks and Gitleaks. Machine-readable results, combined report, and lifecycle evidence record the restricted matrix, 999 attack, upgrades and guard refusals. The late supervised recovery fixture now permits at most one nonzero restart and requires post-restore fresh both-chain/intent progress plus sustained readiness; four fresh repeats pass.
  • OPERATOR_VOTING.md, OPS.md and AGENTS_TRANSACTIONAL_VOTING.md crosslink the matrix, safe test/provisioning workflow and compatibility limits. Independent throughput review preserves actual-end baseline certification, unchanged bounds, post-SQL provider checks, atomic publication and immutable frozen weights.
  • PR #59 CI/main merge and exact-code Coolify deployment complete at 5cd9363307dc1607de952ac3243193c1a16304c7. All three services are running healthy; smoke 13, canonical fixtures and runtime configuration pass. No post-activation ledger warnings appear in Loki; the 13:56:21 direct Coolify warning precedes 13:58:22 activation. Final artifact.
  • Final private windows 240+240+600 all pass (1080 observations, maximum Terra lag 2/BSC 17), followed by two accepted public 45/45 windows. Both original unfunded chain receipts independently verify and exact/expired retries remain identical through the restricted API. Final artifact.

Operational observation: the first public readiness window was 44/45, with one unexplained 503 at 14:00:58 and 200 on the next check. This is recorded, followed by 90 consecutive successful public checks and 1080 successful private observations. Thresholds were preserved and no false healthy state was introduced. The earlier 503 supplies no evidence of forged balances, privilege escape, unsafe test access or immutable-record mutation.

Evidence limits: credential rotation/destructive permission attacks use isolated synthetic fixtures, not live rotation/destructive SQL. No funded-holder/committee blockchain signing, hardware/mobile wallet or live human Legal QA is claimed. Legacy hash-only receipts and uncertified polls retain explicit verification limits. No Prometheus datasource is provisioned; live log evidence uses Grafana/Loki. The issue's SQL privilege/isolation and final-release acceptance criteria are satisfied by the evidence above.

Final evidence is published through PR #60. All mandatory body/comment criteria are satisfied by the checklist above; closing this issue.

Implemented trusted balance definers, explicit SQL privileges and fail-closed destructive-test isolation. Changes are in [PR #53](https://git.cl8y.com/code/voting/pulls/53), [#54](https://git.cl8y.com/code/voting/pulls/54), [#55](https://git.cl8y.com/code/voting/pulls/55), [#56](https://git.cl8y.com/code/voting/pulls/56), and the throughput/test followup [#59](https://git.cl8y.com/code/voting/pulls/59). PR #59 is merged with passing CI. Ledger/API/frontend deployments finished and are running healthy at final code `5cd9363307dc1607de952ac3243193c1a16304c7`. [Final live evidence](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-final-live.json). Original acceptance and test-plan checklist: - [x] Forward migrations qualify definer relations and pin trusted paths with pg_temp last. The original 999 temporary-table attack returns real missing-wallet balance 0. Permanent same-name tables/functions and hostile startup/session paths cannot change canonical balances; six physical API connections are tested. - [x] The actual restricted permission matrix denies ledger writes and UPDATE/DELETE/TRUNCATE of immutable receipts/votes/revisions/signatures/frozen snapshots. Forged snapshot/freeze metadata, processed queue fields, opened-content/identity changes, schema CREATE and database TEMP reject. All legitimate dual-chain signed lifecycle operations still pass through the restricted SQL pool. - [x] Fresh/populated upgrades and repeated grants match the documented per-table/operation/column matrix; no future-table blanket rights remain. PUBLIC/inherited column writes, NOINHERIT/SET-role escape, elevated/owner roles, protected definer ownership and writer-authenticated masking fail startup validation. - [x] Real SCRAM TCP rejects fresh NULL/empty and historical default passwords. Memory-only random provisioning enables genuine restricted login; rotation to a second credential rejects the former with 28P01 and the replacement passes current_user=session_user/effective privileges. The deployed API also passed restricted startup; memory-only inspection confirmed its credential differs from the historical default without printing/persisting it. - [x] Missing/unsafe/production-like test URLs fail offline before connection; no DATABASE_URL fallback exists. Explicit test name/host/destructive opt-in precede connection, then actual database identity/catalog marker precedes migrations/TRUNCATE/role DDL. Wrong markers refuse, marked disposable databases succeed, required jobs fail when configuration is missing, and CI service access requires its explicit exact-host opt-in. No rejection test contacts production. - [x] All isolated suites run after those guards with fresh SCRAM PostgreSQL and durability retained. Production-default INFO/public-error sentinel regressions and secret scanning show no credential-bearing output. Shadow/schema hijack, direct immutable tampering, privilege creep and malicious URL regressions pass. - [x] Promotion005 archives full originals, compares natural keys/IDs before import, rolls back nonidentical conflicts, accepts identical mixed storage and repeats safely. Ambiguous CW20 identities remain archived; legacy tables/tracker and frozen poll history are preserved. Three isolated promotion tests cover these cases. - [x] The preservation comment is satisfied by the [post-recovery inventory](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-post-recovery-inventory.json) and [final storage audit](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-final-live.json): all 15 original registrations retain every original column; the canonical 17 contain only two additional unfunded live probes, with no missing legacy registration. Both BSC transfer rows match and both old balance rows remain. Legacy tracker stays 5 records; canonical tracker has 12 through 005. Original tables/history are retained and runtime recovery/inspection settings have been checked. - [x] Pinned Rust 1.88.0 `CARGO_BUILD_JOBS=2 scripts/verify-release.sh` release13 passes 74 unit/supervisor tests, 121 integrations (66 ledger), 245 schema validations and 146 frontend tests, production build, both nginx checks and Gitleaks. [Machine-readable results](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-release13.json), [combined report](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-issues-36-43.md), and [lifecycle evidence](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-lifecycle-40-42.md) record the restricted matrix, 999 attack, upgrades and guard refusals. The late supervised recovery fixture now permits at most one nonzero restart and requires post-restore fresh both-chain/intent progress plus sustained readiness; four fresh repeats pass. - [x] [OPERATOR_VOTING.md](https://git.cl8y.com/code/voting/src/branch/main/docs/OPERATOR_VOTING.md), [OPS.md](https://git.cl8y.com/code/voting/src/branch/main/docs/OPS.md) and [AGENTS_TRANSACTIONAL_VOTING.md](https://git.cl8y.com/code/voting/src/branch/main/skills/AGENTS_TRANSACTIONAL_VOTING.md) crosslink the matrix, safe test/provisioning workflow and compatibility limits. Independent throughput review preserves actual-end baseline certification, unchanged bounds, post-SQL provider checks, atomic publication and immutable frozen weights. - [x] PR #59 CI/main merge and exact-code Coolify deployment complete at `5cd9363307dc1607de952ac3243193c1a16304c7`. All three services are running healthy; smoke 13, canonical fixtures and runtime configuration pass. No post-activation ledger warnings appear in Loki; the 13:56:21 direct Coolify warning precedes 13:58:22 activation. [Final artifact](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-final-live.json). - [x] Final private windows 240+240+600 all pass (1080 observations, maximum Terra lag 2/BSC 17), followed by two accepted public 45/45 windows. Both original unfunded chain receipts independently verify and exact/expired retries remain identical through the restricted API. [Final artifact](https://git.cl8y.com/code/voting/src/branch/main/docs/verification/2026-10-14-final-live.json). Operational observation: the first public readiness window was 44/45, with one unexplained 503 at 14:00:58 and 200 on the next check. This is recorded, followed by 90 consecutive successful public checks and 1080 successful private observations. Thresholds were preserved and no false healthy state was introduced. The earlier 503 supplies no evidence of forged balances, privilege escape, unsafe test access or immutable-record mutation. Evidence limits: credential rotation/destructive permission attacks use isolated synthetic fixtures, not live rotation/destructive SQL. No funded-holder/committee blockchain signing, hardware/mobile wallet or live human Legal QA is claimed. Legacy hash-only receipts and uncertified polls retain explicit verification limits. No Prometheus datasource is provisioned; live log evidence uses Grafana/Loki. The issue's SQL privilege/isolation and final-release acceptance criteria are satisfied by the evidence above. Final evidence is published through [PR #60](https://git.cl8y.com/code/voting/pulls/60). All mandatory body/comment criteria are satisfied by the checklist above; closing this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/voting#42
No description provided.