Warning
Authorization library for Minos authorization language. This repository contains the language definition, the parser and the authorization engine.
...
The Minos lang is a Domain-specific language, created for write authorization policies.
To describe the syntax we start with the next file:
/* in example.minos */
syntax = 0.16;
/* Resource declaration */
resource User {
/* Environment declaration */
env DEFAULT {
/* policy declaration */
policy {
/* Permissions granted if at least one rule is met */
allow = ["create", "read", "update", "delete"];
/* Rule declaration */
rule {
actor.type = RootUser;
}
rule {
actor.type = resource.type;
actor.id = resource.id;
}
}
}
}
First, comments in the minos files are written between the /*
and */
characters, as in CSS.
/*in example.minos */
Each minos file needs to declare the version of the syntax used at the beginning of the document.
syntax = 0.16;
...
Now, we can comeback to the all file:
syntax = 0.16;
resource User {
env DEFAULT {
policy {
allow = ["create", "read", "update", "delete"];
rule {
actor.type = RootUser;
}
rule {
actor.type = resource.type;
actor.id = resource.id;
}
}
}
}
The block describes a resource named User
with an only one environment named DEFAULT
. Is important to indicate that if the DEFAULT
block exist, will be apply in every authorization process.
Based on the above, we can rewrite the block without environments:
resource User {
policy {
allow = ["create", "read", "update", "delete"];
rule {
actor.type = RootUser;
}
rule {
actor.type = resource.type;
actor.id = resource.id;
}
}
}
The code will be parsed equals to previous example but in this case the DEFAULT
block is implicit. This feature is named "implicit default".
The environments provide encapsulation for policies. To exclude an "default behavior" pattern, is possible use named environments. We can modify the example, and define two environments: Testing
and Production
.
resource User {
env Testing {
policy {
allow = ["create", "read", "update", "delete"];
rule {
actor.type = RootUser;
}
rule {
actor.type = resource.type;
actor.id = resource.id;
}
}
}
env Production {
policy {
allow = ["create", "read", "update", "delete"];
rule {
actor.type = resource.type;
actor.id = resource.id;
}
}
}
}
We can see, that in the Testing
environment, a RootUser
can manipulate the Users
, but in
Production
only the User
can manipulate it self.
Is important to mention that when exist two or more environment, use the implicit DEFAULT
isn't possible. Since in this case the DEFAULT
block not exists and is necessary to indicate the environment's name in the authorization process.
Now, if we are sure that only need two environments, we can rewrite the code, using the DEFAULT
environment.
resource User {
env DEFAULT {
policy {
allow = ["create", "read", "update", "delete"];
rule {
actor.type = resource.type;
actor.id = resource.id;
}
}
}
env Testing {
policy {
allow = ["create", "read", "update", "delete"];
rule {
actor.type = RootUser;
}
}
}
}
This pattern makes it clear that it is an edge case, and allow us to remove the Testing
environment before deployment. But, we only do it because the rules are compatible and we want to use the DEFAULT
environment.
What if, we need a different behavior based not on environments but on the resources themselves? This is possible using the specification feature and the attributed resources.
In the following example, we can two different blocks:
resource File {
policy {
allow = ["read"];
rule {
actor.type = User;
}
}
policy {
allow = ["write", "delete"];
rule {
actor.roles *= ["admin"];
}
}
}
resource File {
id = "confidential.john.data.file.id";
policy {
allow = ["read"];
rule {
actor.id = "john.user.Id";
}
}
}
The first block defines a resource named File
. Based on the rules, the Users
can read a File
but only an actor with role "admin" can write or delete a File
.
In the second block, we find another declaration for File
. We can see some diferencies:
- the resource have an attribute named
id
, and for this is an attributed resource; - the resource have a policy that contains a conflictive rule, because the permission "read" is granted with two policies in the same environment.
How the authorization process works in this case? If the resource is a File
and its id is "confidential.john.data.file.id", the rules for File
will be ignored, and will only apply the policy with the matching id. The consequent of this, is that the File
with id "confidential.john.data.file.id" can't be overwrited nor deleted.
It is simple, if we want to define special rules for an specific resource, we just use the specification.
But If we want use the rules into DEFAULT
environment instead of ignore it, we can extend the first blocks like this:
resource File {
policy {
allow = ["read"];
rule {
actor.type = User;
}
/* start of adition --- */
rule {
resource.id = "confidential.john.data.file.id";
actor.id = "john.user.Id";
}
/* --- end of adition */
}
policy {
allow = ["write", "delete"];
rule {
actor.roles *= ["admin"];
}
}
}
The above code will be parsed since the actor with id "john.user.Id" can read the File
with id "confidential.john.data.file.id". We can see that the if the actor is an User
, the new rule is really useless. This is a problem with the use of actor.id
and/or resource.id
in the same environment: the restrictive rules can be skipped for more relaxed rules.
The use of id attribute in the last example can be useful only in case of the actor isn't an User
. So, we can work with the next data, using the above code:
{
"actor": {
"id": "john.user.Id",
"type": "Employee",
"groups": ["employees"],
"roles": ["admin"]
},
"resource": {
"id": "confidential.john.data.file.id",
"type": "File",
"owner": "john.user.Id"
},
"permissions": ["read"]
}
The authorization is granted, because the rules cover this case exactly. So, what if we have one confidential file for every employee? We must be to write one rule for every file-employee couple? Not really, we can use the actor.owner
attribute, that's be convenient supported in currently minos lang version (v0.16).
resource File {
policy {
allow = ["read"];
rule {
actor.type = User;
}
/* start of change --- */
rule {
resource.owner = actor.id;
}
/* --- end of change */
}
policy {
allow = ["write", "delete"];
rule {
actor.roles *= ["admin"];
}
}
}
Additionally, is possible to enhance the rule, adding the actor.type
requirement:
/*... */
rule {
actor.type = Employee;
resource.owner = actor.id;
}
/*...*/
The attribute comparison is an excellent way to avoid the common places logic duplication. Unfortunately, in currently minos lang version (v0.16) isn't many supported attributes. But we hope to improve support for this feature in future releases.
At this point it is important to explain how minos parser works with blocks with the same identifier. For example, the code of [this section](#Use of id attribute in rules) can be rewrite like this:
resource File {
policy {
allow = ["read"];
rule {
actor.type = User;
}
rule {
resource.id = "confidential.john.data.file.id";
actor.id = "john.user.Id";
}
}
}
resource File {
policy {
allow = ["write", "delete"];
rule {
actor.roles *= ["admin"];
}
}
}
How minos parses the above text?
- Search for resources.
- Add
File
resource. - Search for environments.
- Add implicit
DEFAULT
environment. - Search for policies.
- Add policy to allow permissions
["read"]
and two rules. - Search for more policies.
- Skip searching for more environments, because the block uses implicit
DEFAULT
. - Search for more resources.
- Find
File
resource. - Search for environments.
- Find implicit DEFAULT environment.
- Search for policies.
- Add policy to allow permissions
["write", "delete"]
and one rule. - Search for more policies.
- Skip searching for more environments, because the block uses implicit
DEFAULT
. - Search for more resources ...
This behavior is the same with resources in other files. For example, we can rewrite the above code like this:
/* in file.minos */
resource File {
policy {
allow = ["read"];
rule {
actor.type = User;
}
}
}
/* in file2.minos */
resource File {
policy {
allow = ["read"];
rule {
resource.id = "confidential.john.data.file.id";
actor.id = "john.user.Id";
}
}
}
/* in file3.minos */
resource File {
policy {
allow = ["write", "delete"];
rule {
actor.roles *= ["admin"];
}
}
}
The final parsing, will be exactly the same that the first example in this section.
Macros behave like abbreviations. And are zero cost in runtime, because are "expanded" during parsing.
WARNING: The macro syntax are only available in versions its ends with M
character. For example: version = 0.16M;
can use macros, but version = 0.16;
can't. Why? Because the files with macros needs special algorithms to expand its during parsing time and we can avoid this operations if us are sure that the files not use its.
For example, the next files will are equals, after parsed:
syntax = 0.16M;
#BASIC_USER_PERMISSIONS {
"read_status",
"update_status"
}
#ADVANCED_USER_PERMISSIONS {
"create",
"delete",
"sudo"
}
#BY_SELF_AUTH {
actor.id = resource.id;
actor.type = resource.type;
actor.status = Active;
}
#BY_ADMIN_AUTH {
actor.roles *= "admin";
actor.status = Active;
}
resource User {
env STD {
policy {
allow = [
#[BASIC_USER_PERMISSIONS]
];
rule {
#[BY_SELF_AUTH]
}
}
policy {
allow = [
#[BASIC_USER_PERMISSIONS],
#[ADVANCED_USER_PERMISSIONS]
];
rule {
#[BY_ADMIN_AUTH]
}
}
}
env ROOT {
policy {
allow = [
#[BASIC_USER_PERMISSIONS],
#[ADVANCED_USER_PERMISSIONS]
];
rule {
actor.type = SuperUser;
}
}
}
}
syntax = 0.16;
resource User {
env STD {
policy {
allow = [
"read_status",
"update_status"
];
rule {
actor.id = resource.id;
actor.type = resource.type;
actor.status = Active;
}
}
policy {
allow = [
"read_status",
"update_status",
"create",
"delete",
"sudo"
];
rule {
actor.roles *= "admin";
actor.status = Active;
}
}
}
env ROOT {
policy {
allow = [
"read_status",
"update_status",
"create",
"delete",
"sudo"
];
rule {
actor.type = SuperUser;
}
}
}
}
Interestingly, the first file is larger than the file not using macros. So why use macros, anyway? For the same reasons we divide our code into small functions: code reuse and ease of functionality extension.
Currently only be two macro types: macros with permissions, and macros with requirements. It's have specific rules to write its.
-
The last permission within a macro cannot end in a comma (
,
).#BAD_MACRO_DEFINITION { "create", "delete", "sudo", /* ❌ */ } #CORRECT_MACRO_DEFINITION { "create", "delete", "sudo" /* ✅ */ }
-
Every requirement within a macro must end with a colon (
;
).#BAD_MACRO_DEFINITION { actor.status = Active /* ❌ */ } #CORRECT_MACRO_DEFINITION { /* ✅ */ actor.id = resource.id; actor.type = resource.type; actor.status = Active; }
-
No macro can mix permissions and requirements nor add another minos structure inside it.
#INVALID_MACRO { actor.status = active; /* <----- requirement ❌ */ "create"/* <--- permisssion ❌ */ } #INVALID_MACRO_2 { resource User { <--- resource block ❌ */ policy { /*...*/ } } } #INVALID_MACRO_3 { #[INVALID_MACRO] /* <--- macro call ❌ */ }
-
The macros only be called inside allow blocks or inside rule blocks.
resource User { #[DEFAULT_ENV_MACRO] /* <---- bad macro calling ❌ */ env TEST { policy { allow = ["read"]; rule { actor.type = SuperUser; } } #[POLICY_MACRO] /* <---- bad macro calling ❌ */ } }
-
Never use semicolon after macro call; but inside allow blocks is possible to use comma.
policy { allow = [ #[DEFAULT_PERMISSIONS]; /* <---- Don't use semicolon! ❌ */ ]; rule { #[FOO] /* ✅ */ #[BAZZ] /* ✅ */ } } policy { allow = [ #[DEFAULT_PERMISSIONS], /* <---- Valid macro call ✅ */ "lock_data", #[ADVANCED_PERMISSIONS], /* <---- Valid macro call ✅ */ ] }
-
Never call macros with incompatible content.
#[ALLOW_MACRO] { "read", "update", } #[BY_SELF_AUTH] { actor.type = resource.type; actor.id = resource.id; } resource File { policy { allow = [ #[BY_SELF_AUTH] /*<--- invalid token found: "Requirement", expected: "String" ❌ */ ] rule { #[ALLOW_MACRO] /*<--- invalid token found: "String", expected: "Requirement" ❌ */ } } }