tl;dr: how do i implement a permissions model like (e.g.) github\'s
Updated to try to address some of @philipxy\'s comments:
Well it is problematic when you try to make a database behave the same way as something like Github. Databases have a completely different paradigm. It is why is is just as bad to design databases based on how C# works. Not the same functionality or rules. However it is an interesting question because too few people pay attention to the permission model as part of their database design and just grant everybody rights to everything because it is easier.
In the first place, users should not be able to grant rights to others, only admins should do that. Users should NEVER be creating objects if you have a good database design. Database design has no business being handled by amateurs.
Users can be individuals or you can use application users where all database requests coming from a specific application have the same rights. Applications can even have multiple generic users such as XYZAdmin, XYZUser, XYZReadonly (often used for senior managers who need to be able to see the data but aren't going to adjust it.). From my experience the biggest problems with the generic users is that it becomes difficult to audit who changed what in the database (very important in a regulated environment) and some users may have more permissions than they really need.
In databases you have several basic types of permissions. You have permissions that extend to the whole database and object level permissions. You also have some specific server permissions such as the permissions to use Bulk insert or execute jobs. Groups should also be given permissions concerning which databases they can see on the server as most database servers have multiple databases. So you can grant a user permissions to write to any table or grant them no rights to tables but only to specific stored procedures or views. In general administrative personnel get the overall rights to everything (or server level rights such a Bulk insert rights). This includes DBAs who have full rights and other specialists such as data analysts/senior developers/build team members who may only have rights concerning jobs and data entry or creating new objects but not rights to perform tasks such as setting permissions. All other users should be locked in to object level permissions.
Now no one should be given permissions as an individual (or at least it should be a rare thing). Each person should be in various groups which have rights. Individuals can be in more that one group and how those right interact between groups can be explained in the documentation associated with the specific database product you are using (yes this is specific by product, there is no one size fits all database permissions model.) The beauty of this is that when someone is no longer a user, you only have to remove them from the group(s) and the permissions are gone rather than searching out a multitude of individual permissions.
Now when you are handling permissions by groups, you need to define the groups. You may also need to define some additional views or stored procedures if you want a group to only have permissions on a subset of the records or columns instead of the whole table. If you have multiple clients in one database and need to make sure the permissions are only for one specific client, then you need to use stored procedures/views and grant permissions only at the stored procedure/view level not at the table level. Using views for permissions can get tricky because each database product has specific rules about what makes a view updateable. You need to understand this at a deep level to determine how you are going to manage permissions.
All database objects created will need to script the specific groups being granted permission to that object as part of the creation script. (You would never create database objects through any kind of GUI, always through scripts kept in source control.)
You can also do some permissions work by client by setting up some database tables with meta data about specific pages the user can access or specific clients that he is allowed to access. Then when the application is loaded, the users data from these tables is loaded and the application would decide what he or she could do. This is frankly easier but can be risky.
It means the users have to have table level permission and users really should not have those. Because the database level permissions are broader, it is easier for a malicious user to log in outside of the application and do things that he or she should not be able to do. This is something to be especially wary of if you have internal users who can easily log in through something other than the user's application such as SSMS for SQL Server.
If your database application is in a legally regulated field such as finance or health care, you need to be very strict about permissions and using ORMs instead of stored procedures is contraindicated because at no time should you be setting permissions at the table level.
Predicates and tables
A proposition is a statement that is true or false of a business situation. A predicate is a column-parameterized statement that given a row gives a proposition. A table (base or query result) holds the rows that make a true proposition from its predicate.
user (with id) U has name N
R is a grantor (may grant permissions)
user U has permission to update asset A
grantor R gave permission to grantor E to use an operator of type 'CRUD'
grantor E is of type 'user' AND grantor E has permission to update assets
Business rules
A business rule is an always-true statement that defines a term or describes a policy or process.
A user is uniquely identified by an id assigned when their cheque clears.
A crudable is an asset, group or organization.
A grantor is a user, group, organization.
"Grantee" refers to a grantor receiving or holding a permission.
Users can be in organizations.
You can make true statements that are parameterless predicates. These can use parameter names that are bound by FOR ALL
& FOR SOME
(THERE EXISTS
). Business rules phrased in terms of such propositional predicates and/or table names are database constraints. Given User(U,N)
& Grantor(R)
as shorthands for the first two predicates above as predicates for tables User
& Grantor
, the following lines all say the same thing:
A user is a grantor.
FOR ALL U, if U is a user then U is a grantor.
FOR ALL U, (FOR SOME N, User(U, N)) IMPLIES Grantor(U).
(SELECT U FROM User) ⊆ (SELECT R AS U FROM Grantor).
FOR ALL U & N, User(U, N) IMPLIES Grantor(U).
FOR ALL U & N, (U, N) IN User IMPLIES (U) IN Grantor.
FOREIGN KEY User (U) REFERENCES Grantor (R);
states what the above do (note its similarity to the middle two) plus that R is UNIQUE NOT NULL in Grantor.
Don't confuse rules with predicates. They have different uses & usually different forms. (A parameterless sentence template can be used as either.) A rule is a true statement; a predicate is a parameterized statement. Look at how my answer separates them. Base tables and query result tables have predicates. But a rule may suggest that you need a base predicate/table to record something. We have base predicates/tables when we see from a rule that we have to record some statements about the current situation. Note some rules inspire no base predicates.
You probably want to reify types and permissions.
A user is a grantor of type 'user'.
Permission named 'C' is permission for a grantee to create a crudable.
Grantor E is of type 'user'.
Permission P is of type 'CRUD'.
Grantor R gave permission P of type 'CRUD' on crudable C to grantee E.
Design is finding necessary & sufficient rules & base predicates
Here are relevant predicates to record situations that your exposition suggests arise.
- users
U identifies a user
- users can be in groups
G identifies a group
user U is in group G
- users can be in organizations
O identifies an organization
user U is in organization O
- groups can be in organizations
group G is in organization O
- a user will be permitted CRUD operations on an asset, group, or organization
A identifies a crudable of type 'asset'
user U is permitted CRUD operations on crudable C
5.1 as an individual user, or as a member of a group, or as a member of an organization (or as a member of a group where that group belongs to an org that has permissions),
P identifies a permission
organization O is permitted CRUD operations on crudable C
or because the asset/group/org is viewable (readable) to anonymous users ("public")
crudable C is public
- a user should also have a set of permissions to say whether they can set the above permissions
grantor R has permission to set CRUD permission for users on crudable C --?
What are "the above permissions"? Maybe you mean user CRUD permission and organization CRUD permission? Maybe you mean there are individual permissions for operations Create, Read, etc? You need to be clearer.
What are the permissions in "a set of permissions"? By "permission" here do you instead really mean "particular permission to a particular grantee"? You need to be more clearer.
The way to be clearer is to give rules & predicates that are as simple as possible but also not so simple that they don't mention relevant entities/values. You may afterwards want to generalize multiple rules & predicates into single ones. Eg instead of dealing with users, groups, organizations and assets, have grantors and crudables: Grantors may grant permissions.
& grantor R gives permission P to grantee E
. If some such permissions are also associated with specific grantees you might also need predicates like grantor R gives permission P to grantee E re permission Q and grantee F
.
6.1. a user can set the permissions for any asset, group, or org they create,
user U created crudable C
or any asset, group, or org for which they have been given permission to set permissions.
user U has permission to set permission P for crudable C --?
You will want to record things like that user U has name N and ...
.
Learn about database design
Search re database/SQL subtyping/inheritance/polymorphism idioms. Eg user, group and organization are types of permission possessors & holders; I made them subtypes of a type grantor. Maybe you want some kind of permission target type that is the union of crudable & grantor. Maybe you want types of permissions. Maybe some permission permissions have associated grantees. Maybe 'C', 'R', 'U' & 'D' are permissions, and 'CRUD' is a type of permission. You probably want to record what grantor gave what permission to a grantee.
Later we can replace tables by their join if the join is on a shared PK/UNIQUE with the same set of values in both. When we can join on a PK/UNIQUE & FK we can replace tables by one like their join but with the FK nullable. There are yet other times we can replace multiple tables by one without problems. But first identify basic predicates.
Learn about relational database design. Follow some information design method. Best are members of the the NIAM/FCO-IM/ORM2 family. Peek at IDEF1X. Don't rely on products.
Learn about constraints. They follow from predicates and business rules. They are truths about possible business situations in terms of the predicates. Equivalently, they are truths about possible database states in terms of the tables. Also learn about constraints in SQL, both declarative (PK, UNIQUE, FK) & triggered.
It seems to me that you need to create the concept of an entity which can receive permissions on a repository or an organization.
In this model the problem becomes relatively simple, as the permissions would revolve around an RepositoryPermissions
table of the form:
(EntityId, RepositoryId, canCreate, canRead, canUpdate, canDelete)
and a OrganizationPermissions
table of the form:
(EntityId, OrganizationId, canCreate, canRead, canUpdate, canDelete)
There are two types of Entities Groups
and Users
, and the permissions of any user will need to be checked in four ways:
the following query should retrieve all permission entries pertaining to the user someUserId
on repository someRepoId
SELECT
rp.canCreate,
rp.canRead,
rp.canWrite,
rp.canDelete
FROM RepositoryPermissions AS rp
Left JOIN Users AS u ON u.EntityId = rp.EntityId
Left JOIN Groups AS g ON g.EntityId = rp.EntityId
Left JOIN GroupUsers AS gu ON gu.GroupId = g.GroupId
WHERE rp.repositoryId IS "someRepoId" AND (
u.UserId IS "someUserId" OR
gu.UserId IS "someUserId"
)
UNION
SELECT
op.canCreate,
op.canRead,
op.canWrite,
op.canDelete
FROM Repositories AS r
JOIN OrganizationPermissions AS op ON r.repositoryId = op.repositoryId
Left JOIN Users AS u ON u.EntityId = op.EntityId
Left JOIN Groups AS g ON g.EntityId = op.EntityId
Left JOIN GroupUsers AS gu ON gu.GroupId = g.GroupId
WHERE r.repositoryId IS "someRepoId" AND (
u.UserId IS "someUserId" OR
gu.UserID IS "someUserID"
)