From 5d4fb853830776fe3ac143eda54ca4a7c3712a86 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sun, 31 Jul 2022 15:16:49 +0200 Subject: [PATCH] TODO tracking --- Glossary.md | 18 -- TODO-progress.png | Bin 0 -> 7433 bytes TODO.md | 82 +++++++++ ...2022-07-18.row-level-security-mechanism.md | 160 ------------------ doc/test-concept.md | 19 +-- tools/todo-progress | 22 +++ tools/todo-progress.gnuplot | 23 +++ 7 files changed, 132 insertions(+), 192 deletions(-) delete mode 100644 Glossary.md create mode 100644 TODO-progress.png create mode 100644 TODO.md delete mode 100644 adr/2022-07-18.row-level-security-mechanism.md create mode 100755 tools/todo-progress create mode 100644 tools/todo-progress.gnuplot diff --git a/Glossary.md b/Glossary.md deleted file mode 100644 index 2757ecc0..00000000 --- a/Glossary.md +++ /dev/null @@ -1,18 +0,0 @@ -# hsadminNg Glossary - -### Business Object - -Represents an object from the - -### Tenant - -The RBAC - -### RBAC - -abbreviation for *Role Based Access Control* - -### Role Based Access Control (RBAC) - -A system to control access to business objects by defining users, roles, and permissions. -For more information see diff --git a/TODO-progress.png b/TODO-progress.png new file mode 100644 index 0000000000000000000000000000000000000000..b08f6f1258fbe868fc6355ba7820cb87c81f4642 GIT binary patch literal 7433 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 +| Datum | Budget | Aufwand | Leistung | Restschuld | +|------------|-------:|--------:|---------:|-----------:| +| 2022-07-17 | 553 | 44 | 0 | 553 | +| 2022-07-24 | 553 | 8 | 0 | 553 | +| 2022-07-31 | 553 | 143 | 76 | 477 | + + + diff --git a/adr/2022-07-18.row-level-security-mechanism.md b/adr/2022-07-18.row-level-security-mechanism.md deleted file mode 100644 index 51b72245..00000000 --- a/adr/2022-07-18.row-level-security-mechanism.md +++ /dev/null @@ -1,160 +0,0 @@ -# Use VIEWs with JOIN into Permission-Assignments for Row-Level-Security - -**Status:** -- [x] proposed by Michael Hönnig -- [ ] accepted by (Participants) -- [ ] rejected by (Participants) -- [ ] superseded by (superseding ADR) - -## Context and Problem Statement - -We need to decide how to apply the access rules defined in our RBAC system to the visibility of table rows for the accessing user. - -The core problem here is, that in our RBAC system, determining the permissions of the accessing user has to consider a hierarchy of roles. - -### Technical Background - -The session variable `hsadminng.currentUser` contains the accessing (domain-level) user, which is unrelated to the PostgreSQL user). - -Given is a stored function `isPermissionGrantedToSubject` which detects if the accessing user has a given permission (e.g. 'view'). - -Given is also a stored function `queryAllPermissionsOfSubjectId` which returns the flattened view to all permissions assigned to the given accessing user. - -In the following code snippets `customer` is just an example domain table. - -## Considered Options - -* Perform Visibility-Checks programmatically in the Backend -* Add Visibility-Checks in the Backend -* POLICY with ENABLE ROW LEVEL SECURITY -* VIEW-RULE with ON SELECT DO INSTEAD -* VIEW with JOIN into Flattened Permissions - -### Perform Visibility-Checks programmatically in the Backend - -In this solution, the database ignores row level visibility and returns all rows which match a given query. Afterwards, the result is filtered programmatically with Java-code in the backend. - -#### Advantages - -Very flexible access, programmatic, rules could be implemented. - -The role-hierarchy and permissions for currently logged-in users user could be cached in the backend. - -The access logic can be tested in pure Java unit tests. - -At least regarding this aspect, an in-memory database could be used for integration testing; though the recursive Role-evaluation uses PostgreSQL features anyway. - -#### Disadvantages - -It's inefficient when initial query is not very restrictive, e.g. as on overview pages in a frontend, which often show all accessible objects, large parts or even whole database tables need to be transferred from the database to the backend. - -It's error-prone and security leaks can happen too easily, because after every query the access rights for all participating joins have to be considered. - -### Add Visibility-Checks in the Backend - -In this solution again, the database ignores row level visibility and returns all rows which match a given query. And the backend adds filter conditions to each query sent to the database. - -#### Advantages - -At least regarding this aspect, an in-memory database could be used for integration testing. - -#### Disadvantages - -It's error-prone and security leaks can happen too easily, because for every query the access rights for all participating joins have to be considered. - -### POLICY with ENABLE ROW LEVEL SECURITY - -For restricted DB-users, which are used by the backend, access to rows is filtered using a policy: - - SET SESSION AUTHORIZATION DEFAULT; - CREATE ROLE restricted; - GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO restricted; - ALTER TABLE customer ENABLE ROW LEVEL SECURITY; - CREATE POLICY customer_policy ON customer - FOR SELECT - TO restricted - USING ( - isPermissionGrantedToSubject(findPermissionId('customer', id, 'view'), currentUserId()) - ); - - SET SESSION AUTHORIZATION restricted; - SET hsadminng.currentUser TO 'alex@example.com'; - SELECT * from customer; -- will only return visible rows - -#### Advantages - -Using POLICY together with ENABLE ROW LEVEL SECURITY is the PostgreSQL native mechanism to control access to data on the role level. Therefore, it looked like an obvious and elegant solution. - -Every access at from the backend is under access control at the database level. - -### Disadvantages - -Unfortunately security mechanisms in PostgreSQL prevent the query optimizer to work well beyond ownership barriers (session user vs. table owner) and a SELECT from a table with 1 million objects needed over 30 seconds with our hierarchical RBAC policy. - -We are bound to PostgreSQL, including integration tests and testing the RBAC system itself. - -### VIEW-RULE with ON SELECT DO INSTEAD - - SET SESSION SESSION AUTHORIZATION DEFAULT; - CREATE VIEW cust_view AS - SELECT * FROM customer; - CREATE OR REPLACE RULE "_RETURN" AS - ON SELECT TO cust_view - DO INSTEAD - SELECT * FROM customer WHERE isPermissionGrantedToSubject(findPermissionId('customer', id, 'view'), currentUserId()); - - SET SESSION AUTHORIZATION restricted; - SET hsadminng.currentUser TO 'alex@example.com'; - SELECT * from customer; -- will only return visible rows - -#### Advantages - -Every access at from the backend is under access control at the database level. - -Also using ON UPDATE etc., original tables could be completely hidden from the backend, and thus improved security. - -### Disadvantages - -Unfortunately security mechanisms in PostgreSQL prevent the query optimizer to work well beyond ownership barriers (session user vs. table owner) and a SELECT from a table with 1 million objects needed over 30 seconds with our hierarchical RBAC policy. - -We are bound to PostgreSQL, including integration tests and testing the RBAC system itself. - -An extra view needed for every table. - - -### VIEW with JOIN into flattened permissions - -We do not access the tables directly from the backend, but via views which join the flattened permissions - - SET SESSION SESSION AUTHORIZATION DEFAULT; - CREATE OR REPLACE VIEW cust_view AS - SELECT c.id, c.reference, c.prefix - FROM customer AS c - JOIN queryAllPermissionsOfSubjectId(currentUserId()) AS p - ON p.tableName='customer' AND p.rowId=c.id AND p.op='view'; - GRANT ALL PRIVILEGES ON cust_view TO restricted; - - SET SESSION SESSION AUTHORIZATION restricted; - SET hsadminng.currentUser TO 'alex@example.com'; - SELECT * from cust_view; -- will only return visible rows - -Alternatively the JOIN could also be applied in a "ON SELECT DO INSTEAD"-RULE, if there is any advantage for later features. - -#### Advantages - -Every access at from the backend is under access control at the database level. - -No special PostgreSQL features needed; though the recursive Role-evaluation uses PostgreSQL features anyway. - -Very fast, on my laptop a SELECT * FROM a table with 1 million rows just took about 50ms. - -Also using ON UPDATE etc., original tables could be completely hidden from the backend, and thus improved security. - -### Disadvantages - -An extra view needed for every table. - - -## Decision Outcome - -We chose the option **"VIEW with JOIN into flattened permissions"** because it supports the best combination of performance and security with almost no disadvantge. diff --git a/doc/test-concept.md b/doc/test-concept.md index 237102a6..95feb30f 100644 --- a/doc/test-concept.md +++ b/doc/test-concept.md @@ -11,22 +11,11 @@ ### General Issues -The following test concept uses the terms "double" and "mock" (maybe in inflected form like "mocking" or "mocked"), "whitebox-test" and "blackbox-tests" which I would like to define first. +The following test concept uses terms like "double" and "mock" (maybe in inflected form like "mocking" or "mocked"), "whitebox-test" and "blackbox-tests" and "test-fixture". +Please look up their definition in the [glossary](glossary.md) -#### Test-Doubles, Dummies, Fakes, Mocks, Spies and Stubs - -A "double" is a general term for something which replaces a real implementation of a dependency of the unit under test. -This can be a "dummy", a "fake", a "mock", a "spy" or a "stub". -Often the term "mock" is used in a generic way, because typical mocking libraries like *Mockito* can also be used as dummies or spies and can replace fakes. - -A fake would be a double without using any library, but rather a manual fake implementation of a dependency. Where our APIs should be designed in a way that it's possible, using a mocking library like *Mockito* often leads to shorter test code. -#### Whitebox- and Blackbox-Tests - -A whitebox-test knows and considers the internals of an implementation, e.g. it knows which dependencies it needs and can test special, implementation-dependent cases. - -A blackbox-test does not know and not consider such internals of an implementation, it just tests externally observable behaviour. ### Kinds of Tests @@ -35,7 +24,9 @@ Depending on the concrete aspects which we want to test, we are using different #### Unit-Tests In this project a *Unit* for *UnitTests* can be a single method (function), a class or even a group of classes which express a common concept. -The unit are whitebox-tests and count into test-code-coverage. + +The unit are technically whitebox-tests and count into test-code-coverage. +But the whitebox-knowledge should only be used for the text-fixture. Unit-Test in this project are implemented with *JUnit Jupiter*, *Mockito* and *AssertJ*. diff --git a/tools/todo-progress b/tools/todo-progress new file mode 100755 index 00000000..8e01ad7b --- /dev/null +++ b/tools/todo-progress @@ -0,0 +1,22 @@ +#!/bin/bash +declare -a required=(gnuplot sponge) +for cmd in "${required[@]}"; do + command -v $cmd >/dev/null 2>&1 || { echo >&2 "Required '$cmd' not installed => aborting."; exit 1; } +done + +let budget=`grep '^| ... |' ' >.todo-progress.md +sed -e '1,/todo-progress begin:/d' -e '/todo-progress end./,$d' TODO.md >>.todo-progress.md +echo "| $(date --iso-8601) | $(printf "%6d" $budget) | $(printf "%7d" $effort) | $(printf "%8d" $output) | $(printf "%10d" $remainder) |" >>.todo-progress.md +echo '' >>.todo-progress.md +uniq <.todo-progress.md | sponge .todo-progress.md +sed -i -e '/todo-progress begin:/,/todo-progress end./!b' -e '/todo-progress end./!d;r .todo-progress.md' -e 'd' TODO.md + +sed -e's/^|//' <.todo-progress.md | tr '|' ';' | grep -v '|---' >.todo-progress.csv +gnuplot tools/todo-progress.gnuplot +rm .todo-progress.md .todo-progress.csv + diff --git a/tools/todo-progress.gnuplot b/tools/todo-progress.gnuplot new file mode 100644 index 00000000..69601fc0 --- /dev/null +++ b/tools/todo-progress.gnuplot @@ -0,0 +1,23 @@ +set xdata time # x-axis values are time (date) values +set timefmt "%Y-%m-%d" # date value format +set datafile separator ";" # CSV column separator is semicolon +set key autotitle columnhead # first data line contains column titles +set format x "%y-%m-%d" # display date format + +set xrange ["2022-07-11":"2022-10-31"] # x-axis value-range +set yrange [0:600] # y-axis value-range + +set key inside # graph legend style +set xtics rotate by -45 # rotate dates on x-axis 45deg for cleaner display +set title 'hsadmin-ng Projektfortschritt' # graph title + +set terminal png # output format +set term png size 920, 640 # output canvas size +set output 'TODO-progress.png' # output file name + +plot '.todo-progress.csv' using 1:2 with linespoints linetype rgb "black" linewidth 2, \ + '' using 1:3 with linespoints linetype rgb "red" linewidth 2, \ + '' using 1:4 with linespoints linetype rgb "green" linewidth 2, \ + '' using 1:5 with linespoints linetype rgb "blue" linewidth 2 + +