OpenABAC is an open sourced attribute based access control system that largely follows the industry standard definition of ABAC. Forking and deploying this repo, developers can get an ABAC system up and running quickly and easily. The stand out feature of OpenABAC is a non technical friendly dashboard, aimed to lower the skill ceiling for system administrators to manage their enterprise's fork.
An attribute based access control (ABAC) is the granular cousin of the familiar and intuitive role based access control (RBAC) system. Where RBAC defines access control at the role level, and then assigning each user type to a specific role/roles, ABAC systems define access control as policies, then associating policies to users. For further reading, here is a good article providing a comparison between these two authorization frameworks.
In a team, it is common for team members to have a core task, say addressing tickets from customer support. However, it is not uncommon for teams to be subdivided further into smaller tasks, like, developing feature X, documenting feature Y, cleaning dirty data in DB Z, etc. These sub teams may need access to certain tools that other sub teams don't require. In RBAC, since access permissions are defined ahead of time, you'll need N number of RBAC policies for the N sub teams you have. It is also not uncommon for another team outside the current team to access the same stuff, for example another M teams with only this permission in common might be contributing to documentation of feature Y. Suppose we want to revoke access to all contributors of feature Y, we will be required to change permissions in all M policies.
In ABAC, Actions
are the building blocks of Policies
, and Policies
are a grouping of allowed actions for the policy holder. Every task from the previous paragraph can be considered an Action
. In fact, it is best practice to further divide those tasks into components. For example, developing feature X might become
- Read access for X
- Write access for X
- Export access for X
- ...
Then, having your actions split into their smallest logical components, you may compose policies for various groups. Down the line, should a permission need to be revised, if your policies are correctly defined, revoking or simply updating the policy will cause all policy holders across M teams to be affected. Moreover, since policies are reusable, each team can just be thought of as a collection of policies. Adding and subtracting policies is trivial.
The "action oriented" nature of ABAC allows further access control that "user oriented" doesn't. Let's say, we want to allow access to the DB Z during work hours only. In RBAC, you would have to implement that logic in the application layer, polluting business logic with access control logic. In ABAC, a Context
defines a parameter under which the Action
requested shall be entertained.
Pro tip: To determine if an Action requires further breaking down, ask yourself, is there a potential for different users to only require a subset of this Action?
The key to defining policies intelligently is to understand your business case very well. Don't worry, OpenABAC provides APIs to create, delete, and modify policies. Your business logic can safely require changes to policies without any manual intervention.
This project is for you if:
âś… | You want system administrators to be able to help diagnose authorization issues with minimal training |
âś… | RBAC alone isn't sufficient |
âś… | You have an application where your users might need different permissions often |
âś… | You need a free and open sourced solution |
âś… | You want the convenience of a plug and play experience while leaving ability for optimization down the road |
âś… | You don't mind self hosting the ABAC system |
This project is not for you if:
❌ | You want a hosted solution |
❌ | Your application doesn't involve multiple entities accessing and modifying shared sensitive data |
❌ | You don't want to manage the complexity of an ABAC system |
❌ | You don't forsee access requirements changing often in your application |
JWTs are an industry standard for securely transmitting data. A private key, only known to the application and authorization service (OpenABAC) is used to digitally sign some data. Consider 2 sources of the same data, one properly signed with a private key and the other one a mere copy of the data, the data that was signed can be guaranteed to have come from the application's computation, while the mere copy will be identified and can be deemed illegitimate. OpenABAC is powered by this very principle.
To use OpenABAC, you will host a copy of OpenABAC. You'll also have your application. Imporatantly, it is extremely common for applications to have a method to identify users. By far, the most common method is UUID.
In the simplest implementation, when your user requests for some sensitive data from your application, your application will make an external api call to your hosted OpenABAC service for authorization. That external API call needs to contain a JWT signed Bearer token of the user's unique identifier, and in the url, the unique actionName
your user is trying to perform. Between these 2 components, you'll tell OpenABAC the action your user is requesting to perform, and a non-fraudulent identification of the user. OpenABAC will then return a response telling you if your user has a policy that includes the actionName
. Since the JWT was cryptographically signed by a private key only your server and your copy of OpenABAC knows, any successful JWT decoding on the ABAC side can be assumed to come from your server. To ensure that your JWT encoding stays secret, be sure to communicate via HTTPS and to rotate private keys regularly.
In advanced implementations, if your applciation runs on Typescript and you don't mind starting a MySQL data connection, you may copy the folder from /abac
and all of its contents into your project. This folder contains all the logic for authorization. Doing so, your application will reduce some latency since you won't be making an authorization request from a separately hosted service.
The authorization model in OpenABAC is simple. Whenever you need to know if a particular user of your application is allowed to perform a requested action, just send over the actionName
and the applicationUserId
. In the background, OpenABAC will efficiently lookup this actionName
among all the possible actions this applicationUserId
has a policy for, and if there's a match, an authorized message is sent back to the authorization caller (usually your apis).
An action is the smallest unit of work that your application will perform. To decide what should be an action and what shouldn't, consider whether the action in question needs to be restricted. If yes, it should be included, otherwise, it should be assumed that the public has access to this action. A detail that is often underdiscussed is the maintenance of these actions. Indeed, your application needs to be aware of these actionName
s - the authorization model assumes you pass in the requisite actionName
, so it implies your application is aware of these actionName
s. Smartly maintaining this is key to reducing bugs in your authorization scheme.
actionName
s have to be unique across the abac - otherwise authorization will not be effective. The action name is a 255 character field, and OpenABAC is not opinionated on how you should construct good action names. A suggested technique is to logically modularize your authorization requirements, inspired by directories and URLs. Being proficient in your application's requirements will help you make smart decisions and "feature-proofing" your action name schema.
A policy is a group of actions. Policies exist so that instead of assigning all the actions to a user, the policy will allow you to do less work to assign all the same actions. The benefit to such a grouping is a reduced mental overhead for development and debugging, and less overall work if an identical set of actions can be reused. A good way to think about policies are the "things" that are actually "attached" to the user. By thinking as such, constructing good policies become more intuitive. As rules of thumb, good policies represent a minimal set of actions that will be commonly used together. The same action can be attached to multiple policies.
Policies can be further divided into an allow
policy or a deny
policy. Lets suppose you have a critical internal service that you don't want junior engineers to ever come close to. In addition to not provisioning a policy to access that critical service, you may even supply a deny
policy that wraps all the actions that you don't want junior engineers to touch. So, if a junior engineer ever requests for an action that belongs to the critical service, the deny policy kicks in and rejects the authorization request. By default, all policies are allow policies. When you attach a deny policy to an applicationUserId
, an check in the background makes sure that a contradiction doesn't happen - an allow policy with some action A will not be found in the deny policy. During an authorization request, deny policies are always checked before allow policies - if the action requested is found in a deny policies, the request is rejected immediately.
policyName
s, like actionName
s, are unique across the abac. Similar care and foresight should be practiced in deciding how policies should be named.
A Context is an additional parameter that needs to be cleared before authorization is approved in the event of a match in Action. It is essentially a second layer of authorization after an Action matches. This is strictly optional and Actions to Contexts can have a many to many relationship. There are 2 types of contexts, time based and text based. Time based contexts are usually used to restrict an action to only be available in a certain time frame. The available operators are >, <, >=, <=, BETWEEN, ==, !=. You are provided 2 time related fields timeValue1
and timeValue2
. You will only use both if the BETWEEN operator is selected, otherwise only timeValue1
will be used. Text based contexts will read from the User
's jsonCol
field. In textValue
, provide the field from jsonCol
that the Context should expect, then the operators >, <, >=, <=, IN, ==, != can be used to check for authorization. If the textValue
doesn't get found in the jsonCol
beloging to this user, an error gets thrown and authorization fails. Since Context
s are attached to Action
s, it is beneficial to think of Contexts as a furhter authorization after Action.
A User is probably the simplest concepts there is in OpenABAC. The applicationUserId
is the user id that you associate this user in your main business application. This field does not discriminate any of the methods of generating user ids, but each id has to be under 255 characters long. The jsonCol
is a JSON object that you may use only with Context
s discussed above. The jsonCol
is a good place to provide some additional data about this user that should influence authorization of this user. However, keep in mind, since Contexts are further authorizations after Actions, it is erroneous to think that "authorization can come from jsonCol
". The previous statement is only partly true. When thinking about setting up a good authorization scheme, do not focus on beefing up jsonCol
s to start. Instead, think about Action
s, and then any Context
s that actions require, then if the context is a text based context, think about jsonCol
s. However, as a general rule of thumb, do not treat the jsonCol
as a "replica" of the data from your main application - keep it as lean and as general as possible.
Contexts provide a rich mechanism to further validate a user's access to an action. In this section, we specify how each operator works and how they are evaluated in the backend. As briefly mentioned, there are 8 different operators available in OpenABAC and we provide an overview on how they work with the data fields like timeValue1
, timeValue2
, and textValue
.
- BETWEEN
- Required:
timeValue1
,timeValue2
in JavascriptDate
format. - Is evaluated with the server's locale time. The server's locale time must be in between
timeValue1
andtimeValue2
for authorization to pass. This implies that thetimeValue1
andtimeValue2
must be relative to the timezone that the server will be using.
- Required:
-
, <, >=, <=, ==, !=
- Required:
timeValue1
JavascriptDate
format ORtextValue
, but not both. NevertimeValue2
. - Evaluated as strings. The value from the jsonCol is operated on the context value. For example, lets say the jsonCol has a field
YOE: 5
and the context hasentity: YOE, textValue: 6, operator: <
, then during evaluation sequence is read as5 < 6
, which means the authorization fails. Say the operator was!=
, then the evaluattion sequence is read as5 != 6
which would be true and authorization passes.
- Required:
- IN
- Required: Comma separated values in
textValue
; think array without square brackets. If only 1 value, omit trailing comma otherwise an empty space will be part of the entity values. - If either one of the IN values matches the value from the jsonCol, this context is considered to pass. The evaluation is done using the
===
typescript evaluator. You may add a space between a comma and the next letter for readability - it will be handled during evaluation either way.
- Required: Comma separated values in
One of the out standing features of OpenABAC is the administrator UI. This UI allows less technical team members to avoid upskilling in OpenABAC's implementation and MySQL. In this section, we'll outline the various APIs available for consumption via your application. It will however not specify the APIs that are used within the UI.
As mentioned in Usage Pattern, authorization requests to OpenABAC requires a signed JWT with the user's application user id. Note, "application" refers to the app you're primarily building.
Note: All APIs assume the initial JWT signature check passed. If it didn't, OpenABAC will immediately return a
403: Forbidden Error
. Recall, every request needs to contain the user's application id in the JWT.
Context authorizations are done using the user's
jsonCol
object. If the user needs a certain action, it must also have the required contexts in thejsonCol
. Check the OpenABAC concepts above to see how contexts work in detail.
- The main authorization API
- Params:
actionName
is the unique action name that your user is requesting access for
- Note:
Contexts
conditions associated with the action has to be satisfied, otherwise it will fail.
- Returns:
authorized
: boolean. True if authorized, false otherwise.message
: string. An additional message if request is unauthorized.
- Gets all the actions associated with this user
- Returns:
actions
: string[]. A list of action all names that this user is allowed to do.
- Gets the entire user object including its associated policy names that are allowed policy type.
- Returns:
success
: boolean. Indication of successful update of the user.data
: string of json objectsid
(from ABAC),jsonCol
, list ofpolicies
- Creates the user object itself
- Body:
applicationUserId
: new application user id to use. Even if not updating this, must include in payload.jsonCol
: additional metadata that will only be used in verifyingContext
. Note thatContext
may only be checked via data from this jsonCol.
- Returns:
success
: boolean. Indication of successful creation of the user.data
: string. If this endpoint suceeds the created user is returned, otherwise error message found here.
- Upserts info on the user object itself - not this user's associated policies (for that check the next api).
- Body:
applicationUserId
: new application user id to use. Even if not updating this, must include in payload.jsonCol
: additional metadata that will only be used in verifyingContext
. Note thatContext
may only be checked via data from this jsonCol.
- Returns:
success
: boolean. Indication of successful update of the user.data
: string. If this endpoint suceeds the payload to this API is returned, otherwise error message found here.
- Upserts the UserPolicy mapping. Note that if any one of the policies don't exist, or any error in general, the upsert doesn't perform.
- Body:
policyNames
: A list ofpolicyName
. It takes care of traditional create, update, and delete endpoints. Include existing and new policies in the payload since this is an upsert operation.
- Returns:
success
: boolean. Indication of successful update of the user.data
: string. If this endpoint suceeds, the list ofpolicyNames
is returned here.
- At the time of deletion, the
User
can't have anyPolicy
attached. - Returns:
success
: boolean. Indication of successful update of the user.data
: string. If this endpoint suceeds, theapplicationUserId
is returned here.
- Creates policies. Commonly used in immediately with
/abac/createAction
to create policies out of newly created actions. This endpoint is a transaction behind the scenes. Meaning, either all your policies get created or none. - Body:
Note that
policyName
has to be unique. If either one isn't, a 409 conflict is returned
body.listOfPolicies: [
{
policyName : string
policyDescription: string
allow: boolean
},
{
...
}
]
- Returns:
success
: boolean. Indication of successful creation of ALL policies frombody.listOfPolicies
.message
: string. An additional message in case a policy fails to get created.
- Returns the policy object and the actions associated with this policy. Commonly used if you have a use case for reading more about the policy and or the policy's associated actions.
- Params:
policyName
: Name of the policy you want to read
- Return:
data
:actionsAssociated
: list ofactionName
spolicy
: The entire policy object
- Updates the policy object, not the PolicyActionMapping (that comes later). Must provide all the fields of a policy, not just the one you want to patch. A precondition to updating is the requester of this endpoint must own the policy, which will be checked in the backend.
- Params:
policyName
: Name of the policy you want to update
- Body:
policyName
: Has to be a unique name, otherwise a 409 conflict error is returnedpolicyDescription
: Description of the policyallow
: boolean
- Returns:
success
: boolean. Indication of successful update.message
: string. An additional message in case a policy fails to get updated.data
: The updated policy object
- Deletes a policy. No action can be attached to the policy at the time of deletion, otherwise a 400 error is returned
- Params:
policyName
: Name of the policy you want to delete
- Returns:
success
: boolean. Indication of successful deletion.message
: string. An additional message in case a policy fails to get deleted.
- Used to attach or remove actions in a policy (Upsert). The entire list of wanted
actionName
s must be provided. This is essentially an upsert operation. - Body
actionNames
: List ofactionName
to be set into a policypolicyName
: Name of policy to set actions into.
- Returns
success
: boolean. Indication of successful attachment of allactionName
intopolicyName
message
: string. An additional message in case any action fails to be attached to the policy.
Actions are not specific to any particular user since they are meant to be primitives to be reused by many users. As such, an extension to OpenABAC could be to implement a
sudo
user as the authorized requester. However, from a security perspective this isn't strictly necessary because by sending in signed JWTs, we can be sure the request came from the application's servers.
- Creates actions. Commonly used when a new resource is created and provisioning some actions is required. This endpoint is a transaction behind the scenes. Meaning, either all your actions get created or none.
Note that
actionName
has to be unique. If either one isn't, a 409 conflict is returned
body.listOfActions: [
{
actionName : string
actionDescription: string
},
{
...
}
]
- Returns
success
: boolean. Indication of successful creation of ALL actions frombody.listOfActions
.message
: string. An additional message in case an action fails to get created.
- Returns the action, policies associated with this action, and contexts associated with this action. The
applicationUserId
has to own this action otherwise it is unreadable. - Params:
actionName
: Name of the action to read
- Returns
success
: boolean. Indication of successful readdata
:policyList
: List ofpolicyName
action
: The action objectcontextList
: List ofcontextName
- Updates an action object - not the action-context mappings nor the policy-action mappings. Provide the entire action object since this is a PUT request
- Params:
actionName
: Name of the action to update
- Body:
actionName
: Updated action nameactionDescription
: Updated action description
- Returns:
success
: boolean. Indication of successful updatemessage
: string of error logs if anydata
: updated action object
- Deletes the action. This action must not be attached to any policy and context to succeed.
- Params:
actionName
: Name of the action to delete
- Returns:
success
: boolean. Indication of successful deletemessage
: string of error logs if any
- Used to attach or remove context in an action (Upsert). The entire list of wanted
contextName
s must be provided. This is essentially an upsert operation. - Body
contextNames
: List ofcontextName
to be set into an actionactionName
: Name of action to set contexts into.
- Returns
success
: boolean. Indication of successful attachment of allcontextNames
intoactionName
message
: string. An additional message in case any action fails to be attached to the policy.
Contexts are not specific to any particular user since they are meant to be primitives to be reused by many actions. As such, an extension to OpenABAC could be to implement a
sudo
user as the authorized requester. However, from a security perspective this isn't strictly necessary because by sending in signed JWTs, we can be sure the request came from the application's servers.
- Creates ABAC
Context
. Usage pattern varies. - Body:
contextName
- Unique across the entire system.
contextDescription
- Describes what this context does.
operator
- Operators can only be
BETWEEN
IN
<
>
<=
>=
==
!=
- Operators can only be
entity
- This refers to a field from the
jsonCol
field from the User object
- This refers to a field from the
- Depending on the operator, different values are used. The special cases are
BETWEEN
- In this case,
timeValue1
andtimeValue2
must be filled in andtextValue
must remain empty
- In this case,
IN
- In this case,
textValue
must not be empty and if this clause needs to contain multiple items, the items should be comma separated. DO NOT place square brackets or any other indication this is an array. ThetextValue
field is stored as a string.
- In this case,
- With every other operator, only
textValue
ortimeValue1
can be used.
- Return:
success
: boolean. Indication of successful creation of context.data
: a json object of the created context.
- Param
contextName
: The unique name of the context
- Gets the context and all the actions associated with it.
- Return
data
context
: The context objectactions
: A list of actions attaching the context
- Body
context
: The full context object since this is not a patch operation. If the intent is to updatecontextName
, if its not unique, an error will be thrown.
- Return
success
: Indication of successful updatemessage
: Additional message incase update fails
- Param
contextName
: The unique name of the context
- This
Context
cannot be attached to anyAction
at the time of deletion - Return
success
: Indication of successful updatemessage
: Additional message incase update fails
Ideally, the process of ABAC should be really quick, considering the frequency that it should theoretically be called in order to securely provide users ability to perform interaction with sensitive data. MySQL as the database of choice was a tradeoff between absolute performance and ease of development and maintenance. More enterprise scale databases such as Sql Server would've been a better choice from a performance and scalability standpoint, however to fit the requirement of being more accessible for development and maintenance, the open sourced MySQL database was a better choice. Ofcourse, it begs the question why not PosgtreSQL instead of MySQL, and it came down to the lack of need for added sophistication that PosgtreSQL offers.
ABAC applications can be expected to perform read heavy workloads. If you were to perform any one optimization after setting up OpenABAC, it is to introduce caching. Caching allows you to speed up the read performance of data by temporarily storing some data into a specialized in memory database (as opposed to the traditional file-based database which is slower, ie MySQL) such as Redis. We recommend using the Read-Through caching technique and a short time to live (TTL) for safety. However, the algorithm to invalidate data points in the cache should be decided by your application's usage pattern and careful monitoring over time.
An extension discussed in the Action and Context API sections were about implementing a "sudo" user when performing any API in those namespaces. Actions and Contexts are the primitives upon which Policies are built on top of. As such, they aren't "owned" by any one user in the same sense as a User owning a Policy. However, this is optional. The Action and Context APIs still expect to read a validly signed JWT in its payload at request time, if we may assume the JWTs weren't stolen, then we can safely conclude that the request came from your application's server, a valid source.
Speaking of JWTs, as a safety measure, we recommend reducing the lifespan of JWTs to as small as possible without severely impacting the performance of your app. There are 2 approaches to designing JWT invalidation. The first option is to dynamically generate JWTs per OpenABAC API call, trading off more compute for better security. The second option is to extend the TTL of each JWT then keep JWTs in a cache, trading off less compute for less security. Decide based on performance tolerance, compliance requirements, and usage patterns.
- Next14^
- Docker
- NodeJS (version that supports Next14)
DATABASE_URL_DEV=mysql://user1:root_password@localhost:3306/openabac
DATABASE_HOST_DEV=localhost
DATABASE_USER_DEV=user1
DATABASE_PASSWORD_DEV=password1
DATABASE_NAME_DEV=openabac
IS_PRODUCTION=false (Set to true in production)
USE_PRODUCTION_DB=false (Set to true if using Docker MySQL or in production)
JWT_SECRET=0aJFfCNpsvvlcIJ2DXlPjnZN8BD2OUXe0sgdfhR1IGp8jrH84kGCuZmGkV41vFW
First, set up a local database:
yarn run dev:up
Then, start the server:
npm run dev
# or
yarn dev
Close the database connection when you're done with:
yarn run db:down
Open http://localhost:3000 with your browser to see the result.
With applicationUserId:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBsaWNhdGlvblVzZXJJZCI6InN1ZG8iLCJleHAiOjE3NDI5NDU2MDMuNzQ1OTY2fQ.62v6FoEV3NCIKFYF7KB1rlRpyCzt219HLfE5PvXkiV0
{
"applicationUserId": "sudo123",
"exp": 1742945603.745966
}
Without applicationUserId:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDI5NDU2MDMuNzQ1OTY2fQ.egLrzcD3P3OYQlmpQBDTg8xGkMrPTn6zQ1iKL-Df0Ms