How can I determine permissions in a hierarchical organization? - postgresql

I’m trying to create performant logic for determining permissions within a hierarchical organization.
Employees are assigned to one or more units. Units are hierarchical with (theoretically) infinite depth (in reality it’s no more than 6 layers).
For example, employee Jane may be the Supervisor of the Accounts Receivable unit (a child of the Accounting unit), and also Member of the Ethics Committee (a child of Committees, which is itself a child of Office of the CEO).
As the Supervisor of Accounts Receivable, Jane should have permission to view personnel files of everyone else in Accounts Receivable, but not in the Ethics Committee since she’s just a Member. Similarly, regular employees of the Accounts Receivable unit should not be able to view one another’s profiles, though they’d all need permission to, say, view the accounting records of the company.
I imagine the database architecture for this will look something like:
| **employees** | **units** | **positions** | **assignments** | **permissions** |
| ------------- | ----------- | ------------- | --------------- | --------------- |
| id | id | id | employee_id | unit_id |
| name | name | title | unit_id | is_management |
| | parent_path | is_management | position_id | ability |
With that in mind, how can I write a performant query to determine which permissions Jane has over Sam, an Accountant in Accounts Receivable, versus over Bill, a Receptionist in Office of the CEO?
The closest I have is something like:
create function permissions(actor employees, subject employees) returns setof permissions as $$
begin
for unit_id in select unit_id from assignments where employee_id = subject.id loop
select permissions.name
from assignments
left join units on (unit.id = assignments.unit_id)
left join positions on (positions.id = assignments.position_id)
left join permissions on (
permissions.unit_id = units.id
and permissions.is_management = positions.is_management
)
where assignments.user_id = actor.id
and (units.parent_path = unit_id or units.parent_path #> unit_id)
end loop;
end;
$$ language plpgsql stable;

Related

How can I ensure that a join table is referencing two tables with a composite FK, one of the two column being in common on both tables?

I have 3 tables : employee, event, and these are N-N so the 3rd table employee_event.
The trick is, they can only N-N within the same group
employee
+---------+--------------+
| id | group |
+---------+--------------+
| 1 | A |
| 2 | B |
+---------+--------------+
event
+---------+--------------+
| id | group |
+---------+--------------+
| 43 | A |
| 44 | B |
+----
employee_event
+---------+--------------+
| employee_id | event_id |
+-------------+--------------+
| 1 | 43 |
| 2 | 44 |
+---------+--------------+
So the combination employee_id=1 event_id=44 should not be possible, because employee from group A can not attend an event from group B. How can I secure my DB with this?
My first idea is to add the column employee_event.group so that I can make my two FK (composite) with employee_id + group and event_id + group respectively to the table employee and event. But is there a way to avoid adding a column in the join table for the only purpose of FKs?
Thx!
You may create a function and use it as a check constraint on table employee_event.
create or replace function groups_match (employee_id integer, event_id integer)
returns boolean language sql as
$$
select
(select group from employee where id = employee_id) =
(select group from event where id = event_id);
$$;
and then add a check constraint on table employee_event.
ALTER TABLE employee_event
ADD CONSTRAINT groups_match_check
CHECK groups_match(employee_id, event_id);
Still bear in mind that rows in employee_event that used to be valid may become invalid but still remain intact if certain changes in tables employee and event occur.

Need something akin to DISTINCT ON (a) AND DISTINCT ON (b) where normal DISTINCT clause isn't working

Just as a preface, I've tried using SELECT DISTINCT ON(a, b) and SELECT DISTICNT ON (a) ... UNION SELECT DISTINCT ON (b) but neither worked, so I'm reaching out for a possible solution.
I have two tables, player and card (a card represents something like a hitman's contract, with a reference to the 'killer' and the 'victim' which both reference the player table).
When a killer successfully kills a victim, the victim's state is set to 'dead' and their card is freed (by setting the card's killer_id to NULL), entering a kind of "free card pool". The killer is then set to 'idle' (as in waiting for a new card to be assigned to them), and their now completed card is set to completed.
What I'm trying to do is assign idle players cards from the freed card pool, with the additionaly caveat that a player can never receive themselves as a target, i.e. JOINing card.victim_id ON player.id is not allowed. So performing:
SELECT c.id AS card_id, p.id AS player_id
FROM card c
FULL OUTER JOIN player p ON true
WHERE p.state = 'idle'
AND c.killer_id IS NULL
AND c.victim_id != p.id;
returns all possible combinations of cards and players. What I need though is for every card and every player to be combined uniquely, which is to say that every card is assigned to a player and every player is assigned a card, where no card_id or player_id has appeared in a previous row. I've created a DB fiddle here to illustrate my point.
So given the above query, say I get the following dataset:
| card_id | player_id |
|-------------|---------------|
| 2 | 1 |
| 4 | 1 |
| 2 | 3 |
| 4 | 3 |
I want to pare it down into one of the following:
| card_id | player_id | | card_id | player_id |
|-------------|---------------| OR |-------------|---------------|
| 2 | 1 | | 2 | 3 |
| 4 | 3 | | 4 | 1 |
where there are no duplicate values for either column, but the actual combo of card_id and player_id itself doesn't matter.
As stated before, I tried using both SELECT DISTINCT ON(card_id, player_id) and SELECT DISTICNT ON (card_id) ... UNION SELECT DISTINCT ON (player_id) but neither worked.
Any help would be much appreciated!

Joining two tables - filtering in 2 separate conditions

I have two tables:
Table 1
Customer|Seller Currency|Invoice #|Invoice Currency
ABC |USD |123 |MXP
I have a second table where I store the bank accounts of my Customer
table 2
Customer | Bank Account | Currency
ABC | BANK1 | MXP
ABC | BANK2 | INP
ABC | BANK3 | USD
I want to join these two tables so that when I am creating a dashboard I can show the following:
If the Customer ABC has a bank account in the currency of the invoice then show that bank account (in this case the result would be bANK1)
If the Customer Does NOT have a bank account in the currency of the invoice then show the bank account in the currency of the Customer (in this case it would be Bank3
Anything else does show "no valid bank account"
When I'm joining my tables it is bringing multiple records....how can I achieve this?
Let's call your two tables customer_invoices and customer_accounts.
You can use the following query with two LEFT JOINs (so, if no bank account is present we still keep the left data, and one GROUP BY (in case somebody has more than one bank account in a given currency, we don't show all of them):
SELECT
customer_invoices.customer,
customer_invoices.seller_currency,
customer_invoices.invoice_number,
customer_invoices.invoice_currency,
coalesce( min(account_invoice_currency.bank_account),
min(account_seller_currency.bank_account),
'no valid bank account') AS chosen_bank_account
FROM
customer_invoices
LEFT JOIN customer_accounts AS account_seller_currency
ON account_seller_currency.customer = customer_invoices.customer
AND account_seller_currency.currency = customer_invoices.seller_currency
LEFT JOIN customer_accounts AS account_invoice_currency
ON account_invoice_currency.customer = customer_invoices.customer
AND account_invoice_currency.currency = customer_invoices.invoice_currency
GROUP BY
customer_invoices.customer, customer_invoices.seller_currency,
customer_invoices.invoice_number, customer_invoices.invoice_currency ;
You'll get:
customer | seller_currency | invoice_number | invoice_currency | chosen_bank_account
:------- | :-------------- | -------------: | :--------------- | :------------------
ABC | USD | 123 | MXP | BANK1
You can check the whole setup at dbfiddle here
References:
COALESCE()
LEFT JOIN

Count rows from a related table

I have the following query to gather data for a report:
SELECT COUNT(*) as inspected,
count(*) filter(where status='fail') as failed,
count(*) filter(where status='deficient') as impaired,
count(*) filter(where status='pass') as passed,
device_types.name
FROM inspection_data
INNER JOIN devices ON devices.id=inspection_data.device_id
INNER JOIN device_types ON devices.device_type_id=device_types.id
WHERE inspection_id = 3
GROUP BY device_types.id
ORDER BY device_types.name
This query is working as intended (Though I'm sure could be optimized somewhat. SQL isn't my strong suit). The problem is that I now want to gather one more summary datum. I want to count the number of each device_type_id in the devices table for this location_id.
I'll try to map out the database tables:
| devices | device_types | inspection_data |
|:--------------:|:------------:|:---------------:|
| id | id | id |
| device_type_id | name | inspection_id |
| location_id | | device_id |
| | | status |
So when I run the query, I'm receiving results similar to this:
| inspected | failed | impaired | passed | name |
|:---------:|:------:|:--------:|--------|----------------------------|
| 6 | 0 | 2 | 4 | Air Sampling Type Detector |
| 9 | 1 | 1 | 7 | Alarm Bell |
And this is great. My hangup is that not all devices for a location have to be inspected during an inspection. So for example, let's say there are actually 15 "Alarm Bell" devices for this location, but only 9 were inspected as part of this inspection, as per the table above. How do I go about including another column in this output, named "total" with a value of 15 for the Alarm Bell device type, and so on for each of the device types in the report?
I hope I've adequately described what I'm trying to do. I am utterly stumped on how to go about this without running a second query, and I really don't want to do that unless absolutely necessary because it just clutters the code up even more.
I think you want a left join. However, I'm not sure what table goes first. My best guess is:
SELECT COUNT(*) as total,
COUNT(id.device_id) as inspected,
COUNT(id.device_id) filter (where status='fail') as failed,
COUNT(id.device_id) filter (where status='deficient') as impaired,
COUNT(id.device_id) filter (where status='pass') as passed,
dt.name
FROM devices d INNER JOIN
device_types dt
ON d.device_type_id = dt.id LEFT JOIN
inspection_data id
ON d.id = id.device_id AND
id.inspection_id = 3
GROUP BY dt.id
ORDER BY dt.name

Select rows of multiple tables via joins

I have some tables which are related to each others.
A short demonstration:
Sites:
id | clip_id | article_id | unit_id
--------------+------------+--------
1 | 123 | 12 | 7
Clips:
id | title | desc |
------------+--------
1 | foo2 | abc1
Articles:
id | title | desc | slug
------------+---------------------
1 | foo2 | abc1 | article.html
Units:
id | vertical_id | title |
------------------+-------+
1 | 123 | abc |
Verticals:
id | name |
-----------+
1 | vfoo |
Now I want to do something like below:
SELECT ALL VERTICAL, UNIT, SITE, CLIP, ARTICLE attributes
from VERTICAL, UNIT, SITE, CLIP, ARTICLE TABLES
WHERE vertical_id = 2
Can some one help me how can I use joins for this?
Here is a running example of possibly what you want: http://sqlfiddle.com/#!15/af63b/2
select * from
sites
inner join units on sites.unit_id=units.id
inner join clips on clips.id=sites.clip_id
inner join articles on articles.id=sites.article_id
inner join verticals on verticals.id=units.vertical_id
where units.vertical_id=123
The problem is, that the description you gave us did not clearly specify which columns to join:
(answered) Why does units have a link to site via site_id and sites a link back to units via unit_id?
(answered) Why does units have a link to verticals via vertical_id and verticals a link back to units via unit_id?
I am guessing that your data does not giva a consistent example to get rows using the join. For vertical_id=123 there is no corresponding entry in verticals.
Edit:
I corrected the SQL due to corrections within the question. With this the two questions are answered.
select s.id, s.clip_id, s.article_id, u.title, u.vertical_id, c.title, v.unit_id, c.desc, a.slug
from sites s
join units u on s.id = u.id
join clips c on u.id = c.id
join verticals v on c.id = v.id
join articles a on v.id = a.id
where v.vertical_id = 'any id'