Database design - granting access to records - database

I am designing a database that restricts access to certain objects. My colleague and I have discussed different ways to approach this, with there being to main candidates: 1) Implicit access, and 2) Explicit access
For illustration, let's assume there are the following tables:
User (Id)
ObjectA (Id, ParentId) -- Where ParentId is an ObjectA
ObjectB (Id, ObjectAId)
UserObjectA (UserId, ObjectAId) -- Grants access to an ObjectA
UserObjectB (UserId, ObjectBId) -- Grants access to an ObjectB
Implicit approach:
Because ObjectA serves as a containing entity for ObjectB, if a user has access to an ObjectA that is the container for an ObjectB, then he also has access to the contained ObjectB, even though there is no such explicit record in UserObjectB.
Similarly, if a user has access to a parent ObjectA, then he has access to all ObjectA descendants, even though there is no such record in UserObjectA.
Additionally, if a user has no records in either access-granting table, then implicitly he has access to all records in ObjectA and ObjectB.
Explicit approach:
To have access to either an ObjectA or ObjectB record, a user must have a record in UserObjectA or UserObjectB, respectively.
No record equals no access, period.
The implicit approach has two benefits: 1) Save space when a user has access to many objects implicitly, and 2) a user who implicitly has access to all objects will have access to all objects added in the future without having to trigger inserts or handle the inserts in sprocs when objects are created.
The explicit approach has the benefit that queries are much simpler, maintanable, and more performant.
Initially, we ran with the implicit approach. However, after getting into the implementation of various sprocs, the logic to handle the access is becoming a beast and there are various subtleties that we've run into that make this approach more error-prone than the explicit approach. (Note that the real scenario is somewhat more complicated than the simplified example.) I'm finding myself constantly implementing recursive CTEs to determine access, which doesn't allow me to (when considering performance) abstract away certain parts of the logic in views or inline TVFs. So I have to repeat and tweak the error-prone logic in lots of different sprocs. If anything ever changed, we'd have a big maintenance task on our hands.
So, have we made a mistake going with this implicit access approach? I'm definitely having second thoughts and would appreciate advice form anyone who has experience with similar design decisions.

If you can wait a month Postgres 9.5 will be out and has Row Security. Oracle has it now if you have ten million bucks kicking around.
For now, or in other dbs, you can mimic row security:
Each protected table gets an "owner" column. By default only the owner can select, update, or delete that row.
Each "child" table also has an owner column, with a cascading foreign key to the parent table. So if change parent.owner, then this changes all children.owners as well
Use updateable CHECK OPTION views to enforce security.
You need to set current_user from your application. Here's how for pg + spring
In Postgres:
create schema protected;
create table protected.foo (
foo_id int primary key,
bar text,
owner name not null default_current user
);
create table protected.foo_children (
foo_child_id int primary key,
foo_id int not null references foo(food_id),
owner name not null default current_user references foo(owner) on update cascade
);
Now some CHECK OPTION views - use security_barrier if postgres:
create view public.foo with (security_barrier) as
select
*
from protected.foo
where
owner = current_user
WITH CHECK OPTION;
create view public.foo_children with (security_barrier) as
select
*
from protected.foo_children
where
owner = current_user
WITH CHECK OPTION;
grant delete, insert, select, update on public.foo to some_users;
grant delete, insert, select, update on public.foo_children to some_users;
For sharing, you need to add some more tables. The important thing is that you can index the right columns so that you don't kill performance:
create schema acl;
create table acl.foo (
foo_id int primary key references protected.foo(foo_id),
grantee name not null,
privilege char(1) not null
);
Update your views:
create or update view public.foo with (security_barrier) as
select
*
from protected.foo
where
owner = current_user
or exists ( select 1 from acl.foo where privilege in ('s','u','d') and grantee = current_user) );
--add update trigger that checks for update privilege
--add delete trigger that checks for delete privilege

Related

Issue updating sequences

