Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
simple-access
Advanced tools
Attribute-Role-Based Hybrid Access Control Library
V2.0 Breaking Changes
Theconditions
property has been removed from theaction
object due to its side effects. Instead, you can use thescope
property to add custom attributes and validate them using application logic.
npm install simple-access --save
Access control is a security technique that regulates who or what can view or use resources in a computing environment.
Access Control involves:
Role-Based Access Control is a policy-neutral access-control mechanism defined around roles and privileges assigned to a user.
RBAC is an additive model, so if you have overlapping role assignments, your effective grants are the union of your role assignments.
Example:
If we have an API the provides data to eCommerce application, we may have a role of manager
with access to a resource product
and allowed actions ["create", "read", "update"]
, and another role operation
with access to a resource product
and allowed actions ["archive"]
. So Any users with both manager
and operation
roles will be able to ["create", "read", "update", "archive"]
products.
Attribute-Based Access Control provides access to users based on who they are rather than what they do.
An entity capable of accessing resources.
An entity that contains and/or receives information to which access is controlled, like "Files", "Messages".
Role is the level of access given to subject (user or business entity) when this role assigned to it, it is properly viewed as a semantic construct around which access control policy is formulated.
Subject (User or business entity) can have one or more roles assigned based on their responsibilities and qualifications.
Role Schema
{
"name": "string",
"resources": [
{
"name": "string",
"actions": [
{
"name": "string",
"attributes": ["string"],
"scope": "object"
}
]
}
]
}
Scope is just an object that may include any metadata to be used but applications to evaluate additional constraints on subject based on application business logic.
Permission describes the way in which a subject may access a resource
Permission Schema
{
"granted": "boolean",
"access": {
"roles": ["string"],
"action": "string",
"resource": "string"
},
"grants": "object",
"attributes": ["string"],
"scope": "object"
}
This library does not handle roles storage and management (Not the library concern), It has a basic MemoryAdapter
implemented by extending BaseAdapter
, If you need to handle more complex scenarios you will probably need to extend BaseAdapter
implementing your own storage for example using [MySQL, MongoDB, Redis]
Memory adapter is a built-in roles adapter that stores all roles data into memory with simple structure.
You can implement your own roles adapter by extending BaseAdapter
class and implement getRolesByName
method, considering that the getRolesByName
can return Array<Role>
or Promise<Array<Role>>
and you can define the return type when creating your adapter class.
Example:
import { BaseAdapter, Role, ErrorEx } from "simple-access";
export class MemoryAdapter<
R extends [string, string, string]
> extends BaseAdapter<R, Array<Role<R>>> {
private _roles: Array<Role<R>>;
private _cache: { [k: string]: Role<R> } = {};
constructor(roles: Array<Role<R>>) {
super("MemoryAdapter");
this.setRoles(roles);
}
setRoles(roles: Array<Role<R>>): void {
if (roles == null || !Array.isArray(roles) || roles.length === 0) {
throw new ErrorEx(
ErrorEx.VALIDATION_ERROR,
`Missing/Invalid roles array in "${this.constructor.name}"`
);
}
this._roles = roles;
this._cache = {};
// Cache roles by name
this._roles.forEach((role: Role<R>) => {
// this.validateGrant(grant, true);
this._cache[role.name] = role;
});
}
getRoles(): Array<Role<R>> {
return this._roles;
}
getRolesByName(names: Array<Role<R>["name"]>): Array<Role<R>> {
const result: Array<Role<R>> = [];
if (names == null) {
throw new ErrorEx(
ErrorEx.VALIDATION_ERROR,
`Roles names array can not be null or undefined`
);
}
for (let i = 0; i < names.length; i += 1) {
if (this._cache[names[i]] != null) {
result.push(this._cache[names[i]]);
}
}
return result;
}
}
Let's use the following set of roles as an example:
import type { Role } from "simple-access";
type RoleNamesType = "administrator" | "operation";
type ResourceNamesType = "product" | "order" | "file";
type ActionNamesType = "create" | "read" | "update" | "delete"
type RoleDefinition = [RoleNamesType, ResourceNamesType, ActionNamesType]
const roles: Role<RoleDefinition>[] = [
{
"name": "administrator",
"resources": [
{"name": "product", "actions": ["*"]},
{"name": "order", "actions": ["*"]},
{"name": "file", "actions": ["*"]}
]
},
{
"name": "operation",
"resources": [
{
"name": "product",
"actions": [
{"name": "create", "attributes": ["*"]},
{"name": "read", "attributes": ["*"]},
{"name": "update", "attributes": ["*","!history"]},
{
"name": "delete",
"attributes": ["*"]
}
]
},
{
"name": "order",
"actions": [
{"name": "create", "attributes": ["*"]},
{"name": "read", "attributes": ["*"]},
{"name": "update", "attributes": ["*"]}
]
}
]
}
];
You can check access using can
method:
can(role: Array<string> | string, action: string, resource: string): Promise<Permission> | Permission
Check subject (with "operation" role) permission to "read" the resource "order", please note that can
method return type depends on the return type of getRolesByName
method in the adaptor you are using. In the following example the getRolesByName
method in the MemoryAdaptor
return type is Array<Role>
import {
type Role,
SimpleAccess,
MemoryAdapter
} from "simple-access";
import type {
RoleDefinition
} from './roles';
type RoleNamesType =
"administrator"
| "operation";
type ResourceNamesType =
"product"
| "order"
| "file";
type ActionNamesType =
"create"
| "read"
| "update"
| "delete"
type RoleDefinition = [RoleNamesType, ResourceNamesType, ActionNamesType]
const roles: Role<RoleDefinition>[] = [
{
"name": "administrator",
"resources": [
{
"name": "product",
"actions": ["*"]
},
{
"name": "order",
"actions": ["*"]
},
{
"name": "file",
"actions": ["*"]
}
]
},
{
"name": "operation",
"resources": [
{
"name": "product",
"actions": [
{
"name": "create",
"attributes": ["*"]
},
{
"name": "read",
"attributes": ["*"]
},
{
"name": "update",
"attributes": ["*", "!history"]
},
{
"name": "delete",
"attributes": ["*"]
}
]
},
{
"name": "order",
"actions": [
{
"name": "create",
"attributes": ["*"]
},
{
"name": "read",
"attributes": ["*"]
},
{
"name": "update",
"attributes": ["*"]
}
]
}
]
}
];
const adapter = new MemoryAdapter(roles);
const simpleAccess = new SimpleAccess<RoleDefinition, typeof adapter>(adapter);
const permission = simpleAccess.can("operation", "read", "order");
if (permission.granted) {
console.log("Permissin Granted");
}
The returned permission
{
"granted": true,
"access": {
"roles": [
"operation"
],
"action": "read",
"resource": "order"
},
"grants": {
"product": {
"create": {
"name": "create",
"attributes": [
"*"
]
}
}
},
"attributes": [
"*"
],
"scope": {}
}
can
function only checks if subject with assigned role operation can access the resource order through read action, It will not filter resource data, as this functionality provided through other functions and needs additional information to work properly.
Simple Access will merge overlapped roles before validation according to these details:
["administrator", "operation"]
then this subject will have access to these resource ["product", "order", "file"]
["read", "create"]
["read", "update"]
and role B with actions ["create", "delete"]
will be merged in one resource order with actions ["read", "update", "create", "delete"]
["*"]
will override attributes ["name", "age", "!address"]
["name", "age"]
will merge with attributes ["address"]
into new attributes array ["name", "age", "address"]
["*", "!address"]
will be merged with attributes ["age"]
into new attributes array ["*", "!address"]
["*", "!age"]
will be merged with attributes ["*", "!image", "!address"]
into new attributes array ["*"]
and ["age" , "image", "address"]
will be allowed because they are not negated from both sides["*", "!age"]
will be merged with attributes ["image"]
into new attributes array ["*", "!age"]
and "image"
attributes will be omitted because its by default allowedProjected (Allowed) attributes gets merged based on a union operation
Negated (Not allowed) attributes gets merged based on intersection operation
{}
will override all other scopes{"group": 123}
will be merged with scope {"tenant": 321}
into:{
"group": 123,
"tenant": 321
}
Example:
import {
type Role,
SimpleAccess,
MemoryAdapter
} from "simple-access";
import type {
RoleDefinition
} from './roles';
type RoleNamesType =
"administrator"
| "operation";
type ResourceNamesType =
"product"
| "order"
| "file";
type ActionNamesType =
"create"
| "read"
| "update"
| "delete"
type RoleDefinition = [RoleNamesType, ResourceNamesType, ActionNamesType]
const roles: Role<RoleDefinition>[] = [
{
"name": "administrator",
"resources": [
{
"name": "product",
"actions": ["*"]
},
{
"name": "order",
"actions": ["*"]
},
{
"name": "file",
"actions": ["*"]
}
]
},
{
"name": "operation",
"resources": [
{
"name": "product",
"actions": [
{
"name": "create",
"attributes": ["*"]
},
{
"name": "read",
"attributes": ["*"]
},
{
"name": "update",
"attributes": ["*", "!history"]
},
{
"name": "delete",
"attributes": ["*"]
}
]
},
{
"name": "order",
"actions": [
{
"name": "create",
"attributes": ["*"]
},
{
"name": "read",
"attributes": ["*"]
},
{
"name": "update",
"attributes": ["*"]
}
]
}
]
}
];
const adapter = new MemoryAdapter(roles);
const simpleAccess = new SimpleAccess<RoleDefinition, typeof adapter>(adapter);
const permission = simpleAccess.can(["operation", "support"], "read", "order");
if(permission.granted) {
console.log("Permissin Granted");
}
Simple Access can filter resource object data based on specific permission and return only allowed attributes.
Simple Access is depending on Floppy Filter library for complex object filtering, Please check its documentation to know how to select or negate attributes in an efficient way.
You can do this using filter
function:
filter(permission: Permission, data: Object): Object
Example:
import {
type Role,
SimpleAccess,
MemoryAdapter
} from "simple-access";
import type {
RoleDefinition
} from './roles';
type RoleNamesType =
"administrator"
| "operation";
type ResourceNamesType =
"product"
| "order"
| "file";
type ActionNamesType =
"create"
| "read"
| "update"
| "delete"
type RoleDefinition = [RoleNamesType, ResourceNamesType, ActionNamesType]
const roles: Role<RoleDefinition>[] = [
{
"name": "administrator",
"resources": [
{
"name": "product",
"actions": ["*"]
},
{
"name": "order",
"actions": ["*"]
},
{
"name": "file",
"actions": ["*"]
}
]
},
{
"name": "operation",
"resources": [
{
"name": "product",
"actions": [
{
"name": "create",
"attributes": ["*"]
},
{
"name": "read",
"attributes": ["*"]
},
{
"name": "update",
"attributes": ["*", "!history"]
},
{
"name": "delete",
"attributes": ["*"]
}
]
},
{
"name": "order",
"actions": [
{
"name": "create",
"attributes": ["*"]
},
{
"name": "read",
"attributes": ["*"]
},
{
"name": "update",
"attributes": ["*"]
}
]
}
]
}
];
const adapter = new MemoryAdapter(roles);
const simpleAccess = new SimpleAccess<RoleDefinition, typeof adapter>(adapter);
const resource = {
"authorId": 1002,
"price": 75.08
};
const permission = simpleAccess.can("operation", "read", "order");
if(permission.granted) {
const filteredResource = simpleAccess.filter(permission, resource);
}
For simplicity and flexibility you can also call filter
from Permission object
const permission = simpleAccess.can("operation", "read", "order");
if(permission.granted) {
const filteredResource = permission.filter(resource);
}
MIT
FAQs
Attribute-Role-Based Hybrid Access Control Library
We found that simple-access demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.