kryptheon-night 0.1.26 → 0.1.29
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 +1 -1
- package/attack.js +63 -2
- package/bin/kryptheon-night.js +3 -1
- package/collision.js +15 -0
- package/engine.sql +365 -63
- package/finding.js +84 -18
- package/package.json +1 -1
- package/recheck.js +135 -14
- package/scan.js +23 -5
- package/sqlengine.js +4 -4
- package/tamper.js +243 -52
package/engine.sql
CHANGED
|
@@ -1625,6 +1625,54 @@ END $$;
|
|
|
1625
1625
|
* and `implied`, the keys the app never declared, so a `workspace_id` with no
|
|
1626
1626
|
* foreign key still names a workspace that is there.
|
|
1627
1627
|
*/
|
|
1628
|
+
/*
|
|
1629
|
+
* Sets of columns where exactly one may be filled in. Mirrors exactlyOne in
|
|
1630
|
+
* attack.js: (a IS NOT NULL)::int + (b IS NOT NULL)::int = 1, and
|
|
1631
|
+
* num_nonnulls(a, b) = 1, read only when the constraint says nothing else.
|
|
1632
|
+
* Found on a blind test (HelixOps), where documents pointed at a work order
|
|
1633
|
+
* or an inspection, the seeder pointed it at both, and it was never tested.
|
|
1634
|
+
*/
|
|
1635
|
+
CREATE OR REPLACE FUNCTION __KN__.exactly_one(tab jsonb)
|
|
1636
|
+
RETURNS jsonb LANGUAGE plpgsql IMMUTABLE AS $$
|
|
1637
|
+
DECLARE
|
|
1638
|
+
name_ constant text := '("(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*)';
|
|
1639
|
+
summed text := '\(\(' || name_ || ' IS NOT NULL\)\)::integer';
|
|
1640
|
+
groups jsonb := '[]'::jsonb;
|
|
1641
|
+
con jsonb;
|
|
1642
|
+
definition text;
|
|
1643
|
+
rest text;
|
|
1644
|
+
counted text[];
|
|
1645
|
+
columns jsonb;
|
|
1646
|
+
found text;
|
|
1647
|
+
BEGIN
|
|
1648
|
+
FOR con IN SELECT * FROM jsonb_array_elements(coalesce(tab->'constraints', '[]'::jsonb)) LOOP
|
|
1649
|
+
CONTINUE WHEN con->>'kind' <> 'c';
|
|
1650
|
+
definition := coalesce(con->>'definition', '');
|
|
1651
|
+
columns := '[]'::jsonb;
|
|
1652
|
+
FOR found IN SELECT (m)[1] FROM regexp_matches(definition, summed, 'gi') m LOOP
|
|
1653
|
+
columns := columns || to_jsonb(found);
|
|
1654
|
+
END LOOP;
|
|
1655
|
+
rest := replace(regexp_replace(definition, summed, '', 'gi'), '+', '');
|
|
1656
|
+
IF jsonb_array_length(columns) = 0 THEN
|
|
1657
|
+
counted := regexp_match(definition, 'num_nonnulls\(([^()]*)\)', 'i');
|
|
1658
|
+
IF counted IS NOT NULL THEN
|
|
1659
|
+
FOR found IN SELECT (m)[1] FROM regexp_matches(counted[1], name_, 'g') m LOOP
|
|
1660
|
+
columns := columns || to_jsonb(found);
|
|
1661
|
+
END LOOP;
|
|
1662
|
+
rest := regexp_replace(rest, 'num_nonnulls\([^()]*\)', '', 'i');
|
|
1663
|
+
END IF;
|
|
1664
|
+
END IF;
|
|
1665
|
+
-- Nothing may be left but the CHECK itself, brackets and "= 1".
|
|
1666
|
+
CONTINUE WHEN jsonb_array_length(columns) < 2
|
|
1667
|
+
OR upper(regexp_replace(rest, '[()\s]', '', 'g')) <> 'CHECK=1';
|
|
1668
|
+
groups := groups || jsonb_build_array((
|
|
1669
|
+
SELECT jsonb_agg(CASE WHEN left(c, 1) = '"' THEN replace(substr(c, 2, length(c) - 2), '""', '"') ELSE c END
|
|
1670
|
+
ORDER BY at)
|
|
1671
|
+
FROM jsonb_array_elements_text(columns) WITH ORDINALITY AS e(c, at)));
|
|
1672
|
+
END LOOP;
|
|
1673
|
+
RETURN groups;
|
|
1674
|
+
END $$;
|
|
1675
|
+
|
|
1628
1676
|
CREATE OR REPLACE FUNCTION __KN__.row_for(
|
|
1629
1677
|
source text, tab jsonb, person text, distinct_ text, overrides jsonb, attempt integer,
|
|
1630
1678
|
seeding jsonb DEFAULT NULL)
|
|
@@ -1636,6 +1684,10 @@ DECLARE
|
|
|
1636
1684
|
values_ jsonb := '[]'::jsonb;
|
|
1637
1685
|
borrowed jsonb := '{}'::jsonb;
|
|
1638
1686
|
part_of_key jsonb := '[]'::jsonb;
|
|
1687
|
+
only_one jsonb := '{}'::jsonb;
|
|
1688
|
+
group_ jsonb;
|
|
1689
|
+
kept text;
|
|
1690
|
+
must_fill boolean;
|
|
1639
1691
|
key jsonb;
|
|
1640
1692
|
row_ jsonb;
|
|
1641
1693
|
col jsonb;
|
|
@@ -1664,6 +1716,22 @@ BEGIN
|
|
|
1664
1716
|
part_of_key := part_of_key || (key->'columns');
|
|
1665
1717
|
END LOOP;
|
|
1666
1718
|
|
|
1719
|
+
-- Where exactly one of a set may be filled in: the one already chosen, or
|
|
1720
|
+
-- else the first that can be - a key needs a parent row to point at. The
|
|
1721
|
+
-- rest are written as nothing, whatever their default.
|
|
1722
|
+
FOR group_ IN SELECT * FROM jsonb_array_elements(__KN__.exactly_one(tab)) LOOP
|
|
1723
|
+
SELECT c INTO kept FROM jsonb_array_elements_text(group_) WITH ORDINALITY AS e(c, pos)
|
|
1724
|
+
WHERE forced ? c ORDER BY pos LIMIT 1;
|
|
1725
|
+
IF kept IS NULL THEN
|
|
1726
|
+
SELECT c INTO kept FROM jsonb_array_elements_text(group_) WITH ORDINALITY AS e(c, pos)
|
|
1727
|
+
WHERE NOT (part_of_key ? c) OR borrowed ? c ORDER BY pos LIMIT 1;
|
|
1728
|
+
END IF;
|
|
1729
|
+
FOR name IN SELECT jsonb_array_elements_text(group_) LOOP
|
|
1730
|
+
only_one := jsonb_set(only_one, ARRAY[name], to_jsonb(name = kept));
|
|
1731
|
+
END LOOP;
|
|
1732
|
+
kept := NULL;
|
|
1733
|
+
END LOOP;
|
|
1734
|
+
|
|
1667
1735
|
FOR col IN SELECT * FROM jsonb_array_elements(tab->'columns') LOOP
|
|
1668
1736
|
name := col->>'name';
|
|
1669
1737
|
|
|
@@ -1673,6 +1741,13 @@ BEGIN
|
|
|
1673
1741
|
CONTINUE;
|
|
1674
1742
|
END IF;
|
|
1675
1743
|
|
|
1744
|
+
IF only_one->name = 'false'::jsonb THEN
|
|
1745
|
+
columns := columns || to_jsonb(name);
|
|
1746
|
+
values_ := values_ || jsonb_build_array('null'::jsonb);
|
|
1747
|
+
CONTINUE;
|
|
1748
|
+
END IF;
|
|
1749
|
+
must_fill := coalesce(only_one->name = 'true'::jsonb, false);
|
|
1750
|
+
|
|
1676
1751
|
IF name = owner THEN
|
|
1677
1752
|
columns := columns || to_jsonb(name);
|
|
1678
1753
|
values_ := values_ || jsonb_build_array(to_jsonb(person));
|
|
@@ -1712,9 +1787,11 @@ BEGIN
|
|
|
1712
1787
|
values_ := values_ || jsonb_build_array(borrowed->name);
|
|
1713
1788
|
CONTINUE;
|
|
1714
1789
|
END IF;
|
|
1715
|
-
-- Anything with a default can supply its own value.
|
|
1790
|
+
-- Anything with a default can supply its own value. A column that may be
|
|
1791
|
+
-- empty is left empty - unless it is the one of a set that has to be
|
|
1792
|
+
-- filled in.
|
|
1716
1793
|
CONTINUE WHEN col->>'default_expr' IS NOT NULL;
|
|
1717
|
-
CONTINUE WHEN NOT (col->>'not_null')::boolean;
|
|
1794
|
+
CONTINUE WHEN NOT (col->>'not_null')::boolean AND NOT must_fill;
|
|
1718
1795
|
|
|
1719
1796
|
columns := columns || to_jsonb(name);
|
|
1720
1797
|
-- A CHECK that lists what it will accept beats anything invented here.
|
|
@@ -2414,12 +2491,36 @@ BEGIN
|
|
|
2414
2491
|
CONTINUE WHEN labels IS NULL OR jsonb_array_length(labels) < 2;
|
|
2415
2492
|
lowest := __KN__.lowest_role(labels);
|
|
2416
2493
|
IF lowest IS NOT NULL THEN
|
|
2417
|
-
|
|
2494
|
+
-- Every role not plainly in charge, the lowest first and then upwards:
|
|
2495
|
+
-- the order reads are tried in.
|
|
2496
|
+
RETURN jsonb_build_object('column', col->>'name', 'type', col->>'type', 'lowest', lowest, 'ladder',
|
|
2497
|
+
jsonb_build_array(lowest) || coalesce((
|
|
2498
|
+
SELECT jsonb_agg(l ORDER BY pos DESC)
|
|
2499
|
+
FROM jsonb_array_elements_text(labels) WITH ORDINALITY AS e(l, pos)
|
|
2500
|
+
WHERE l <> lowest AND l !~* 'owner|admin|super|root'), '[]'::jsonb));
|
|
2418
2501
|
END IF;
|
|
2419
2502
|
END LOOP;
|
|
2420
2503
|
RETURN NULL;
|
|
2421
2504
|
END $$;
|
|
2422
2505
|
|
|
2506
|
+
/*
|
|
2507
|
+
* The columns of a table that look like they hold a secret: a token, a secret
|
|
2508
|
+
* or a key, a password, or the payload a webhook carried. Mirrors
|
|
2509
|
+
* secretColumns in tamper.js. Found on a blind test (HelixOps).
|
|
2510
|
+
*/
|
|
2511
|
+
CREATE OR REPLACE FUNCTION __KN__.secret_columns(tab jsonb)
|
|
2512
|
+
RETURNS jsonb LANGUAGE sql IMMUTABLE AS $$
|
|
2513
|
+
SELECT coalesce(jsonb_agg(c->>'name' ORDER BY pos), '[]'::jsonb)
|
|
2514
|
+
FROM jsonb_array_elements(coalesce(tab->'columns', '[]'::jsonb)) WITH ORDINALITY AS e(c, pos)
|
|
2515
|
+
WHERE (c->>'name') ~* '(^|_)(token|secret|password|passwd|pwd|payload|credentials?|(api|private|access|client|signing|service|encryption|secret|license)_?key|(request|response)_body)(_|$)';
|
|
2516
|
+
$$;
|
|
2517
|
+
|
|
2518
|
+
/* Whether a role's own name says it may only look. */
|
|
2519
|
+
CREATE OR REPLACE FUNCTION __KN__.look_only(role_ text)
|
|
2520
|
+
RETURNS boolean LANGUAGE sql IMMUTABLE AS $$
|
|
2521
|
+
SELECT role_ ~* 'view' OR role_ ~* 'read' OR role_ ~* 'guest' OR role_ ~* 'observ' OR role_ ~* 'audit';
|
|
2522
|
+
$$;
|
|
2523
|
+
|
|
2423
2524
|
/* Whether a column is a key on its own: one row per person, at most. */
|
|
2424
2525
|
CREATE OR REPLACE FUNCTION __KN__.alone_unique(tab jsonb, column_name text)
|
|
2425
2526
|
RETURNS boolean LANGUAGE sql IMMUTABLE AS $$
|
|
@@ -2461,7 +2562,8 @@ BEGIN
|
|
|
2461
2562
|
AND NOT (k->'columns' ? person);
|
|
2462
2563
|
CONTINUE WHEN jsonb_array_length(keys) = 0 OR __KN__.alone_unique(tab, person);
|
|
2463
2564
|
found := found || jsonb_build_array(jsonb_build_object(
|
|
2464
|
-
'table', tab, 'person', person, 'role', role_->>'column', 'lowest', role_->>'lowest',
|
|
2565
|
+
'table', tab, 'person', person, 'role', role_->>'column', 'lowest', role_->>'lowest',
|
|
2566
|
+
'roleType', role_->>'type', 'ladder', role_->'ladder', 'keys', keys));
|
|
2465
2567
|
END LOOP;
|
|
2466
2568
|
-- A table named for members first: it is the one a team rule asks.
|
|
2467
2569
|
SELECT coalesce(jsonb_agg(m ORDER BY (m->'table'->>'name') !~* 'member', at), '[]'::jsonb) INTO found
|
|
@@ -2521,7 +2623,49 @@ RETURNS bigint LANGUAGE sql IMMUTABLE AS $$
|
|
|
2521
2623
|
ELSE 0 END;
|
|
2522
2624
|
$$;
|
|
2523
2625
|
|
|
2524
|
-
|
|
2626
|
+
/*
|
|
2627
|
+
* Whether the app's own rules name this role among those allowed to make
|
|
2628
|
+
* this kind of write: the role in a list in a permissive rule for that
|
|
2629
|
+
* command, or in a list inside a helper that rule calls. A rule written for
|
|
2630
|
+
* one role on its own (role = 'analyst', LaunchRail) is not a list of who may
|
|
2631
|
+
* write. Mirrors allowedByRules in tamper.js.
|
|
2632
|
+
*/
|
|
2633
|
+
CREATE OR REPLACE FUNCTION __KN__.allowed_by_rules(rules jsonb, table_name text, what text, role_ text, type_ text)
|
|
2634
|
+
RETURNS boolean LANGUAGE plpgsql IMMUTABLE AS $$
|
|
2635
|
+
DECLARE
|
|
2636
|
+
esc constant text := '([.*+?^${}()|\[\]\\])';
|
|
2637
|
+
label text := '''' || regexp_replace(replace(role_, '''', ''''''), esc, '\\\1', 'g') || '''';
|
|
2638
|
+
type_name text := regexp_replace(replace(split_part(coalesce(type_, 'text'), '.', -1), '"', ''), esc, '\\\1', 'g');
|
|
2639
|
+
in_rule_list text := 'ARRAY\[[^]]*' || label || '::("?[A-Za-z0-9_$]+"?\.)?"?' || type_name || '"?';
|
|
2640
|
+
-- The same list written as an array literal, kept as '{...}'::role[].
|
|
2641
|
+
in_rule_literal text := '''\{([^}'']*,)?"?' || regexp_replace(role_, esc, '\\\1', 'g') ||
|
|
2642
|
+
'"?(,[^}'']*)?\}''::("?[A-Za-z0-9_$]+"?\.)?"?' || type_name || '"?\[\]';
|
|
2643
|
+
in_helper_list text := '(\mIN\s*\(|ARRAY\[)[^])]*' || label;
|
|
2644
|
+
command text := CASE what WHEN 'add' THEN 'INSERT' WHEN 'change' THEN 'UPDATE' WHEN 'delete' THEN 'DELETE' END;
|
|
2645
|
+
policy jsonb;
|
|
2646
|
+
body text;
|
|
2647
|
+
BEGIN
|
|
2648
|
+
FOR policy IN SELECT * FROM jsonb_array_elements(coalesce(rules->'policies', '[]'::jsonb)) LOOP
|
|
2649
|
+
CONTINUE WHEN policy->>'table_name' <> table_name;
|
|
2650
|
+
CONTINUE WHEN upper(coalesce(policy->>'permissive', 'PERMISSIVE')) <> 'PERMISSIVE';
|
|
2651
|
+
CONTINUE WHEN upper(coalesce(policy->>'cmd', 'ALL')) NOT IN ('ALL', command);
|
|
2652
|
+
body := coalesce(policy->>'qual', '') || ' ' || coalesce(policy->>'with_check', '');
|
|
2653
|
+
IF body ~* in_rule_list OR body ~* in_rule_literal THEN RETURN true; END IF;
|
|
2654
|
+
IF EXISTS (
|
|
2655
|
+
SELECT 1 FROM jsonb_array_elements(coalesce(rules->'functions', '[]'::jsonb)) f
|
|
2656
|
+
WHERE __KN__.calls_function(body, f->>'name')
|
|
2657
|
+
AND coalesce(f->>'src', '') ~* in_helper_list) THEN
|
|
2658
|
+
RETURN true;
|
|
2659
|
+
END IF;
|
|
2660
|
+
END LOOP;
|
|
2661
|
+
RETURN false;
|
|
2662
|
+
END $$;
|
|
2663
|
+
|
|
2664
|
+
-- The three-argument teammate of 0.1.26-0.1.28, dropped so an engine
|
|
2665
|
+
-- upgraded in place keeps one version and every call stays unambiguous.
|
|
2666
|
+
DROP FUNCTION IF EXISTS __KN__.teammate(text, jsonb, jsonb);
|
|
2667
|
+
|
|
2668
|
+
CREATE OR REPLACE FUNCTION __KN__.teammate(source text, tables jsonb, seeded jsonb, rules jsonb)
|
|
2525
2669
|
RETURNS jsonb LANGUAGE plpgsql AS $$
|
|
2526
2670
|
DECLARE
|
|
2527
2671
|
findings jsonb := '[]'::jsonb;
|
|
@@ -2533,20 +2677,27 @@ DECLARE
|
|
|
2533
2677
|
shape integer;
|
|
2534
2678
|
at text;
|
|
2535
2679
|
theirs jsonb;
|
|
2680
|
+
team jsonb;
|
|
2536
2681
|
overrides jsonb;
|
|
2682
|
+
joins jsonb;
|
|
2537
2683
|
key jsonb;
|
|
2538
2684
|
name text;
|
|
2539
2685
|
parent text;
|
|
2540
|
-
|
|
2686
|
+
parent_key jsonb;
|
|
2687
|
+
to_team jsonb;
|
|
2688
|
+
pointed jsonb;
|
|
2541
2689
|
row_ jsonb;
|
|
2542
2690
|
join_ text;
|
|
2543
2691
|
refused text;
|
|
2544
2692
|
owner text;
|
|
2693
|
+
where_ text[];
|
|
2545
2694
|
not_theirs text;
|
|
2546
2695
|
target text;
|
|
2547
2696
|
column_ text;
|
|
2548
2697
|
finding_key text;
|
|
2549
|
-
|
|
2698
|
+
read_key text;
|
|
2699
|
+
moves jsonb;
|
|
2700
|
+
move jsonb;
|
|
2550
2701
|
was bigint;
|
|
2551
2702
|
now_ bigint;
|
|
2552
2703
|
before_ jsonb;
|
|
@@ -2554,6 +2705,14 @@ DECLARE
|
|
|
2554
2705
|
can jsonb;
|
|
2555
2706
|
changed jsonb;
|
|
2556
2707
|
stuck text;
|
|
2708
|
+
secrets jsonb;
|
|
2709
|
+
look text;
|
|
2710
|
+
role_ text;
|
|
2711
|
+
tried jsonb;
|
|
2712
|
+
i integer;
|
|
2713
|
+
before_all jsonb;
|
|
2714
|
+
below jsonb;
|
|
2715
|
+
listed boolean;
|
|
2557
2716
|
BEGIN
|
|
2558
2717
|
FOR member IN SELECT * FROM jsonb_array_elements(__KN__.memberships(tables)) LOOP
|
|
2559
2718
|
SELECT (s->>'attempt')::integer INTO shape
|
|
@@ -2571,37 +2730,52 @@ BEGIN
|
|
|
2571
2730
|
CONTINUE WHEN theirs IS NULL;
|
|
2572
2731
|
|
|
2573
2732
|
-- Their team, as the first person's own membership names it.
|
|
2574
|
-
|
|
2733
|
+
team := '{}'::jsonb;
|
|
2575
2734
|
FOR key IN SELECT * FROM jsonb_array_elements(member->'keys') LOOP
|
|
2576
2735
|
FOR name IN SELECT jsonb_array_elements_text(key->'columns') LOOP
|
|
2577
2736
|
IF name <> member->>'person' THEN
|
|
2578
|
-
|
|
2737
|
+
team := jsonb_set(team, ARRAY[name], coalesce(theirs->name, 'null'::jsonb));
|
|
2579
2738
|
END IF;
|
|
2580
2739
|
END LOOP;
|
|
2581
2740
|
END LOOP;
|
|
2582
|
-
|
|
2583
|
-
|
|
2741
|
+
parent_key := member->'keys'->0;
|
|
2742
|
+
parent := parent_key->>'refTable';
|
|
2584
2743
|
|
|
2585
|
-
--
|
|
2744
|
+
-- Putting the other person in that team, as each role in turn. Built up
|
|
2745
|
+
-- front, before anybody changes role.
|
|
2746
|
+
joins := '{}'::jsonb;
|
|
2586
2747
|
refused := NULL;
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2748
|
+
FOR role_ IN SELECT jsonb_array_elements_text(member->'ladder') LOOP
|
|
2749
|
+
BEGIN
|
|
2750
|
+
overrides := jsonb_set(team, ARRAY[member->>'role'], to_jsonb(role_));
|
|
2751
|
+
row_ := __KN__.row_for(source, member->'table', __KN__.user_b(), '9', overrides, shape);
|
|
2752
|
+
joins := jsonb_set(joins, ARRAY[role_],
|
|
2753
|
+
to_jsonb(__KN__.insert_statement(source, member->'table'->>'name', row_)));
|
|
2754
|
+
EXCEPTION WHEN OTHERS THEN
|
|
2755
|
+
IF role_ = member->>'lowest' THEN refused := SQLERRM; END IF;
|
|
2756
|
+
END;
|
|
2757
|
+
END LOOP;
|
|
2758
|
+
join_ := joins->>(member->>'lowest');
|
|
2759
|
+
|
|
2760
|
+
-- Whether they can be put in the team at all, tried once on its own.
|
|
2761
|
+
IF refused IS NULL THEN
|
|
2590
2762
|
BEGIN
|
|
2591
2763
|
EXECUTE join_;
|
|
2592
2764
|
RAISE EXCEPTION 'kryptheon: undoing the join' USING ERRCODE = 'KN001';
|
|
2593
|
-
EXCEPTION
|
|
2594
|
-
|
|
2765
|
+
EXCEPTION
|
|
2766
|
+
WHEN SQLSTATE 'KN001' THEN
|
|
2767
|
+
NULL;
|
|
2768
|
+
WHEN OTHERS THEN
|
|
2769
|
+
refused := SQLERRM;
|
|
2595
2770
|
END;
|
|
2596
|
-
|
|
2597
|
-
refused := SQLERRM;
|
|
2598
|
-
END;
|
|
2771
|
+
END IF;
|
|
2599
2772
|
IF refused IS NOT NULL THEN
|
|
2600
2773
|
-- Said once, on the table that would not take them.
|
|
2601
2774
|
blocked := blocked || jsonb_build_array(jsonb_build_object(
|
|
2602
2775
|
'table', member->'table'->>'name',
|
|
2603
2776
|
'key', 'role:' || (member->'table'->>'name') || ':' || (member->>'lowest'),
|
|
2604
|
-
'why',
|
|
2777
|
+
'why', 'as a "' || (member->>'lowest') || '" in ' || (member->'table'->>'name') ||
|
|
2778
|
+
': I could not add one: ' || refused));
|
|
2605
2779
|
CONTINUE;
|
|
2606
2780
|
END IF;
|
|
2607
2781
|
|
|
@@ -2610,54 +2784,172 @@ BEGIN
|
|
|
2610
2784
|
WHERE s->>'table' = tab->>'name');
|
|
2611
2785
|
CONTINUE WHEN done ? (tab->>'name');
|
|
2612
2786
|
done := done || to_jsonb(tab->>'name');
|
|
2613
|
-
|
|
2787
|
+
SELECT (s->>'attempt')::integer INTO shape
|
|
2788
|
+
FROM jsonb_array_elements(coalesce(seeded, '[]'::jsonb)) s
|
|
2789
|
+
WHERE s->>'table' = tab->>'name';
|
|
2790
|
+
shape := coalesce(shape, 0);
|
|
2614
2791
|
owner := __KN__.owner_column(tab);
|
|
2615
|
-
|
|
2616
|
-
--
|
|
2617
|
-
|
|
2618
|
-
|
|
2792
|
+
|
|
2793
|
+
-- A table that names the team itself, and the team it is to name.
|
|
2794
|
+
SELECT k INTO to_team
|
|
2795
|
+
FROM jsonb_array_elements(__KN__.foreign_keys(tab) || __KN__.implied_keys(tab, tables))
|
|
2796
|
+
WITH ORDINALITY AS e(k, pos)
|
|
2797
|
+
WHERE k->>'refTable' = parent
|
|
2798
|
+
AND jsonb_array_length(k->'columns') = jsonb_array_length(parent_key->'refColumns')
|
|
2799
|
+
ORDER BY pos LIMIT 1;
|
|
2800
|
+
IF NOT FOUND THEN to_team := NULL; END IF;
|
|
2801
|
+
pointed := '{}'::jsonb;
|
|
2802
|
+
IF to_team IS NOT NULL THEN
|
|
2803
|
+
FOR i IN 0 .. jsonb_array_length(to_team->'columns') - 1 LOOP
|
|
2804
|
+
pointed := jsonb_set(pointed, ARRAY[to_team->'columns'->>i], coalesce(team->(
|
|
2805
|
+
SELECT parent_key->'columns'->>((p.pos - 1)::integer)
|
|
2806
|
+
FROM jsonb_array_elements_text(parent_key->'refColumns') WITH ORDINALITY AS p(c, pos)
|
|
2807
|
+
WHERE p.c = to_team->'refColumns'->>i), 'null'::jsonb));
|
|
2808
|
+
END LOOP;
|
|
2809
|
+
END IF;
|
|
2810
|
+
|
|
2811
|
+
-- Never their own rows, and only the team they joined, where the table
|
|
2812
|
+
-- says which team a row is in.
|
|
2813
|
+
where_ := ARRAY[]::text[];
|
|
2814
|
+
IF owner IS NOT NULL THEN
|
|
2815
|
+
where_ := where_ || (__KN__.always_quote(owner) || ' IS DISTINCT FROM ' || quote_literal(__KN__.user_b()));
|
|
2816
|
+
END IF;
|
|
2817
|
+
FOR name IN SELECT jsonb_object_keys(pointed) ORDER BY 1 LOOP
|
|
2818
|
+
where_ := where_ || (__KN__.always_quote(name) || ' = ' || quote_literal(pointed->>name));
|
|
2819
|
+
END LOOP;
|
|
2820
|
+
not_theirs := CASE WHEN array_length(where_, 1) IS NULL THEN ''
|
|
2821
|
+
ELSE ' WHERE ' || array_to_string(where_, ' AND ') END;
|
|
2619
2822
|
target := __KN__.always_quote(source) || '.' || __KN__.always_quote(tab->>'name');
|
|
2620
2823
|
column_ := __KN__.always_quote(__KN__.first_writable(tab));
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2824
|
+
moves := jsonb_build_array(
|
|
2825
|
+
jsonb_build_object('what', 'change',
|
|
2826
|
+
'statement', 'UPDATE ' || target || ' SET ' || column_ || ' = ' || column_ || not_theirs),
|
|
2827
|
+
jsonb_build_object('what', 'delete', 'statement', 'DELETE FROM ' || target || not_theirs));
|
|
2828
|
+
|
|
2829
|
+
-- Adding a row to the team, said only of a role whose own name says it
|
|
2830
|
+
-- may only look, and written under the other person's name: a rule that
|
|
2831
|
+
-- lets a member add a row only as themselves is them speaking for
|
|
2832
|
+
-- themselves.
|
|
2833
|
+
IF to_team IS NOT NULL AND EXISTS (
|
|
2834
|
+
SELECT 1 FROM jsonb_array_elements_text(member->'ladder') r WHERE __KN__.look_only(r)) THEN
|
|
2835
|
+
BEGIN
|
|
2836
|
+
row_ := __KN__.row_for(source, tab, __KN__.user_a(), '11', pointed, shape);
|
|
2837
|
+
moves := jsonb_build_array(jsonb_build_object('what', 'add', 'lookOnly', true,
|
|
2838
|
+
'statement', __KN__.insert_statement(source, tab->>'name', row_))) || moves;
|
|
2839
|
+
EXCEPTION WHEN OTHERS THEN
|
|
2840
|
+
NULL;
|
|
2841
|
+
END;
|
|
2842
|
+
END IF;
|
|
2843
|
+
|
|
2844
|
+
-- Every role not in charge, from the lowest up, the way reads are tried
|
|
2845
|
+
-- (LaunchRail: production gave "analyst" its own UPDATE rule and only
|
|
2846
|
+
-- "viewer" was tried). The first role that can write where the app's own
|
|
2847
|
+
-- rules do not list it is the one named. What each write did before
|
|
2848
|
+
-- joining is asked once.
|
|
2849
|
+
before_all := '{}'::jsonb;
|
|
2850
|
+
FOR move IN SELECT * FROM jsonb_array_elements(moves) LOOP
|
|
2851
|
+
before_all := jsonb_set(before_all, ARRAY[move->>'what'],
|
|
2852
|
+
__KN__.try_write('authenticated', __KN__.user_b(), move->>'statement'));
|
|
2853
|
+
END LOOP;
|
|
2854
|
+
below := '[]'::jsonb;
|
|
2855
|
+
FOR role_ IN SELECT jsonb_array_elements_text(member->'ladder') LOOP
|
|
2856
|
+
finding_key := 'role:' || (tab->>'name') || ':' || role_;
|
|
2857
|
+
IF NOT (joins ? role_) THEN
|
|
2858
|
+
blocked := blocked || jsonb_build_array(jsonb_build_object(
|
|
2859
|
+
'table', tab->>'name', 'key', finding_key,
|
|
2860
|
+
'why', 'as a "' || role_ || '" in ' || (member->'table'->>'name') || ': I could not add one'));
|
|
2861
|
+
EXIT;
|
|
2862
|
+
END IF;
|
|
2863
|
+
can := '[]'::jsonb;
|
|
2864
|
+
changed := '{}'::jsonb;
|
|
2865
|
+
stuck := NULL;
|
|
2866
|
+
listed := false;
|
|
2867
|
+
FOR move IN SELECT * FROM jsonb_array_elements(moves) LOOP
|
|
2868
|
+
CONTINUE WHEN coalesce((move->>'lookOnly')::boolean, false) AND NOT __KN__.look_only(role_);
|
|
2869
|
+
-- A write the rules list this role for is not asked: whatever it did
|
|
2870
|
+
-- would not be reported, and a key refusing it left the table untested.
|
|
2871
|
+
IF __KN__.allowed_by_rules(rules, tab->>'name', move->>'what', role_, member->>'roleType') THEN
|
|
2872
|
+
listed := true;
|
|
2873
|
+
CONTINUE;
|
|
2874
|
+
END IF;
|
|
2875
|
+
before_ := before_all->(move->>'what');
|
|
2876
|
+
after_ := __KN__.try_write_joined('authenticated', __KN__.user_b(), move->>'statement', joins->>role_);
|
|
2877
|
+
was := __KN__.rows_moved(before_);
|
|
2878
|
+
now_ := __KN__.rows_moved(after_);
|
|
2879
|
+
IF was IS NULL OR now_ IS NULL THEN
|
|
2880
|
+
stuck := CASE WHEN now_ IS NULL THEN after_->>'why' ELSE before_->>'why' END;
|
|
2881
|
+
ELSIF now_ > was THEN
|
|
2882
|
+
can := can || to_jsonb(move->>'what');
|
|
2883
|
+
changed := jsonb_set(changed, ARRAY[move->>'what'], to_jsonb(now_ - was));
|
|
2884
|
+
END IF;
|
|
2885
|
+
END LOOP;
|
|
2886
|
+
|
|
2887
|
+
IF jsonb_array_length(can) = 0 AND stuck IS NOT NULL THEN
|
|
2888
|
+
blocked := blocked || jsonb_build_array(jsonb_build_object(
|
|
2889
|
+
'table', tab->>'name', 'key', finding_key,
|
|
2890
|
+
'why', 'as a "' || role_ || '" in ' || (member->'table'->>'name') || ': ' || stuck));
|
|
2891
|
+
EXIT;
|
|
2892
|
+
END IF;
|
|
2893
|
+
completed := completed || to_jsonb(finding_key);
|
|
2894
|
+
IF jsonb_array_length(can) > 0 THEN
|
|
2895
|
+
findings := findings || jsonb_build_array(jsonb_build_object(
|
|
2896
|
+
'kind', 'role',
|
|
2897
|
+
'table', tab->>'name',
|
|
2898
|
+
'who', role_,
|
|
2899
|
+
'via', member->'table'->>'name',
|
|
2900
|
+
'roleColumn', member->>'role',
|
|
2901
|
+
'parent', parent,
|
|
2902
|
+
'can', can,
|
|
2903
|
+
'changed', changed,
|
|
2904
|
+
'below', below,
|
|
2905
|
+
'columns', coalesce((SELECT jsonb_agg(c->>'name') FROM jsonb_array_elements(tab->'columns') c), '[]'::jsonb),
|
|
2906
|
+
'rlsEnabled', coalesce((tab->>'rlsEnabled')::boolean, false)));
|
|
2907
|
+
EXIT;
|
|
2908
|
+
END IF;
|
|
2909
|
+
IF NOT listed THEN below := below || to_jsonb(role_); END IF;
|
|
2910
|
+
END LOOP;
|
|
2911
|
+
|
|
2912
|
+
-- Reading what a member should not see, said only of a table holding a
|
|
2913
|
+
-- secret. Every role not in charge, from the lowest up; the first that
|
|
2914
|
+
-- can read is the one named.
|
|
2915
|
+
secrets := __KN__.secret_columns(tab);
|
|
2916
|
+
CONTINUE WHEN jsonb_array_length(secrets) = 0;
|
|
2917
|
+
look := 'SELECT 1 FROM ' || target || not_theirs;
|
|
2918
|
+
was := __KN__.rows_moved(__KN__.try_write('authenticated', __KN__.user_b(), look));
|
|
2919
|
+
tried := '[]'::jsonb;
|
|
2920
|
+
FOR role_ IN SELECT jsonb_array_elements_text(member->'ladder') LOOP
|
|
2921
|
+
read_key := 'teamread:' || (tab->>'name') || ':' || role_;
|
|
2922
|
+
IF joins ? role_ THEN
|
|
2923
|
+
after_ := __KN__.try_write_joined('authenticated', __KN__.user_b(), look, joins->>role_);
|
|
2924
|
+
ELSE
|
|
2925
|
+
after_ := jsonb_build_object('ok', false, 'count', 0, 'why', 'I could not add one');
|
|
2926
|
+
END IF;
|
|
2633
2927
|
now_ := __KN__.rows_moved(after_);
|
|
2634
2928
|
IF was IS NULL OR now_ IS NULL THEN
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2929
|
+
blocked := blocked || jsonb_build_array(jsonb_build_object(
|
|
2930
|
+
'table', tab->>'name', 'key', read_key,
|
|
2931
|
+
'why', 'as a "' || role_ || '" in ' || (member->'table'->>'name') || ': ' ||
|
|
2932
|
+
CASE WHEN now_ IS NULL THEN after_->>'why' ELSE 'reading it failed before joining' END));
|
|
2933
|
+
EXIT;
|
|
2639
2934
|
END IF;
|
|
2935
|
+
completed := completed || to_jsonb(read_key);
|
|
2936
|
+
IF now_ > was THEN
|
|
2937
|
+
findings := findings || jsonb_build_array(jsonb_build_object(
|
|
2938
|
+
'kind', 'teamread',
|
|
2939
|
+
'table', tab->>'name',
|
|
2940
|
+
'who', role_,
|
|
2941
|
+
'via', member->'table'->>'name',
|
|
2942
|
+
'roleColumn', member->>'role',
|
|
2943
|
+
'parent', parent,
|
|
2944
|
+
'readable', now_ - was,
|
|
2945
|
+
'secrets', secrets,
|
|
2946
|
+
'below', tried,
|
|
2947
|
+
'columns', coalesce((SELECT jsonb_agg(c->>'name') FROM jsonb_array_elements(tab->'columns') c), '[]'::jsonb),
|
|
2948
|
+
'rlsEnabled', coalesce((tab->>'rlsEnabled')::boolean, false)));
|
|
2949
|
+
EXIT;
|
|
2950
|
+
END IF;
|
|
2951
|
+
tried := tried || to_jsonb(role_);
|
|
2640
2952
|
END LOOP;
|
|
2641
|
-
|
|
2642
|
-
IF jsonb_array_length(can) = 0 AND stuck IS NOT NULL THEN
|
|
2643
|
-
blocked := blocked || jsonb_build_array(jsonb_build_object(
|
|
2644
|
-
'table', tab->>'name', 'key', finding_key, 'why', as_ || ': ' || stuck));
|
|
2645
|
-
CONTINUE;
|
|
2646
|
-
END IF;
|
|
2647
|
-
completed := completed || to_jsonb(finding_key);
|
|
2648
|
-
IF jsonb_array_length(can) > 0 THEN
|
|
2649
|
-
findings := findings || jsonb_build_array(jsonb_build_object(
|
|
2650
|
-
'kind', 'role',
|
|
2651
|
-
'table', tab->>'name',
|
|
2652
|
-
'who', member->>'lowest',
|
|
2653
|
-
'via', member->'table'->>'name',
|
|
2654
|
-
'roleColumn', member->>'role',
|
|
2655
|
-
'parent', parent,
|
|
2656
|
-
'can', can,
|
|
2657
|
-
'changed', changed,
|
|
2658
|
-
'columns', coalesce((SELECT jsonb_agg(c->>'name') FROM jsonb_array_elements(tab->'columns') c), '[]'::jsonb),
|
|
2659
|
-
'rlsEnabled', coalesce((tab->>'rlsEnabled')::boolean, false)));
|
|
2660
|
-
END IF;
|
|
2661
2953
|
END LOOP;
|
|
2662
2954
|
END LOOP;
|
|
2663
2955
|
|
|
@@ -3006,11 +3298,20 @@ DECLARE
|
|
|
3006
3298
|
tab jsonb;
|
|
3007
3299
|
col jsonb;
|
|
3008
3300
|
expectation text;
|
|
3301
|
+
outbound boolean;
|
|
3009
3302
|
BEGIN
|
|
3010
3303
|
FOR tab IN SELECT * FROM jsonb_array_elements(tables) LOOP
|
|
3304
|
+
-- A table keeping another service's credentials, so this app can call
|
|
3305
|
+
-- it: a token there opens nothing in this app, and two rows sharing one
|
|
3306
|
+
-- is not a way in. Found on a blind test (LaunchRail), where
|
|
3307
|
+
-- integration_credentials.access_token was reported as "can exist twice".
|
|
3308
|
+
outbound := tab->>'name' ~* '(^|_)(integrations?|providers?|oauth|connections?|connectors?|external|third_party)(_|$)'
|
|
3309
|
+
OR EXISTS (SELECT 1 FROM jsonb_array_elements(coalesce(tab->'columns', '[]'::jsonb)) c
|
|
3310
|
+
WHERE c->>'name' ~* '^provider$');
|
|
3011
3311
|
FOR col IN SELECT * FROM jsonb_array_elements(coalesce(tab->'columns', '[]'::jsonb)) LOOP
|
|
3012
3312
|
expectation := __KN__.must_be_unique(col->>'name', tab->>'name');
|
|
3013
3313
|
CONTINUE WHEN expectation IS NULL;
|
|
3314
|
+
CONTINUE WHEN expectation = 'credential' AND outbound;
|
|
3014
3315
|
found := found || jsonb_build_array(jsonb_build_object(
|
|
3015
3316
|
'table', tab->>'name',
|
|
3016
3317
|
'column', col->>'name',
|
|
@@ -3178,7 +3479,8 @@ BEGIN
|
|
|
3178
3479
|
|
|
3179
3480
|
seen := __KN__.impersonate(copy_name, theirs || views);
|
|
3180
3481
|
wrote := __KN__.tamper(copy_name, theirs, sown->'seeded', copy_plan->'policies');
|
|
3181
|
-
ranked := __KN__.teammate(copy_name, theirs, sown->'seeded'
|
|
3482
|
+
ranked := __KN__.teammate(copy_name, theirs, sown->'seeded',
|
|
3483
|
+
jsonb_build_object('policies', copy_plan->'policies', 'functions', copy_plan->'functions'));
|
|
3182
3484
|
stranded := __KN__.orphan(copy_name, theirs, sown->'seeded');
|
|
3183
3485
|
raced := __KN__.collide(copy_name, theirs, copy_plan->'indexes');
|
|
3184
3486
|
|