I want to update the sequences for a table in RDS Postgres 11. Tried the following commands but I don't see the changes committed to the db. I even used commit.
What am I missing?
1. SELECT setval(pg_get_serial_sequence('table1', 'id'), coalesce(max(id),0) + 1, false) FROM table1;
2. SELECT setval('table1_id_seq', (SELECT COALESCE(max(id), 0) + 1 FROM table1));
ALTER TABLE table1 ALTER COLUMN id SET DEFAULT nextval('table1_id_seq');
CREATE TABLE public.table1 (
id serial4 NOT NULL, --default nextval('table1_id_seq'::regclass)
account_id int4 NOT NULL,
CONSTRAINT table1_pkey PRIMARY KEY (id) );
select currval('table1_id_seq') returns 6.
If you ...
don't see the changes committed to the db
... even though you positively committed the transaction (and you are connected to the right database), then I see only two possible explanations.
1. Barking up the wrong tree
In your question, public.table1 is schema-qualified in the CREATE TABLE statement, but not in either of:
SELECT setval(pg_get_serial_sequence('table1', 'id'), ...
SELECT setval('table1_id_seq', ...
If you have another table1 in another schema that comes before 'public' in the search_path, you end up modifying the respective sequence of that table.
Since the factory default in Postgres is search_path = "$user",public, the obvious suspect is a table of the same name in the "home" schema of the current role. See:
How does the search_path influence identifier resolution and the "current schema"
Solution:
Fix the search path or schema-qualify table and sequence names.
2. Missing privilege
It should be safe to assume your database role has SELECT (or even all) privileges on table1. But you need separate, additional privileges on the underlying SEQUENCE to run setval() on it.
You should see an error message for missing privileges, though!
See:
Explicitly granting permissions to update the sequence for a serial column necessary?
Solution:
GRANT USAGE ON SEQUENCE table1_id_seq TO the_role; -- your role here
Or work with an IDENTITY column instead of a serial (Postgres 10+), which inherits privileges for the table implicitly. See:
Auto increment table column
How to reset Postgres' primary key sequence when it falls out of sync?

Can I use Inheritance of PostgreSQL for getting a particular set of columns in many tables in one database?

In Postgresql 10 , I want to have same set of columns for audit purpose in all transactional tables of a particular database with same Foreign Key Constraints.
I am thinking of creating a master table with the set of 4 columns:
createdBy createdOn updatedBy updatedOn
Then inherit all transactional tables from this master table.
Is this the right approach and is inheritance suited for this? When it comes to storage of data, how it works behind the scenes when I insert records into the derived/child tables. What happens when data is deleted from child tables. Can I lock my master table so that no one accidentally deletes any records from master table ?
I see no problem with that approach but it works differently from your description.
I will use the following tables for illustration:
CREATE TABLE MasterAudit (
createdBy TEXT DEFAULT current_user,
createdOn TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp,
updatedBy TEXT DEFAULT current_user,
updatedOn TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp
);
CREATE TABLE SlaveAudit (
Val Text
) INHERITS(MasterAudit);
This definition allows to skip columns when inserting / use the default keyword for inserts and updates.
What does SELECT do (visible when using EXPLAIN)?
Behind the scene, data inserted into SlaveAudit is stored into SlaveAudit; selecting from MasterAudit works with UNION of tables, including MasterAudit itself (it is valid to insert data into the parent table, although it would not make much sense in this very case).
SELECT * FROM SlaveAudit reads data from SlaveAudit. The additional column Val from SlaveAudit is returned.
SELECT * FROM MasterAudit reads data from MasterAudit UNION SlaveAudit. The additional column Val is not returned.
SELECT * FROM ONLY MasterAudit reads data from MasterAudit only.
Illustration aside, the correct way to select from MasterAudit is by using the pseudo-column tableoid in order to determine where each record comes from.
Be careful though, it can be very long to get if all your tables inherit from MasterAudit
SELECT relname, MasterAudit.*
FROM MasterAudit
JOIN pg_class ON MasterAudit.tableoid = pg_class.oid
Let's insert stuff.
INSERT INTO SlaveAudit(Val) VALUES ('Some value');
What query will result in deleting it?
DELETE FROM SlaveAudit will remove that record (obviously).
DELETE FROM MasterAudit will remove the record too. Oops! that is not what we want.
TRUNCATE TABLE SlaveAudit and TRUNCATE Table MasterAudit will have the same result as the 2 DELETE.
Time to manage access.
IMHO, no commands apart from SELECT should ever be granted on MasterAudit.
Creating a table that inherits MasterAudit can only be done by its owner. You may want to change the tables' owner.
ALTER TABLE MasterAudit OWNER TO ...
Almost all the privileges must be revoked. It includes the table owner (but please note the super user will not be affected). SELECT on MasterAudit may be granted to everyone if you want.
REVOKE ALL ON MasterAudit FROM public, ...
GRANT SELECT ON MasterAudit TO public
Check the access by ensuring the following queries fail:
INSERT INTO MasterAudit VALUES(default, default, default, default)
DELETE FROM MasterAudit

SET IDENTITY_INSERT ON/OFF needed on application server, but ALTER permission seems dangerous. Suggestion?

We are building a multi-user web app where they need an unique postId for post they create. Each post has (userId, postId) as the compound primary key.
Right now, postId is an identity value, but because of the need to support some operations that require postId to be inserted as is (no re-numbering), we decide to use SET IDENTITY_INSERT ON/OFF.
However, our DBA told us that such operation is not meant be used by the application server because the ALTER permission requirement:
Permissions
User must own the table or have ALTER permission on the table.
https://msdn.microsoft.com/en-ca/library/ms188059.aspx
If the application server got hacked, with ALTER permission it seems rather risky. Our DBA suggests us to not use identity value at all, and locally generate an unique postId per user.
Can SET IDENTITY_INSERT ON be left on globally?
If it can't be left on globally, does avoiding identity value and use local generation of postId (per user) with max(postId)+1 per user make sense? We much prefer to use identity value if possible because we are worried about possible deadlocks and performance issues associated with custom postId generation.
Starting with SQL Server 2012 you can use sequences like in Oracle. You may be better off with those. First, create the sequence:
CREATE SEQUENCE mySeq AS LONG START WITH 1 INCREMENT BY 1;
GO
Then have the table's primary key default to the next sequence value (instead of being an IDENTITY value):
CREATE TABLE myTable (
myPK LONG PRIMARY KEY DEFAULT (NEXT VALUE FOR mySeq),
myWhatever...
);
If you don't specify a PK value with an INSERT you'll get a unique, generated sequence value. It's basically the same behavior as an IDENTITY. But if you want to specify a PK value you can, as long as you don't violate the primary key's uniqueness - but again, that's the same behavior as an IDENTITY with SET IDENTITY INSERT ON.
It sounds like you need to evaluate your database design if this is possible. A post should be a fixed entity and an identity column as a single primary key should be sufficient. In your comment you mentioned that you might want to copy posts from one user to another user. If you want to split the post so that user1 and user2 can independently control their own versions of the post, then it's just a matter of copying all the post attributes into a new record (which creates a new identity key) and then updating the new records user attribute from User1 to User2. But if you want the users to share the same post... then you should do that with a relationship from user to post to avoid the need to maintain duplicate data in your post table. In other words, if you want to assign user1 and user2 to an identical version of the post, then create a relationship table with two fields (Post ID, User ID). This way you can simply add a user to the post by inserting a new record into the relationship table.
Example: Post 1 is owned by user 1. Post 2 is owned by user 1 and 2.
Post Table - Key (Post ID)
(Post ID=1, PostText="This post is important!")
(Post ID=2, PostText="This post is also important!")
Users - Key (User ID)
(User ID=1, Name="Bob")
(User ID=2, Name="George")
Post Users - Key (Post ID, User ID)
(Post ID=1, User ID=1)
(Post ID=2, User ID=1)
(Post ID=2, User ID=2)
This concerns me a little:
"...the need to support some operations that require postId to be inserted as is (no re-numbering)..."
I assume this sort of operation is the exception, and not the norm? I can also only assume that you're inserting the same post with the same Post ID into the same table without deleting the original? It's still not clear WHY you want to do this.
I really don't see why you'd need to worry about changing the Post ID if you're assigning posts to another user. Nothing else changes except the values in the User ID column by the sounds of it. Unless you mean you can have two or more posts with the same Post ID and the same User ID. If so, why?
To answer your questions:
No, IDENTITY_INSERT cannot be set globally. It is a per object, per session setting.
Using MAX(PostID) + 1 doesn't make sense. Is there a reason why IDENTITY(1, 1) doesn't work for the PostID column? You can re-seed if necessary.
Don't use application-generated UUIDs as key values. They make queries so much slower. At worst, use NEWSEQUENTIALID() in SQL Server if you absolutely want to use a UUID. UUIDs unnecessarily bloat tables and indexes, and with the exception of NEWSEQUENTIALID, are query performance killers.
What you could do is have a Primary key column simply called ID, and then a surrogate key called Post ID if that needs to be non-unique. That way, when you copy a post, the copy of the original gets a new ID, but still retains the original Post ID, but with no need to worry about doing anything unnecessary to the PK.
However, without any changes to the application or the DB, what I would suggest is using a stored procedure executed as the owner of the stored proc (which also owns the Posts table) to set IDENTITY_INSERT to ON only when absolutely necessary. Here is an example I've just written:
create procedure dbo.sp_CopyPost
(
#PostID INT
,#UserID INT
)
with execute as owner
as
begin
set nocount on;
set identity_insert dbo.Posts on;
begin tran
insert into dbo.Posts
(
PostID
,UserID
--Whatever other columns
)
values
(
#PostID
,#UserID
--Whatever other values
)
commit tran
set identity_insert dbo.Posts off;
select ##IDENTITY
end
That will do what you want, based on the current wording of your question, and the comments you've made.
If you need to essentially "plug the gaps" in your identity column, you will find Microsoft-recommended queries to do so in section B here:
https://msdn.microsoft.com/en-us/library/ms186775.aspx

Deleting related records in another table with "Where"-like considerations

I have a data table with a primary key called OptDefID. When a record in this table is deleted I need to go and delete all records from the Permissions table that have that OptDefID in the defID field (in Permissions). The tricky part for me is that the Permissions table does not have a primary key and holds lots of different kinds of permissions, and has a permissiontype field. I need to delete rows that have the OptDefID AND a permissiontype of OptDef.
Because I need to consider the permissiontype, I don't believe a Foreign Key Constraint is appropriate here (or is it?).
I've also considered creating a trigger, but am unsure how to get the OptDefID passed into the trigger.
I can do this via the application itself, but I feel like this should be a database level solution.
What's the best solution?
Say I want to delete from Permissions where defID is 20 and permissiontype is 'OptDef'. There may be another row in Permissions that has a defID of 20, but has a permissiontype of 'Member'. That show should not be deleted because it pertains to Members and not Opt data.
Storing table names in fields prevents foreign keys from working properly, as you have discovered.
I recommend you fix the root problem and separate these two foreign keys, so each of them can be individually enforced. For example:
CREATE TABLE Permissions (
...
OptDefId int,
MemberId int,
FOREIGN KEY (OptDefId) REFERENCES OptDef ON DELETE CASCADE,
FOREIGN KEY (MemberId) REFERENCES Members ON DELETE CASCADE,
CHECK (
(OptDefId IS NOT NULL AND MemberId IS NULL)
OR (OptDefId IS NULL AND MemberId IS NOT NULL)
)
)
The CHECK makes sure only one of the FKs is non-NULL and only non-NULL FKs are enforced.
Alternatively, you could avoid changing your current design and enforce this "special" FK through a trigger, but declarative constraints should be preferred to triggers when possible - declarative constraints tend to leave less room for error and be more "self-documenting".
In case the OptDefId column is only filled when the record in question references the Permissions table, a foreign key should be appropriate. I.e. you have another column MemberId, which in turn could be a foreign key on a Members table.
It is only when you have a single column - let's call it ObjectId - which takes on other meanings as the contents of the type column change, that you cannot use foreign keys.
In that case, a trigger would probably be the best approach, as you already guessed. I only know about triggers in Oracle PL/SQL, where they are passed in as separate, complete rows representing the old and new state. I guess it will be analogous in MS-SQL-Server.
In addition to using join with SELECT statements, you can also join multiple tables in DELETE & UPDATE statements as well.
As I understand the issue, you should be able to join the Permissions table to the table with the OptDefID column & add a WHERE clause similar to the this:
DELETE MyTable
...
WHERE [Permissions].permissiontype = 'OptDef'
Also, these links may be of interest too:
SQL DELETE with JOIN another table for WHERE condition (for MySQL, but still relevant)
Using A SQL JOIN In A SQL DELETE Statement
Using A SQL JOIN In A SQL UPDATE Statement

SQL Server: Can the same table exist in multiple schemas

I thought that the schemas are namespace instances and hence the same table created under 2 different schemas are 2 different objects from the perspective of the database. One of my colleagues claim that schemas are nothing but a security container, hence we can create the same table in different schemas. Is this true?
You are correct.
CREATE TABLE foo.T
(
c int
)
and
CREATE TABLE bar.T
(
c int
)
creates 2 separate objects. You could create a synonym bar.T that aliases foo.T though.
CREATE SCHEMA foo
GO
CREATE SCHEMA bar
GO
CREATE TABLE foo.T(c INT)
GO
CREATE SYNONYM bar.T FOR foo.T;
INSERT INTO foo.T VALUES (1);
SELECT * FROM bar.T;
They are 2 different objects, check the object_id
Yes, it can. Just try it
CREATE SCHEMA OneSchema AUTHORIZATION dbo;
CREATE SCHEMA TwoSchema AUTHORIZATION dbo;
CREATE TABLE dbo.SomeTable (foo int);
CREATE TABLE OneSchema.SomeTable (foo int);
CREATE TABLE TwoSchema.SomeTable (foo int);
A schema is both a securable and part of the "namespace"
I guess that you are trying to solve an issue by dividing data with the same data-structure between different tenants. You don't want to use different databases to reduce costs.
To my mind, it is better to use row-level security in this case. In this case, data are stored in one table, but one tenant can't access data that were created by another tenant.
You could read more in the next article - Row-Level Security
myschema.table1 is different than yourschema.table1

Resources