ekwo-os 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +873 -0
- package/dist/args.d.ts +57 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +134 -0
- package/dist/args.js.map +1 -0
- package/dist/assets/expected-objects.json +9432 -0
- package/dist/assets/migrations/20260911120000_core_companies.sql +260 -0
- package/dist/assets/migrations/20260911120100_accounts_journals.sql +211 -0
- package/dist/assets/migrations/20260911120200_contacts.sql +90 -0
- package/dist/assets/migrations/20260911120300_taxes.sql +146 -0
- package/dist/assets/migrations/20260911120400_entries.sql +338 -0
- package/dist/assets/migrations/20260911120500_documents.sql +279 -0
- package/dist/assets/migrations/20260911120600_payments_reconciliation.sql +251 -0
- package/dist/assets/migrations/20260911120700_bank.sql +178 -0
- package/dist/assets/migrations/20260911120800_analytics_attachments.sql +128 -0
- package/dist/assets/migrations/20260911120900_post_document.sql +279 -0
- package/dist/assets/migrations/20260911121000_reporting.sql +235 -0
- package/dist/assets/migrations/20260911121100_country_templates.sql +226 -0
- package/dist/assets/migrations/20260911121200_fec.sql +71 -0
- package/dist/assets/migrations/20260911130000_instance.sql +77 -0
- package/dist/assets/migrations/20260911130100_instance_members.sql +224 -0
- package/dist/assets/migrations/20260911140000_instance_admins.sql +112 -0
- package/dist/assets/migrations/20260911160000_tax_posting_templates_unique.sql +17 -0
- package/dist/assets/migrations/20260911170000_document_amount_paid.sql +82 -0
- package/dist/assets/migrations/20260911173000_post_payment.sql +122 -0
- package/dist/assets/migrations/20260911173100_sequence_counters_under_rls.sql +84 -0
- package/dist/assets/migrations/20260911183000_country_journal_defaults.sql +143 -0
- package/dist/assets/migrations/20260911193853_line_account_defaults.sql +274 -0
- package/dist/assets/migrations/20260911195054_products.sql +221 -0
- package/dist/assets/migrations/20260911210131_anon_surface.sql +85 -0
- package/dist/assets/migrations/20260912074712_country_packs.sql +318 -0
- package/dist/assets/migrations/20260912080311_report_code_and_region.sql +198 -0
- package/dist/assets/migrations/20260912081014_pack_certification_maintained.sql +31 -0
- package/dist/assets/migrations/20260912081015_pack_certification_backfill.sql +16 -0
- package/dist/assets/migrations/20260912090407_tax_report_boxes.sql +329 -0
- package/dist/assets/migrations/20260912091917_tax_on_base_value.sql +25 -0
- package/dist/assets/migrations/20260912091918_tax_engine_columns.sql +776 -0
- package/dist/assets/migrations/20260912094412_opening_and_closing.sql +759 -0
- package/dist/assets/migrations/20260912095825_charts_of_accounts.sql +326 -0
- package/dist/assets/migrations/20260912100412_financial_statements.sql +632 -0
- package/dist/assets/migrations/20260912104719_one_formula_evaluator.sql +209 -0
- package/dist/assets/migrations/20260912105720_entry_kind_appropriation.sql +26 -0
- package/dist/assets/migrations/20260912105721_appropriation_entry_kind.sql +444 -0
- package/dist/assets/migrations/20260912111751_document_rules.sql +274 -0
- package/dist/assets/migrations/20260912112132_cash_basis_vat_and_fx.sql +1189 -0
- package/dist/assets/migrations/20260913074512_modules.sql +493 -0
- package/dist/assets/migrations/20260913075903_asset_disposal_roles.sql +47 -0
- package/dist/assets/migrations/20260913083216_capabilities.sql +637 -0
- package/dist/assets/migrations/20260913083901_company_invitations.sql +262 -0
- package/dist/assets/migrations/20260913084402_user_preferences.sql +376 -0
- package/dist/assets/migrations/20260913084847_company_profile.sql +261 -0
- package/dist/assets/migrations/20260913085436_numbering_reads_the_pack.sql +235 -0
- package/dist/assets/migrations/20260913085932_api_keys.sql +303 -0
- package/dist/assets/migrations/20260913090216_fiscal_year_bounds.sql +159 -0
- package/dist/assets/migrations/20260913092527_entries_import.sql +219 -0
- package/dist/assets/migrations/20260913101536_null_safe_guards.sql +188 -0
- package/dist/assets/migrations/20260913102115_installer_is_named.sql +462 -0
- package/dist/assets/migrations/20260913102758_no_currency_default.sql +138 -0
- package/dist/assets/migrations/20260913103355_cash_basis_needs_a_box.sql +453 -0
- package/dist/assets/migrations/20260913104014_aged_balance_named_group.sql +79 -0
- package/dist/assets/migrations/20260913104232_foreign_key_indexes.sql +144 -0
- package/dist/assets/migrations/20260913105120_declared_no_reader.sql +41 -0
- package/dist/assets/migrations/20260913111407_pack_languages.sql +223 -0
- package/dist/assets/migrations/20260913112233_schema_comments_for_a_reader.sql +27 -0
- package/dist/assets/migrations/20260913114535_fec_opening_balances.sql +307 -0
- package/dist/assets/migrations/20260914103412_audit_log.sql +509 -0
- package/dist/assets/migrations/20260914111907_pack_upgrade.sql +428 -0
- package/dist/assets/migrations/20260914120500_rounding_reads_the_currency.sql +206 -0
- package/dist/assets/migrations/20260914121200_amounts_round_at_the_currency.sql +2403 -0
- package/dist/assets/migrations/20260914134325_schema_version_0_2_0.sql +36 -0
- package/dist/assets/migrations/20260914143915_accounts_in_use.sql +428 -0
- package/dist/assets/migrations/20260914144731_account_code_frozen.sql +99 -0
- package/dist/assets/migrations/20260914151207_schema_grants_its_own_rights.sql +284 -0
- package/dist/assets/migrations/20260914152840_pack_upgrade_records_its_own_line.sql +237 -0
- package/dist/assets/migrations/20260914163943_declaration_periodicity.sql +349 -0
- package/dist/assets/migrations/20260915094000_foreign_services_received.sql +37 -0
- package/dist/assets/migrations/20260915094500_mentions_read_a_foreign_service.sql +72 -0
- package/dist/assets/migrations/20260915153000_document_shares.sql +568 -0
- package/dist/assets/migrations/20260915160000_ec_sales_list.sql +181 -0
- package/dist/assets/migrations/20260915161842_pack_sources.sql +56 -0
- package/dist/assets/migrations/20260915170500_schema_version_0_3_0.sql +45 -0
- package/dist/assets/migrations/20260915174500_a_vat_category_is_a_code.sql +177 -0
- package/dist/assets/migrations/20260915180000_document_rule_references.sql +85 -0
- package/dist/assets/migrations/20260915181000_territories.sql +342 -0
- package/dist/assets/migrations/20260915181500_ec_sales_list_reads_the_territories.sql +222 -0
- package/dist/assets/migrations/20260915182000_intracom_triangular.sql +53 -0
- package/dist/assets/migrations/20260915182500_a_triangular_supply_says_reverse_charge.sql +99 -0
- package/dist/assets/migrations/20260915191200_a_document_knows_its_language.sql +633 -0
- package/dist/assets/migrations/20260915195000_a_price_that_holds_its_tax.sql +649 -0
- package/dist/assets/migrations/20260915200000_a_posting_names_its_boxes.sql +1004 -0
- package/dist/assets/migrations/20260916094500_a_company_files_more_than_one_declaration.sql +795 -0
- package/dist/assets/migrations/20260916103000_a_ledger_line_names_its_posting.sql +902 -0
- package/dist/assets/migrations/20260916123000_a_vocabulary_for_a_tax_that_is_not_a_vat.sql +144 -0
- package/dist/assets/migrations/20260916124000_a_box_can_be_a_rate_of_a_box.sql +525 -0
- package/dist/assets/migrations/20260916125000_a_tax_follows_the_territory.sql +1318 -0
- package/dist/assets/migrations/20260916126000_a_tax_point_and_its_exception.sql +1211 -0
- package/dist/assets/migrations/20260917090000_a_counterparty_that_learns.sql +561 -0
- package/dist/assets/migrations/20260917120000_what_the_money_pays.sql +545 -0
- package/dist/assets/migrations/20260917150000_a_filing_is_a_row.sql +488 -0
- package/dist/assets/migrations/20260917170000_when_it_is_due.sql +194 -0
- package/dist/assets/migrations/20260917180000_a_box_is_a_number_and_a_kind.sql +213 -0
- package/dist/assets/migrations/20260917190000_what_a_declaration_owes.sql +486 -0
- package/dist/assets/migrations/20260917200000_what_moved_after_it_went.sql +227 -0
- package/dist/assets/migrations/20260918090000_a_form_names_its_file.sql +27 -0
- package/dist/assets/migrations/20260918110000_a_deposit_and_what_came_back.sql +286 -0
- package/dist/assets/migrations/20260918113741_a_fourth_preset_is_named.sql +9 -0
- package/dist/assets/migrations/20260918113807_a_client_reads_and_hands_over.sql +96 -0
- package/dist/assets/migrations/20260918114322_two_definer_functions_check_their_caller.sql +75 -0
- package/dist/assets/migrations/20260918130000_every_company_somebody_keeps.sql +177 -0
- package/dist/assets/migrations/20260918140000_a_session_nobody_prepared.sql +40 -0
- package/dist/assets/migrations/20260918141107_a_line_keeps_the_tax_it_was_posted_with.sql +256 -0
- package/dist/assets/migrations/20260918141342_a_company_has_an_electronic_address.sql +66 -0
- package/dist/assets/migrations/20260918141605_an_invoice_reads_whole_from_the_views.sql +313 -0
- package/dist/assets/migrations/20260918141627_a_policy_asks_once.sql +132 -0
- package/dist/assets/migrations/20260918143352_a_suggestion_reads_the_words_once.sql +204 -0
- package/dist/assets/migrations/20260918143417_a_reference_of_the_caller_and_a_rehearsal.sql +115 -0
- package/dist/assets/migrations/20260918150712_a_company_leaves_with_its_books.sql +1119 -0
- package/dist/assets/migrations/20260918150931_a_statement_is_imported_once.sql +654 -0
- package/dist/assets/migrations/20260918161204_a_posted_document_does_not_move.sql +437 -0
- package/dist/assets/migrations/20260918161538_a_posted_entry_does_not_move.sql +252 -0
- package/dist/assets/migrations/20260918171946_an_entry_is_posted_by_post_entry.sql +289 -0
- package/dist/assets/migrations/20260918174312_schema_version_0_4_0.sql +53 -0
- package/dist/assets/modules/assets/module.json +13 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913081447_assets.sql +1190 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104234_foreign_key_indexes.sql +47 -0
- package/dist/assets/modules/assets/supabase/migrations/20260913104500_assets_capabilities.sql +145 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914122100_assets_round_at_the_currency.sql +564 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914145012_assets_accounts_in_use.sql +37 -0
- package/dist/assets/modules/assets/supabase/migrations/20260914151530_assets_grants_its_own_rights.sql +63 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918113912_assets_client_reads.sql +20 -0
- package/dist/assets/modules/assets/supabase/migrations/20260918150801_assets_leave_with_the_company.sql +41 -0
- package/dist/assets/modules/budgets/module.json +12 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913083012_budgets.sql +210 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104233_foreign_key_indexes.sql +37 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260913104501_budgets_capabilities.sql +72 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914122400_budgets_round_at_the_currency.sql +68 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914145048_budgets_accounts_in_use.sql +30 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260914151812_budgets_grants_its_own_rights.sql +31 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918113954_budgets_client_reads.sql +20 -0
- package/dist/assets/modules/budgets/supabase/migrations/20260918150823_budgets_leave_with_the_company.sql +37 -0
- package/dist/assets/modules/schema/module.1.json +69 -0
- package/dist/assets/seed/00_currencies.sql +15 -0
- package/dist/assets/seed/00_territories.sql +280 -0
- package/dist/assets/seed/05_framework_generic.sql +110 -0
- package/dist/assets/seed/10_pack_be.sql +1284 -0
- package/dist/assets/seed/11_pack_fr.sql +1112 -0
- package/dist/assets/seed/12_pack_lu.sql +2484 -0
- package/dist/assets/seed/13_pack_ee.sql +733 -0
- package/dist/assets/seed/14_pack_gb.sql +796 -0
- package/dist/assets/seed/15_pack_us.sql +751 -0
- package/dist/assets/seed/90_demo_company.sql +368 -0
- package/dist/assets/seed/modules/assets/10_pack_be.sql +58 -0
- package/dist/assets/seed/modules/assets/11_pack_fr.sql +57 -0
- package/dist/assets/seed/modules/assets/14_pack_gb.sql +58 -0
- package/dist/assets/seed/modules/assets/15_pack_us.sql +60 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +134 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +8 -0
- package/dist/bin.js.map +1 -0
- package/dist/books.d.ts +60 -0
- package/dist/books.d.ts.map +1 -0
- package/dist/books.js +168 -0
- package/dist/books.js.map +1 -0
- package/dist/bootstrap.d.ts +236 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +459 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bundle.d.ts +27 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +47 -0
- package/dist/bundle.js.map +1 -0
- package/dist/checklist.d.ts +35 -0
- package/dist/checklist.d.ts.map +1 -0
- package/dist/checklist.js +64 -0
- package/dist/checklist.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +318 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/company.d.ts +59 -0
- package/dist/commands/company.d.ts.map +1 -0
- package/dist/commands/company.js +287 -0
- package/dist/commands/company.js.map +1 -0
- package/dist/commands/contact.d.ts +9 -0
- package/dist/commands/contact.d.ts.map +1 -0
- package/dist/commands/contact.js +82 -0
- package/dist/commands/contact.js.map +1 -0
- package/dist/commands/demo.d.ts +19 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +72 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +68 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/document.d.ts +17 -0
- package/dist/commands/document.d.ts.map +1 -0
- package/dist/commands/document.js +217 -0
- package/dist/commands/document.js.map +1 -0
- package/dist/commands/init.d.ts +31 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +535 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +24 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +128 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/migrate.d.ts +30 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +115 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/module.d.ts +32 -0
- package/dist/commands/module.d.ts.map +1 -0
- package/dist/commands/module.js +285 -0
- package/dist/commands/module.js.map +1 -0
- package/dist/commands/pack.d.ts +22 -0
- package/dist/commands/pack.d.ts.map +1 -0
- package/dist/commands/pack.js +481 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/payment.d.ts +15 -0
- package/dist/commands/payment.d.ts.map +1 -0
- package/dist/commands/payment.js +97 -0
- package/dist/commands/payment.js.map +1 -0
- package/dist/commands/register.d.ts +20 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +134 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +134 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/whoami.d.ts +21 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +133 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/company.d.ts +16 -0
- package/dist/company.d.ts.map +1 -0
- package/dist/company.js +26 -0
- package/dist/company.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +50 -0
- package/dist/config.js.map +1 -0
- package/dist/connection.d.ts +79 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +116 -0
- package/dist/connection.js.map +1 -0
- package/dist/context.d.ts +47 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +93 -0
- package/dist/context.js.map +1 -0
- package/dist/doctor.d.ts +45 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +448 -0
- package/dist/doctor.js.map +1 -0
- package/dist/grants.d.ts +88 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +174 -0
- package/dist/grants.js.map +1 -0
- package/dist/identity.d.ts +54 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +132 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +155 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +303 -0
- package/dist/inventory.js.map +1 -0
- package/dist/migrations.d.ts +76 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +226 -0
- package/dist/migrations.js.map +1 -0
- package/dist/module/read.d.ts +91 -0
- package/dist/module/read.d.ts.map +1 -0
- package/dist/module/read.js +172 -0
- package/dist/module/read.js.map +1 -0
- package/dist/output.d.ts +110 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +173 -0
- package/dist/output.js.map +1 -0
- package/dist/pack/certification.d.ts +27 -0
- package/dist/pack/certification.d.ts.map +1 -0
- package/dist/pack/certification.js +38 -0
- package/dist/pack/certification.js.map +1 -0
- package/dist/pack/compile.d.ts +75 -0
- package/dist/pack/compile.d.ts.map +1 -0
- package/dist/pack/compile.js +801 -0
- package/dist/pack/compile.js.map +1 -0
- package/dist/pack/filing.d.ts +48 -0
- package/dist/pack/filing.d.ts.map +1 -0
- package/dist/pack/filing.js +56 -0
- package/dist/pack/filing.js.map +1 -0
- package/dist/pack/read.d.ts +766 -0
- package/dist/pack/read.d.ts.map +1 -0
- package/dist/pack/read.js +2336 -0
- package/dist/pack/read.js.map +1 -0
- package/dist/pack/schema.d.ts +26 -0
- package/dist/pack/schema.d.ts.map +1 -0
- package/dist/pack/schema.js +181 -0
- package/dist/pack/schema.js.map +1 -0
- package/dist/pack/territories.d.ts +98 -0
- package/dist/pack/territories.d.ts.map +1 -0
- package/dist/pack/territories.js +0 -0
- package/dist/pack/territories.js.map +1 -0
- package/dist/pack/upgrade.d.ts +86 -0
- package/dist/pack/upgrade.d.ts.map +1 -0
- package/dist/pack/upgrade.js +76 -0
- package/dist/pack/upgrade.js.map +1 -0
- package/dist/pack/vat-codes.d.ts +273 -0
- package/dist/pack/vat-codes.d.ts.map +1 -0
- package/dist/pack/vat-codes.js +513 -0
- package/dist/pack/vat-codes.js.map +1 -0
- package/dist/profiles.d.ts +67 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +187 -0
- package/dist/profiles.js.map +1 -0
- package/dist/prompt.d.ts +40 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +120 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +84 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +104 -0
- package/dist/registry.js.map +1 -0
- package/dist/rest.d.ts +81 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +245 -0
- package/dist/rest.js.map +1 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +15 -0
- package/dist/schema.js.map +1 -0
- package/dist/seeds.d.ts +27 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +47 -0
- package/dist/seeds.js.map +1 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +92 -0
- package/dist/session.js.map +1 -0
- package/dist/sql.d.ts +48 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +112 -0
- package/dist/sql.js.map +1 -0
- package/dist/status.d.ts +94 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +120 -0
- package/dist/status.js.map +1 -0
- package/dist/ui.d.ts +55 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +114 -0
- package/dist/ui.js.map +1 -0
- package/package.json +29 -0
- package/schema/output.1.json +613 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
-- Ekwo OS — a role is a preset, a capability is what a policy tests.
|
|
2
|
+
--
|
|
3
|
+
-- `company_members.role` carried three values and every policy in the schema
|
|
4
|
+
-- read one of them, through `is_company_member`, `can_write_company` and
|
|
5
|
+
-- `is_company_owner`. That is a permission model with three positions: a
|
|
6
|
+
-- bookkeeper who may post invoices and must never move a period lock has no
|
|
7
|
+
-- row to sit on, and the answer in every product that meets the case is a
|
|
8
|
+
-- fourth role, then a fifth.
|
|
9
|
+
--
|
|
10
|
+
-- So the role stays, as a **preset**, and what a policy tests becomes a
|
|
11
|
+
-- **capability**: a code from a fixed vocabulary, seeded by this migration,
|
|
12
|
+
-- that names one thing a person may do. `role_capabilities` says what each
|
|
13
|
+
-- preset holds. `company_members.capabilities_granted` and
|
|
14
|
+
-- `capabilities_revoked` adjust one member on top of their preset, in both
|
|
15
|
+
-- directions, and a revoke wins over a grant — including on an owner, because
|
|
16
|
+
-- a company that wants its owner unable to close a year is describing its own
|
|
17
|
+
-- separation of duties and not a mistake.
|
|
18
|
+
--
|
|
19
|
+
-- **One source of truth.** Every policy that read a role now calls
|
|
20
|
+
-- `has_capability()`, and the three published helpers are rewritten on top of
|
|
21
|
+
-- it rather than left beside it: `can_write_company()` is the answer to
|
|
22
|
+
-- `entries.write` and nothing else. A role is never tested by a policy again.
|
|
23
|
+
--
|
|
24
|
+
-- **What this migration does not do.** It does not consider an instance
|
|
25
|
+
-- administrator. Administering an installation is not being on the books of a
|
|
26
|
+
-- company — `docs/decisions.md` says so and a test asserts it — so the
|
|
27
|
+
-- policies that already admit an administrator keep saying it themselves,
|
|
28
|
+
-- beside the capability, and `has_capability()` answers about membership only.
|
|
29
|
+
--
|
|
30
|
+
-- **The door left open.** A module adds its own codes to `capabilities` with
|
|
31
|
+
-- `area` set to the module's code, and its own rows to `role_capabilities`.
|
|
32
|
+
-- Nothing here enumerates the areas, and nothing refuses a code it has not
|
|
33
|
+
-- seen.
|
|
34
|
+
|
|
35
|
+
-- ---------------------------------------------------------------------------
|
|
36
|
+
-- The vocabulary
|
|
37
|
+
-- ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
create table capabilities (
|
|
40
|
+
code text primary key,
|
|
41
|
+
area text not null,
|
|
42
|
+
description text not null,
|
|
43
|
+
constraint capabilities_code_shape check (code ~ '^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$')
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
comment on table capabilities is
|
|
47
|
+
'Everything a member may be allowed to do, one row per code. Seeded by this migration for the core; a module adds its own with `area` set to the module code.';
|
|
48
|
+
comment on column capabilities.area is
|
|
49
|
+
'What the code belongs to — a table family of the core, or the code of a module.';
|
|
50
|
+
|
|
51
|
+
-- Twenty codes, and every one of them is read by a policy or by a guard in
|
|
52
|
+
-- this file. Two that an earlier sketch carried are deliberately absent.
|
|
53
|
+
-- `reports.read` would have guarded `trial_balance()` and the statements,
|
|
54
|
+
-- which sum ledger lines row level security has already filtered: a member
|
|
55
|
+
-- without `entries.read` gets an empty report today, and a second lock on the
|
|
56
|
+
-- same door is a lock nobody turns. `exports.run` is the same argument about
|
|
57
|
+
-- the FEC. Neither is a capability until something can genuinely refuse on
|
|
58
|
+
-- it.
|
|
59
|
+
insert into capabilities (code, area, description) values
|
|
60
|
+
('documents.read', 'documents', 'Read invoices, credit notes, quotes and their lines.'),
|
|
61
|
+
('documents.write', 'documents', 'Create and change draft documents, their lines and their attachments.'),
|
|
62
|
+
('documents.post', 'documents', 'Book a document to the ledger. Cannot be undone.'),
|
|
63
|
+
('entries.read', 'entries', 'Read the ledger: entries, lines, matchings and the counters behind the numbering.'),
|
|
64
|
+
('entries.write', 'entries', 'Create and change draft entries and their lines.'),
|
|
65
|
+
('entries.post', 'entries', 'Post an entry, which draws its number. Cannot be undone.'),
|
|
66
|
+
('payments.read', 'payments', 'Read payments in and out.'),
|
|
67
|
+
('payments.write', 'payments', 'Record a payment and book it.'),
|
|
68
|
+
('reconcile.write', 'reconcile', 'Match a debit against a credit, and undo a matching.'),
|
|
69
|
+
('bank.read', 'bank', 'Read bank accounts, statements and statement lines.'),
|
|
70
|
+
('bank.write', 'bank', 'Add bank accounts and statement lines.'),
|
|
71
|
+
('contacts.read', 'contacts', 'Read customers, suppliers and other third parties.'),
|
|
72
|
+
('contacts.write', 'contacts', 'Create and change contacts.'),
|
|
73
|
+
('products.read', 'products', 'Read the catalogue.'),
|
|
74
|
+
('products.write', 'products', 'Create, change and retire catalogue items.'),
|
|
75
|
+
('settings.read', 'settings', 'Read the chart of accounts, the journals, the taxes, the analytic plan and the financial years.'),
|
|
76
|
+
('settings.write', 'settings', 'Change the chart of accounts, the journals, the taxes, the analytic plan and the financial years.'),
|
|
77
|
+
('company.write', 'company', 'Change the company itself: its profile, its default accounts and its period locks.'),
|
|
78
|
+
('members.manage', 'members', 'Invite, change and remove members, and issue machine access keys.'),
|
|
79
|
+
('year_end.close', 'year_end', 'Close and re-open a financial year.');
|
|
80
|
+
|
|
81
|
+
-- ---------------------------------------------------------------------------
|
|
82
|
+
-- The presets
|
|
83
|
+
--
|
|
84
|
+
-- The three roles keep exactly what they could do the day before this
|
|
85
|
+
-- migration, expressed in the new vocabulary: a viewer reads, an accountant
|
|
86
|
+
-- keeps the books, an owner also administers the company and its members.
|
|
87
|
+
--
|
|
88
|
+
-- There is no fourth role. `admin` was considered and left out: the existing
|
|
89
|
+
-- helpers drew one line — write the books, or administer the company — and
|
|
90
|
+
-- `owner` is already the second half of it, so a role between them would have
|
|
91
|
+
-- been a name with no work to do. A member who needs exactly that is an
|
|
92
|
+
-- accountant with `members.manage` granted, which is what the grant column is
|
|
93
|
+
-- for.
|
|
94
|
+
-- ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
create table role_capabilities (
|
|
97
|
+
role member_role not null,
|
|
98
|
+
capability text not null references capabilities(code) on delete cascade,
|
|
99
|
+
primary key (role, capability)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
comment on table role_capabilities is
|
|
103
|
+
'What each preset holds. A role is never tested by a policy; it is resolved here into capabilities.';
|
|
104
|
+
|
|
105
|
+
insert into role_capabilities (role, capability)
|
|
106
|
+
select 'viewer'::member_role, code from capabilities where code like '%.read';
|
|
107
|
+
|
|
108
|
+
insert into role_capabilities (role, capability)
|
|
109
|
+
select 'accountant'::member_role, code
|
|
110
|
+
from capabilities
|
|
111
|
+
where code like '%.read'
|
|
112
|
+
or code in ('documents.write', 'documents.post', 'entries.write', 'entries.post',
|
|
113
|
+
'payments.write', 'reconcile.write', 'bank.write', 'contacts.write',
|
|
114
|
+
'products.write', 'settings.write', 'year_end.close');
|
|
115
|
+
|
|
116
|
+
insert into role_capabilities (role, capability)
|
|
117
|
+
select 'owner'::member_role, code from capabilities;
|
|
118
|
+
|
|
119
|
+
-- ---------------------------------------------------------------------------
|
|
120
|
+
-- The adjustments, per member
|
|
121
|
+
-- ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
alter table company_members
|
|
124
|
+
add column if not exists capabilities_granted text[] not null default '{}',
|
|
125
|
+
add column if not exists capabilities_revoked text[] not null default '{}';
|
|
126
|
+
|
|
127
|
+
comment on column company_members.capabilities_granted is
|
|
128
|
+
'Capabilities this member holds beyond their preset.';
|
|
129
|
+
comment on column company_members.capabilities_revoked is
|
|
130
|
+
'Capabilities this member does not hold whatever their preset says. A revoke wins over a grant and over a role.';
|
|
131
|
+
|
|
132
|
+
-- A code that is not in `capabilities` is a typo, and a typo in a permission
|
|
133
|
+
-- column is a permission nobody notices is missing. A foreign key cannot
|
|
134
|
+
-- reach inside an array, so this is a trigger — and it reads the table, so a
|
|
135
|
+
-- module's codes are accepted the moment the module's migration adds them.
|
|
136
|
+
create or replace function company_members_capabilities_known()
|
|
137
|
+
returns trigger
|
|
138
|
+
language plpgsql
|
|
139
|
+
as $$
|
|
140
|
+
declare
|
|
141
|
+
v_unknown text;
|
|
142
|
+
begin
|
|
143
|
+
select c into v_unknown
|
|
144
|
+
from unnest(new.capabilities_granted || new.capabilities_revoked) as c
|
|
145
|
+
where c not in (select code from capabilities)
|
|
146
|
+
limit 1;
|
|
147
|
+
|
|
148
|
+
if v_unknown is not null then
|
|
149
|
+
raise exception 'unknown_capability: % is not a capability of this installation', v_unknown
|
|
150
|
+
using errcode = '23514';
|
|
151
|
+
end if;
|
|
152
|
+
|
|
153
|
+
return new;
|
|
154
|
+
end;
|
|
155
|
+
$$;
|
|
156
|
+
|
|
157
|
+
create trigger company_members_capabilities_known
|
|
158
|
+
before insert or update of capabilities_granted, capabilities_revoked on company_members
|
|
159
|
+
for each row execute function company_members_capabilities_known();
|
|
160
|
+
|
|
161
|
+
-- ---------------------------------------------------------------------------
|
|
162
|
+
-- The one question every policy asks
|
|
163
|
+
--
|
|
164
|
+
-- SECURITY DEFINER for the reason `company_role()` is: the policy on
|
|
165
|
+
-- `company_members` must not recurse into the table it protects. It answers
|
|
166
|
+
-- about `auth.uid()` and about membership only — an instance administrator is
|
|
167
|
+
-- not a member, and the policies that admit one say so themselves.
|
|
168
|
+
-- ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
create or replace function has_capability(p_company_id uuid, p_capability text)
|
|
171
|
+
returns boolean
|
|
172
|
+
language sql
|
|
173
|
+
stable
|
|
174
|
+
security definer
|
|
175
|
+
set search_path = public, pg_temp
|
|
176
|
+
as $$
|
|
177
|
+
select coalesce(
|
|
178
|
+
(select case
|
|
179
|
+
when p_capability = any (m.capabilities_revoked) then false
|
|
180
|
+
when p_capability = any (m.capabilities_granted) then true
|
|
181
|
+
else exists (
|
|
182
|
+
select 1 from role_capabilities rc
|
|
183
|
+
where rc.role = m.role and rc.capability = p_capability
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
from company_members m
|
|
187
|
+
where m.company_id = p_company_id
|
|
188
|
+
and m.user_id = auth.uid()),
|
|
189
|
+
false);
|
|
190
|
+
$$;
|
|
191
|
+
|
|
192
|
+
comment on function has_capability(uuid, text) is
|
|
193
|
+
'Whether the current user may do one named thing in one company. Revoked beats granted, granted beats the preset, and a non-member holds nothing.';
|
|
194
|
+
|
|
195
|
+
-- What a member may do, for an interface that has to draw it — and, when the
|
|
196
|
+
-- caller may manage members, what somebody else may do.
|
|
197
|
+
create or replace function member_capabilities(p_company_id uuid, p_user_id uuid default null)
|
|
198
|
+
returns setof text
|
|
199
|
+
language plpgsql
|
|
200
|
+
stable
|
|
201
|
+
security definer
|
|
202
|
+
set search_path = public, pg_temp
|
|
203
|
+
as $$
|
|
204
|
+
declare
|
|
205
|
+
v_user uuid := coalesce(p_user_id, auth.uid());
|
|
206
|
+
begin
|
|
207
|
+
if v_user is distinct from auth.uid() and not has_capability(p_company_id, 'members.manage') then
|
|
208
|
+
raise exception 'not_allowed: reading what another member may do needs members.manage'
|
|
209
|
+
using errcode = '42501';
|
|
210
|
+
end if;
|
|
211
|
+
|
|
212
|
+
return query
|
|
213
|
+
select c.code
|
|
214
|
+
from capabilities c
|
|
215
|
+
join company_members m
|
|
216
|
+
on m.company_id = p_company_id and m.user_id = v_user
|
|
217
|
+
where not (c.code = any (m.capabilities_revoked))
|
|
218
|
+
and (c.code = any (m.capabilities_granted)
|
|
219
|
+
or exists (select 1 from role_capabilities rc
|
|
220
|
+
where rc.role = m.role and rc.capability = c.code))
|
|
221
|
+
order by c.code;
|
|
222
|
+
end;
|
|
223
|
+
$$;
|
|
224
|
+
|
|
225
|
+
comment on function member_capabilities(uuid, uuid) is
|
|
226
|
+
'The capabilities one member effectively holds on one company, preset and adjustments resolved. Reading another member''s needs members.manage.';
|
|
227
|
+
|
|
228
|
+
-- ---------------------------------------------------------------------------
|
|
229
|
+
-- The published helpers, rewritten on top of the one question
|
|
230
|
+
--
|
|
231
|
+
-- They keep their names and their meaning, and they stop being a second place
|
|
232
|
+
-- where a permission is decided. `is_company_member` stays a membership test:
|
|
233
|
+
-- belonging to a company is not a capability, it is what makes capabilities
|
|
234
|
+
-- possible. `is_company_owner` stays the literal role, for a caller that wants
|
|
235
|
+
-- to know which preset somebody is on — no policy tests it any more.
|
|
236
|
+
-- ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
create or replace function can_write_company(p_company_id uuid)
|
|
239
|
+
returns boolean
|
|
240
|
+
language sql
|
|
241
|
+
stable
|
|
242
|
+
as $$
|
|
243
|
+
select has_capability(p_company_id, 'entries.write');
|
|
244
|
+
$$;
|
|
245
|
+
|
|
246
|
+
comment on function can_write_company(uuid) is
|
|
247
|
+
'Whether the current user may write the books of a company. Kept for callers that have it; it is now one capability and not a role.';
|
|
248
|
+
|
|
249
|
+
-- ---------------------------------------------------------------------------
|
|
250
|
+
-- Every policy that tested a role now tests a capability
|
|
251
|
+
-- ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
-- The company itself, its members and its financial years.
|
|
254
|
+
drop policy companies_update on companies;
|
|
255
|
+
create policy companies_update on companies
|
|
256
|
+
for update using (has_capability(id, 'company.write') or is_instance_admin())
|
|
257
|
+
with check (has_capability(id, 'company.write') or is_instance_admin());
|
|
258
|
+
|
|
259
|
+
drop policy companies_delete on companies;
|
|
260
|
+
create policy companies_delete on companies
|
|
261
|
+
for delete using (has_capability(id, 'company.write'));
|
|
262
|
+
|
|
263
|
+
drop policy company_members_insert on company_members;
|
|
264
|
+
create policy company_members_insert on company_members
|
|
265
|
+
for insert with check (
|
|
266
|
+
has_capability(company_id, 'members.manage')
|
|
267
|
+
or is_instance_admin()
|
|
268
|
+
or company_has_no_member(company_id)
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
drop policy company_members_update on company_members;
|
|
272
|
+
create policy company_members_update on company_members
|
|
273
|
+
for update using (has_capability(company_id, 'members.manage'))
|
|
274
|
+
with check (has_capability(company_id, 'members.manage'));
|
|
275
|
+
|
|
276
|
+
drop policy company_members_delete on company_members;
|
|
277
|
+
create policy company_members_delete on company_members
|
|
278
|
+
for delete using (has_capability(company_id, 'members.manage'));
|
|
279
|
+
|
|
280
|
+
drop policy fiscal_years_select on fiscal_years;
|
|
281
|
+
create policy fiscal_years_select on fiscal_years
|
|
282
|
+
for select using (has_capability(company_id, 'settings.read'));
|
|
283
|
+
drop policy fiscal_years_write on fiscal_years;
|
|
284
|
+
create policy fiscal_years_write on fiscal_years
|
|
285
|
+
for all using (has_capability(company_id, 'settings.write'))
|
|
286
|
+
with check (has_capability(company_id, 'settings.write'));
|
|
287
|
+
|
|
288
|
+
-- The chart, the journals, the taxes, the analytic plan and what the company
|
|
289
|
+
-- copied from a pack: one family, `settings`.
|
|
290
|
+
drop policy accounts_select on accounts;
|
|
291
|
+
create policy accounts_select on accounts
|
|
292
|
+
for select using (has_capability(company_id, 'settings.read'));
|
|
293
|
+
drop policy accounts_write on accounts;
|
|
294
|
+
create policy accounts_write on accounts
|
|
295
|
+
for all using (has_capability(company_id, 'settings.write'))
|
|
296
|
+
with check (has_capability(company_id, 'settings.write'));
|
|
297
|
+
|
|
298
|
+
drop policy journals_select on journals;
|
|
299
|
+
create policy journals_select on journals
|
|
300
|
+
for select using (has_capability(company_id, 'settings.read'));
|
|
301
|
+
drop policy journals_write on journals;
|
|
302
|
+
create policy journals_write on journals
|
|
303
|
+
for all using (has_capability(company_id, 'settings.write'))
|
|
304
|
+
with check (has_capability(company_id, 'settings.write'));
|
|
305
|
+
|
|
306
|
+
drop policy taxes_select on taxes;
|
|
307
|
+
create policy taxes_select on taxes
|
|
308
|
+
for select using (has_capability(company_id, 'settings.read'));
|
|
309
|
+
drop policy taxes_write on taxes;
|
|
310
|
+
create policy taxes_write on taxes
|
|
311
|
+
for all using (has_capability(company_id, 'settings.write'))
|
|
312
|
+
with check (has_capability(company_id, 'settings.write'));
|
|
313
|
+
|
|
314
|
+
drop policy tax_postings_select on tax_postings;
|
|
315
|
+
create policy tax_postings_select on tax_postings
|
|
316
|
+
for select using (has_capability(company_id, 'settings.read'));
|
|
317
|
+
drop policy tax_postings_write on tax_postings;
|
|
318
|
+
create policy tax_postings_write on tax_postings
|
|
319
|
+
for all using (has_capability(company_id, 'settings.write'))
|
|
320
|
+
with check (has_capability(company_id, 'settings.write'));
|
|
321
|
+
|
|
322
|
+
drop policy analytic_axes_select on analytic_axes;
|
|
323
|
+
create policy analytic_axes_select on analytic_axes
|
|
324
|
+
for select using (has_capability(company_id, 'settings.read'));
|
|
325
|
+
drop policy analytic_axes_write on analytic_axes;
|
|
326
|
+
create policy analytic_axes_write on analytic_axes
|
|
327
|
+
for all using (has_capability(company_id, 'settings.write'))
|
|
328
|
+
with check (has_capability(company_id, 'settings.write'));
|
|
329
|
+
|
|
330
|
+
drop policy analytic_values_select on analytic_values;
|
|
331
|
+
create policy analytic_values_select on analytic_values
|
|
332
|
+
for select using (has_capability(company_id, 'settings.read'));
|
|
333
|
+
drop policy analytic_values_write on analytic_values;
|
|
334
|
+
create policy analytic_values_write on analytic_values
|
|
335
|
+
for all using (has_capability(company_id, 'settings.write'))
|
|
336
|
+
with check (has_capability(company_id, 'settings.write'));
|
|
337
|
+
|
|
338
|
+
drop policy company_packs_select on company_packs;
|
|
339
|
+
create policy company_packs_select on company_packs
|
|
340
|
+
for select using (has_capability(company_id, 'settings.read'));
|
|
341
|
+
drop policy company_packs_write on company_packs;
|
|
342
|
+
create policy company_packs_write on company_packs
|
|
343
|
+
for all using (has_capability(company_id, 'settings.write'))
|
|
344
|
+
with check (has_capability(company_id, 'settings.write'));
|
|
345
|
+
|
|
346
|
+
-- Contacts and the catalogue.
|
|
347
|
+
drop policy contacts_select on contacts;
|
|
348
|
+
create policy contacts_select on contacts
|
|
349
|
+
for select using (has_capability(company_id, 'contacts.read'));
|
|
350
|
+
drop policy contacts_write on contacts;
|
|
351
|
+
create policy contacts_write on contacts
|
|
352
|
+
for all using (has_capability(company_id, 'contacts.write'))
|
|
353
|
+
with check (has_capability(company_id, 'contacts.write'));
|
|
354
|
+
|
|
355
|
+
drop policy products_select on products;
|
|
356
|
+
create policy products_select on products
|
|
357
|
+
for select using (has_capability(company_id, 'products.read'));
|
|
358
|
+
drop policy products_write on products;
|
|
359
|
+
create policy products_write on products
|
|
360
|
+
for all using (has_capability(company_id, 'products.write'))
|
|
361
|
+
with check (has_capability(company_id, 'products.write'));
|
|
362
|
+
|
|
363
|
+
-- Documents, their lines and what is filed with them.
|
|
364
|
+
drop policy documents_select on documents;
|
|
365
|
+
create policy documents_select on documents
|
|
366
|
+
for select using (has_capability(company_id, 'documents.read'));
|
|
367
|
+
drop policy documents_write on documents;
|
|
368
|
+
create policy documents_write on documents
|
|
369
|
+
for all using (has_capability(company_id, 'documents.write'))
|
|
370
|
+
with check (has_capability(company_id, 'documents.write'));
|
|
371
|
+
|
|
372
|
+
drop policy document_lines_select on document_lines;
|
|
373
|
+
create policy document_lines_select on document_lines
|
|
374
|
+
for select using (has_capability(company_id, 'documents.read'));
|
|
375
|
+
drop policy document_lines_write on document_lines;
|
|
376
|
+
create policy document_lines_write on document_lines
|
|
377
|
+
for all using (has_capability(company_id, 'documents.write'))
|
|
378
|
+
with check (has_capability(company_id, 'documents.write'));
|
|
379
|
+
|
|
380
|
+
drop policy attachments_select on attachments;
|
|
381
|
+
create policy attachments_select on attachments
|
|
382
|
+
for select using (has_capability(company_id, 'documents.read'));
|
|
383
|
+
drop policy attachments_write on attachments;
|
|
384
|
+
create policy attachments_write on attachments
|
|
385
|
+
for all using (has_capability(company_id, 'documents.write'))
|
|
386
|
+
with check (has_capability(company_id, 'documents.write'));
|
|
387
|
+
|
|
388
|
+
-- The ledger, its analytics and the counters behind its numbering.
|
|
389
|
+
drop policy entries_select on entries;
|
|
390
|
+
create policy entries_select on entries
|
|
391
|
+
for select using (has_capability(company_id, 'entries.read'));
|
|
392
|
+
drop policy entries_write on entries;
|
|
393
|
+
create policy entries_write on entries
|
|
394
|
+
for all using (has_capability(company_id, 'entries.write'))
|
|
395
|
+
with check (has_capability(company_id, 'entries.write'));
|
|
396
|
+
|
|
397
|
+
drop policy entry_lines_select on entry_lines;
|
|
398
|
+
create policy entry_lines_select on entry_lines
|
|
399
|
+
for select using (has_capability(company_id, 'entries.read'));
|
|
400
|
+
drop policy entry_lines_write on entry_lines;
|
|
401
|
+
create policy entry_lines_write on entry_lines
|
|
402
|
+
for all using (has_capability(company_id, 'entries.write'))
|
|
403
|
+
with check (has_capability(company_id, 'entries.write'));
|
|
404
|
+
|
|
405
|
+
drop policy entry_line_analytics_select on entry_line_analytics;
|
|
406
|
+
create policy entry_line_analytics_select on entry_line_analytics
|
|
407
|
+
for select using (has_capability(company_id, 'entries.read'));
|
|
408
|
+
drop policy entry_line_analytics_write on entry_line_analytics;
|
|
409
|
+
create policy entry_line_analytics_write on entry_line_analytics
|
|
410
|
+
for all using (has_capability(company_id, 'entries.write'))
|
|
411
|
+
with check (has_capability(company_id, 'entries.write'));
|
|
412
|
+
|
|
413
|
+
drop policy journal_sequences_select on journal_sequences;
|
|
414
|
+
create policy journal_sequences_select on journal_sequences
|
|
415
|
+
for select using (
|
|
416
|
+
exists (select 1 from journals j
|
|
417
|
+
where j.id = journal_id and has_capability(j.company_id, 'entries.read'))
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
drop policy matching_sequences_select on matching_sequences;
|
|
421
|
+
create policy matching_sequences_select on matching_sequences
|
|
422
|
+
for select using (has_capability(company_id, 'entries.read'));
|
|
423
|
+
|
|
424
|
+
-- Money, and what it settles.
|
|
425
|
+
drop policy payments_select on payments;
|
|
426
|
+
create policy payments_select on payments
|
|
427
|
+
for select using (has_capability(company_id, 'payments.read'));
|
|
428
|
+
drop policy payments_write on payments;
|
|
429
|
+
create policy payments_write on payments
|
|
430
|
+
for all using (has_capability(company_id, 'payments.write'))
|
|
431
|
+
with check (has_capability(company_id, 'payments.write'));
|
|
432
|
+
|
|
433
|
+
drop policy reconciliations_select on reconciliations;
|
|
434
|
+
create policy reconciliations_select on reconciliations
|
|
435
|
+
for select using (has_capability(company_id, 'entries.read'));
|
|
436
|
+
drop policy reconciliations_write on reconciliations;
|
|
437
|
+
create policy reconciliations_write on reconciliations
|
|
438
|
+
for all using (has_capability(company_id, 'reconcile.write'))
|
|
439
|
+
with check (has_capability(company_id, 'reconcile.write'));
|
|
440
|
+
|
|
441
|
+
drop policy bank_accounts_select on bank_accounts;
|
|
442
|
+
create policy bank_accounts_select on bank_accounts
|
|
443
|
+
for select using (has_capability(company_id, 'bank.read'));
|
|
444
|
+
drop policy bank_accounts_write on bank_accounts;
|
|
445
|
+
create policy bank_accounts_write on bank_accounts
|
|
446
|
+
for all using (has_capability(company_id, 'bank.write'))
|
|
447
|
+
with check (has_capability(company_id, 'bank.write'));
|
|
448
|
+
|
|
449
|
+
drop policy bank_statements_select on bank_statements;
|
|
450
|
+
create policy bank_statements_select on bank_statements
|
|
451
|
+
for select using (has_capability(company_id, 'bank.read'));
|
|
452
|
+
drop policy bank_statements_write on bank_statements;
|
|
453
|
+
create policy bank_statements_write on bank_statements
|
|
454
|
+
for all using (has_capability(company_id, 'bank.write'))
|
|
455
|
+
with check (has_capability(company_id, 'bank.write'));
|
|
456
|
+
|
|
457
|
+
drop policy bank_transactions_select on bank_transactions;
|
|
458
|
+
create policy bank_transactions_select on bank_transactions
|
|
459
|
+
for select using (has_capability(company_id, 'bank.read'));
|
|
460
|
+
drop policy bank_transactions_write on bank_transactions;
|
|
461
|
+
create policy bank_transactions_write on bank_transactions
|
|
462
|
+
for all using (has_capability(company_id, 'bank.write'))
|
|
463
|
+
with check (has_capability(company_id, 'bank.write'));
|
|
464
|
+
|
|
465
|
+
-- ---------------------------------------------------------------------------
|
|
466
|
+
-- Posting and closing are acts, not rows
|
|
467
|
+
--
|
|
468
|
+
-- `documents.post`, `entries.post` and `year_end.close` name three things a
|
|
469
|
+
-- policy on a table cannot express: what changes is a state, and the row is
|
|
470
|
+
-- one the member may already write. They are guarded by triggers on the
|
|
471
|
+
-- transition rather than inside `post_document()`, `post_entry()` and
|
|
472
|
+
-- `close_fiscal_year()` — a guard on the transition holds for every path into
|
|
473
|
+
-- it, including a client that updates the column itself, and it does not
|
|
474
|
+
-- require republishing a function that carries a thousand lines of accounting
|
|
475
|
+
-- for four lines of permission.
|
|
476
|
+
--
|
|
477
|
+
-- `auth.uid() is null` is the installer and the migrations, where row level
|
|
478
|
+
-- security is bypassed rather than satisfied; the same exemption
|
|
479
|
+
-- `next_entry_number()` makes.
|
|
480
|
+
-- ---------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
create or replace function assert_may_post_entry()
|
|
483
|
+
returns trigger
|
|
484
|
+
language plpgsql
|
|
485
|
+
as $$
|
|
486
|
+
begin
|
|
487
|
+
if auth.uid() is not null and not has_capability(new.company_id, 'entries.post') then
|
|
488
|
+
raise exception 'not_allowed: posting an entry in this company needs entries.post'
|
|
489
|
+
using errcode = '42501';
|
|
490
|
+
end if;
|
|
491
|
+
return new;
|
|
492
|
+
end;
|
|
493
|
+
$$;
|
|
494
|
+
|
|
495
|
+
-- Two triggers rather than one: the WHEN clause of an INSERT trigger cannot
|
|
496
|
+
-- read OLD, so the transition and the arrival are written separately.
|
|
497
|
+
create trigger entries_assert_may_post_on_insert
|
|
498
|
+
before insert on entries
|
|
499
|
+
for each row when (new.state = 'posted')
|
|
500
|
+
execute function assert_may_post_entry();
|
|
501
|
+
|
|
502
|
+
create trigger entries_assert_may_post
|
|
503
|
+
before update of state on entries
|
|
504
|
+
for each row when (new.state = 'posted' and old.state is distinct from 'posted')
|
|
505
|
+
execute function assert_may_post_entry();
|
|
506
|
+
|
|
507
|
+
create or replace function assert_may_post_document()
|
|
508
|
+
returns trigger
|
|
509
|
+
language plpgsql
|
|
510
|
+
as $$
|
|
511
|
+
begin
|
|
512
|
+
if auth.uid() is not null and not has_capability(new.company_id, 'documents.post') then
|
|
513
|
+
raise exception 'not_allowed: booking a document in this company needs documents.post'
|
|
514
|
+
using errcode = '42501';
|
|
515
|
+
end if;
|
|
516
|
+
return new;
|
|
517
|
+
end;
|
|
518
|
+
$$;
|
|
519
|
+
|
|
520
|
+
create trigger documents_assert_may_post_on_insert
|
|
521
|
+
before insert on documents
|
|
522
|
+
for each row when (new.state = 'posted')
|
|
523
|
+
execute function assert_may_post_document();
|
|
524
|
+
|
|
525
|
+
create trigger documents_assert_may_post
|
|
526
|
+
before update of state on documents
|
|
527
|
+
for each row when (new.state = 'posted' and old.state is distinct from 'posted')
|
|
528
|
+
execute function assert_may_post_document();
|
|
529
|
+
|
|
530
|
+
create or replace function assert_may_close_year()
|
|
531
|
+
returns trigger
|
|
532
|
+
language plpgsql
|
|
533
|
+
as $$
|
|
534
|
+
begin
|
|
535
|
+
if auth.uid() is not null and not has_capability(new.company_id, 'year_end.close') then
|
|
536
|
+
raise exception 'not_allowed: closing or re-opening a financial year needs year_end.close'
|
|
537
|
+
using errcode = '42501';
|
|
538
|
+
end if;
|
|
539
|
+
return new;
|
|
540
|
+
end;
|
|
541
|
+
$$;
|
|
542
|
+
|
|
543
|
+
create trigger fiscal_years_assert_may_close
|
|
544
|
+
before update of is_closed on fiscal_years
|
|
545
|
+
for each row
|
|
546
|
+
when (new.is_closed is distinct from old.is_closed)
|
|
547
|
+
execute function assert_may_close_year();
|
|
548
|
+
|
|
549
|
+
-- ---------------------------------------------------------------------------
|
|
550
|
+
-- What the counters ask before they hand out a number
|
|
551
|
+
-- ---------------------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
create or replace function next_entry_number(p_journal_id uuid, p_date date)
|
|
554
|
+
returns text
|
|
555
|
+
language plpgsql
|
|
556
|
+
security definer
|
|
557
|
+
set search_path = public, pg_temp
|
|
558
|
+
as $$
|
|
559
|
+
declare
|
|
560
|
+
v_code text;
|
|
561
|
+
v_company uuid;
|
|
562
|
+
v_year smallint := extract(year from p_date)::smallint;
|
|
563
|
+
v_number integer;
|
|
564
|
+
begin
|
|
565
|
+
select j.code, j.company_id into v_code, v_company from journals j where j.id = p_journal_id;
|
|
566
|
+
if v_code is null then
|
|
567
|
+
raise exception 'unknown_journal: journal % does not exist', p_journal_id;
|
|
568
|
+
end if;
|
|
569
|
+
|
|
570
|
+
if auth.uid() is not null and not has_capability(v_company, 'entries.post') then
|
|
571
|
+
raise exception 'not_allowed: drawing a number in this company needs entries.post'
|
|
572
|
+
using errcode = '42501';
|
|
573
|
+
end if;
|
|
574
|
+
|
|
575
|
+
insert into journal_sequences (journal_id, year, last_number)
|
|
576
|
+
values (p_journal_id, v_year, 1)
|
|
577
|
+
on conflict (journal_id, year)
|
|
578
|
+
do update set last_number = journal_sequences.last_number + 1
|
|
579
|
+
returning last_number into v_number;
|
|
580
|
+
|
|
581
|
+
return v_code || '/' || v_year::text || '/' || lpad(v_number::text, 4, '0');
|
|
582
|
+
end;
|
|
583
|
+
$$;
|
|
584
|
+
|
|
585
|
+
create or replace function next_matching_number(p_company_id uuid)
|
|
586
|
+
returns text
|
|
587
|
+
language plpgsql
|
|
588
|
+
security definer
|
|
589
|
+
set search_path = public, pg_temp
|
|
590
|
+
as $$
|
|
591
|
+
declare
|
|
592
|
+
v_number integer;
|
|
593
|
+
begin
|
|
594
|
+
if auth.uid() is not null and not has_capability(p_company_id, 'reconcile.write') then
|
|
595
|
+
raise exception 'not_allowed: matching in this company needs reconcile.write'
|
|
596
|
+
using errcode = '42501';
|
|
597
|
+
end if;
|
|
598
|
+
|
|
599
|
+
insert into matching_sequences (company_id, last_number)
|
|
600
|
+
values (p_company_id, 1)
|
|
601
|
+
on conflict (company_id)
|
|
602
|
+
do update set last_number = matching_sequences.last_number + 1
|
|
603
|
+
returning last_number into v_number;
|
|
604
|
+
|
|
605
|
+
return 'A' || lpad(v_number::text, 4, '0');
|
|
606
|
+
end;
|
|
607
|
+
$$;
|
|
608
|
+
|
|
609
|
+
-- ---------------------------------------------------------------------------
|
|
610
|
+
-- Row level security on the two new tables
|
|
611
|
+
--
|
|
612
|
+
-- The vocabulary and the presets are reference data: anyone signed in reads
|
|
613
|
+
-- them — an interface has to draw the list — and nobody writes them through
|
|
614
|
+
-- the API. A migration is the only thing that fills them.
|
|
615
|
+
-- ---------------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
alter table capabilities enable row level security;
|
|
618
|
+
alter table role_capabilities enable row level security;
|
|
619
|
+
|
|
620
|
+
create policy capabilities_select on capabilities
|
|
621
|
+
for select using (auth.uid() is not null);
|
|
622
|
+
create policy role_capabilities_select on role_capabilities
|
|
623
|
+
for select using (auth.uid() is not null);
|
|
624
|
+
|
|
625
|
+
-- ---------------------------------------------------------------------------
|
|
626
|
+
-- Grants
|
|
627
|
+
--
|
|
628
|
+
-- `has_capability` is called by policies, so `anon` needs EXECUTE on it for
|
|
629
|
+
-- the same reason as the eight helpers before it: without it an anonymous
|
|
630
|
+
-- SELECT raises "permission denied for function" instead of returning
|
|
631
|
+
-- nothing. It answers about `auth.uid()`, which is null for `anon`, so what
|
|
632
|
+
-- it gives away is the word no. `member_capabilities` is not a policy helper
|
|
633
|
+
-- and stays closed.
|
|
634
|
+
-- ---------------------------------------------------------------------------
|
|
635
|
+
|
|
636
|
+
revoke execute on all functions in schema public from public;
|
|
637
|
+
grant execute on function has_capability(uuid, text) to anon;
|