|
type permission = |
|
| Permission(string, string); |
|
|
|
module PermissionComparision = { |
|
type t = permission; |
|
let compare = (Permission(name1, _), Permission(name2, _)) => String.compare(name1, name2); |
|
}; |
|
|
|
module PermissionSet = Set.Make(PermissionComparision); |
|
|
|
type role = { |
|
name: string, |
|
description: string, |
|
permissions: PermissionSet.t |
|
}; |
|
|
|
type organization = { |
|
name: string, |
|
organizationId: string |
|
}; |
|
|
|
let isSameOrgAs = ({organizationId: id1}, {organizationId: id2}) => id1 == id2; |
|
|
|
type user = { |
|
email: string, |
|
userId: string, |
|
organizations: list((organization, list(role))) |
|
}; |
|
|
|
let isSameUserAs = ({userId: id1}, {userId: id2}) => id1 == id2; |
|
|
|
type entity = |
|
| User(user) |
|
| Organization(organization); |
|
|
|
let isSameEntityAs = (entity1, entity2) => |
|
switch (entity1, entity2) { |
|
| (User(user1), User(user2)) => user1 |> isSameUserAs(user2) |
|
| (Organization(org1), Organization(org2)) => org1 |> isSameOrgAs(org2) |
|
| _ => false |
|
}; |
|
|
|
type accessToken = { |
|
name: string, |
|
permissions: PermissionSet.t, |
|
owner: entity |
|
}; |
|
|
|
type credential = |
|
| UserCredential(user) |
|
| AccessToken(accessToken); |
|
|
|
type verdict = |
|
| Authorized |
|
| Unauthorized; |
|
|
|
let (%>=) = (set1, set2) => PermissionSet.subset(set2, set1); |
|
|
|
let (%+) = (set1, set2) => PermissionSet.union(set1, set2); |
|
|
|
let agglomeratePermissions = (roles) => |
|
roles |> List.fold_left((set, {permissions}: role) => set %+ permissions, PermissionSet.empty); |
|
|
|
let toSome = (x) => Some(x); |
|
|
|
let tryFindOrganization = ({organizations}, targetOrg) => |
|
try ( |
|
organizations |
|
|> List.find(((organization, _)) => organization |> isSameOrgAs(targetOrg)) |
|
|> toSome |
|
) { |
|
| _ => None |
|
}; |
|
|
|
let isUserAuthorized = (user, org, requiredPermissions) => |
|
switch (tryFindOrganization(user, org)) { |
|
| Some((_, roles)) when agglomeratePermissions(roles) %>= requiredPermissions => Authorized |
|
| Some(_) => Unauthorized |
|
| None => Unauthorized |
|
}; |
|
|
|
let isAuthorized = (entity, requiredPermissions, credential) => { |
|
let requiredPermissionSet = PermissionSet.of_list(requiredPermissions); |
|
switch (credential, entity) { |
|
/* User trying to gain access to org */ |
|
| (UserCredential(user), Organization(org)) => isUserAuthorized(user, org, requiredPermissionSet) |
|
/* User trying to gain access to own user account */ |
|
| (UserCredential(authUser), User(user)) when authUser |> isSameUserAs(user) => Authorized |
|
/* User trying to gain access to ANOTHER user account */ |
|
| (UserCredential(_), User(_)) => Unauthorized |
|
/* Access token trying to gain access to correct entity */ |
|
| (AccessToken({owner, permissions}), entity) |
|
when entity |> isSameEntityAs(owner) && permissions %>= requiredPermissionSet => |
|
Authorized |
|
/* Access token trying to gain access to a DIFFERENT entity for which it was issued */ |
|
| (AccessToken(_), _) => Unauthorized |
|
} |
|
}; |
Thanks for the detailed review @yawaramin
The second parameter is actually for a description field which has no logic significance, so I actually think it'd be best to remove that from this module.