3

Context

A small web application with REST API and postgres as db, that has users, documents and teams. A user can do basic CRUD operations on document.

A user is always a part of a team. A team is generated on user signup. A team has at least one member and zero or more documents.

Policy model

CREATE TABLE
  IF NOT EXISTS policy (
    id         UUID UNIQUE              NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
    namespace  VARCHAR(64)              NOT NULL,
    subject    UUID                     NOT NULL,
    scope      VARCHAR(64)              NOT NULL,
    object     UUID                     NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL,
    UNIQUE (namespace, subject, scope, object)
  );

Example

| id          | namespace  | subject        | scope             | object         | 
| policy-uuid | "user"     | user-uuid      | "CreateDocument"  | team-uuid      | -> User can create document in team-uuid
| policy-uuid | "user"     | user-uuid      | "CreateTeam"      | app-uuid       | -> User can create team in app-uuid instance
| policy-uuid | "user"     | user-uuid      | "FindDocument"    | document-uuid  | -> User can find document-uuid (search)
| policy-uuid | "user"     | user-uuid      | "DeleteDocument"  | document-uuid  | -> User can delete document-uuid
| policy-uuid | "user"     | user-uuid      | "FindUser"        | user-uuid      | -> User can find user-uuid (search)
| policy-uuid | "user"     | user1-uuid     | "UpdateUser"      | user2-uuid     | -> User1 (admin) can update User2's info
| policy-uuid | "document" | document1-uuid | "LinkDocument"    | document2-uuid | -> Document1 can link to Document2

Checking

So a function new_team() will check if (user.uuid, "CreateTeam", app-uuid) is present in policy table and update_doc(doc: Document) will check (user.uuid, "UpdateDocument", doc.uuid).

Ignore namespace. It's an application level identifier, not used for checking policy.

Question

Are there any obvious pitfalls I haven't considered? And if so, is there a better way of modeling these kinds of policies?

2
  • 1
    is the policy table your actual table or an example for us to understand your explanation?
    – user284677
    Commented Dec 7, 2022 at 20:42
  • @Spyros Actual table minus a few metadata and auditing info stored in jsonb fields.
    – iovo
    Commented Dec 8, 2022 at 6:58

2 Answers 2

1

I've got some good news for you. Your intent is spot on. Your design is OK but never mind that. You've stumbled across something called ABAC or attribute-based access control (some called it policy-based access control). Either way, you don't need to implement it yourself because there are a lot of tools out there (vendors, open-source...) that will help you.

To begin with, there's the paradigm (which is essentially what you tried to design). NIST, the National Institute of Standards and Technology, defined it in their special publication: Guide to Attribute Based Access Control (ABAC) Definition and Considerations: NiST SP 800-162. No need to read through all that, though.

Then there are standards:

  • XACML, the oldest of all (2001) is one good option. You could read more about XACML on Stackoverflow where I've written extensively about it. Salesforce, where I work, uses XACML under the hood for record access control.
  • ALFA, the abbreviated language for authorization which has a much nicer syntax. Have a look here and here for examples.
  • Open Policy Agent is another framework that uses Rego as its language. It stems from the world of Kubernetes and is backed by the Cloud Native Computing Foundation. Its syntax is easy on developers though the policies become perhaps too technical.

There are vendor-specific options like Polar (from a company called Oso HQ) or Amazon's newest release, Cedar.

The main takeaway though is: don't do it yourself. Would you want to reinvent crypto or authentication? No, right? So don't write authorization logic from scratch.

1
  • 1
    Excellent resources, thanks! OPA seems to be the perfect fit for this use-case. It also has great integration story with Kubernetes and policy-as-code + gitops makes perfect sense for an evolving application.
    – iovo
    Commented Dec 8, 2022 at 14:09
2

Security policies can be implemented in many ways, depending on the requirements at hand. There are standards that can guide you through the process, but also many examples and products out there for you to use and not re-invent the wheel (see @David Brossard's answer for some references).

One thing that is not clear from your description is the user/team/policy relation. You say that a team is created upon user registration; when new_team() is called, you check whether a policy exists that allows the user to create a team. But the user does not exist yet (so that a policy to be already in place). So, how does this actually work?

Aside from that, your implementation does not seem to have any major issue from a security point of view.


As a side note, your policy table is a bit problematic from a design point of view:

  • it does not comply to 3NF
  • you use UUIDs as surrogate primary keys; that's too expensive resource wise. UUIDs are best used when you have to refer to records outside of a db for security reasons, but for table joins and saving space you may want to use the arithmetic surrogates as the primary id of your records. That means that you have to have both a number and a UUID for each record, but that's a minor issue comparing to your current design

An example of how I would implement it, is the following:

CREATE TABLE IF NOT EXISTS subjects (
 id INTEGER PRIMARY KEY,
 public_id UUID NOT NULL DEFAULT gen_random_uuid(),
 ...blah,
 ...blah
);

CREATE TABLE IF NOT EXISTS scopes (
 id INTEGER PRIMARY KEY,
 public_id UUID NOT NULL DEFAULT gen_random_uuid(),
 ...blah,
 ...blah
);

CREATE TABLE IF NOT EXISTS namespaces (
 id INTEGER PRIMARY KEY,
 public_id UUID NOT NULL DEFAULT gen_random_uuid(),
 ...blah,
 ...blah
);

CREATE TABLE IF NOT EXISTS objects (
 id INTEGER PRIMARY KEY,
 public_id UUID NOT NULL DEFAULT gen_random_uuid(),
 ...blah,
 ...blah
);

CREATE TABLE IF NOT EXISTS policies (
 id INTEGER PRIMARY KEY,
 public_id UUID NOT NULL DEFAULT gen_random_uuid(),
 namespace_id INTEGER NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
 subject_id INTEGER NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,
 scope_id INTEGER NOT NULL REFERENCES scopes(id) ON DELETE CASCADE,
 object_id INTEGER NOT NULL REFERENCES objects(id) ON DELETE CASCADE,
 created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
 UNIQUE(namespace_id, subject_id, scope_id, object_id)
);

With this design, joining tables can be done by using the number ids, and for your REST API you can use the UUIDs, gaining the benefits from both worlds.

1
  • Thanks for the insights! I didn't know using UUID as PK might have performance issues but going through some articles, it seems using int or other sequential numeric surrogate PK with public_id UUID for API use is lot better. Plus exposing PK is often a security risk anyways. Now I have a few tables to refactor. In the end, I chose not to re-invent the wheel and go with OPA.
    – iovo
    Commented Dec 8, 2022 at 14:09

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .