Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/access_helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {OrgIdToOrgMemberInfo} from "./org";
import {OrgIdToOrgMemberInfo, OrgRoleStructure} from "./org";

export type AccessHelper = {
isRole: (orgId: string, role: string) => boolean
Expand All @@ -23,15 +23,23 @@ export function getAccessHelper(
if (orgMemberInfo === undefined) {
return false;
}
return orgMemberInfo.userAssignedRole === role
if (orgMemberInfo.orgRoleStructure === OrgRoleStructure.MultiRole) {
return orgMemberInfo.userAssignedRole === role || orgMemberInfo.userAssignedAdditionalRoles.includes(role)
} else {
return orgMemberInfo.userAssignedRole === role
}
}

function isAtLeastRole(orgId: string, role: string): boolean {
const orgMemberInfo = orgIdToOrgMemberInfo[orgId]
if (orgMemberInfo === undefined) {
return false;
}
return orgMemberInfo.userInheritedRolesPlusCurrentRole.includes(role)
if (orgMemberInfo.orgRoleStructure === OrgRoleStructure.MultiRole) {
return orgMemberInfo.userAssignedRole === role || orgMemberInfo.userAssignedAdditionalRoles.includes(role)
} else {
return orgMemberInfo.userInheritedRolesPlusCurrentRole.includes(role)
}
}

function hasPermission(orgId: string, permission: string): boolean {
Expand Down
4 changes: 4 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ export function parseJsonConvertingSnakeToCamel(str: string): AuthenticationInfo
this.legacyUserId = value
} else if (key === "impersonator_user") {
this.impersonatorUserId = value
} else if (key === "org_role_structure") {
this.orgRoleStructure = value
} else if (key === "additional_roles") {
this.userAssignedAdditionalRoles = value
} else {
return value
}
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type {
} from "./client"
export { ACTIVE_ORG_ID_COOKIE_NAME } from "./cookies"
export { getActiveOrgId, setActiveOrgId } from "./org"
export type { OrgIdToOrgMemberInfo, OrgMemberInfo } from "./org"
export type { OrgIdToOrgMemberInfo, OrgMemberInfo, OrgRoleStructure } from "./org"
export type { OrgHelper } from "./org_helper"
export { OrgMemberInfoClass, UserClass } from "./user"
export type { OrgIdToOrgMemberInfoClass, UserFields, UserProperties } from "./user"
7 changes: 7 additions & 0 deletions src/org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ export type OrgMemberInfo = {
orgName: string
orgMetadata: { [key: string]: any }
urlSafeOrgName: string
orgRoleStructure: OrgRoleStructure
userAssignedRole: string
userInheritedRolesPlusCurrentRole: string[]
userPermissions: string[]
userAssignedAdditionalRoles: string[]
}
export type OrgIdToOrgMemberInfo = {
[orgId: string]: OrgMemberInfo
}

export enum OrgRoleStructure {
SingleRole = "single_role_in_hierarchy",
MultiRole = "multi_role",
}

export const setActiveOrgId = (orgId: string) => {
Cookies.set(ACTIVE_ORG_ID_COOKIE_NAME, orgId, {
sameSite: "lax",
Expand Down
45 changes: 45 additions & 0 deletions src/tests/access_helper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,48 @@ it("accessHelper validate wrapper methods work", async () => {
expect(accessHelperWrapper.hasAllPermissions(["read", "write", "delete"])).toBeFalsy()
expect(accessHelperWrapperBad.isRole("Admin")).toBeFalsy()
})

// Multi role tests
it("accessHelper validate methods work with multi role", async () => {
const orgs = createOrgs(1, true)
const orgId = orgs[0].orgId
const fakeOrgId = "fakeOrgId"
const orgIdToOrgMemberInfo = createOrgIdToOrgMemberInfo(orgs)

const accessHelper = getAccessHelper(orgIdToOrgMemberInfo)

// org has the roles of "Role A", "Role B", and "Role C" in orgId
expect(accessHelper.isRole(orgId, "")).toBeFalsy()
expect(accessHelper.isRole(orgId, "Role D")).toBeFalsy()
expect(accessHelper.isRole(orgId, "Role A")).toBeTruthy()
expect(accessHelper.isRole(fakeOrgId, "Role A")).toBeFalsy()
expect(accessHelper.isRole(orgId, "Role B")).toBeTruthy()
expect(accessHelper.isRole(orgId, "Role C")).toBeTruthy()

// isAtLeastRole should work the same as isRole for multi role
expect(accessHelper.isAtLeastRole(orgId, "")).toBeFalsy()
expect(accessHelper.isAtLeastRole(orgId, "Role D")).toBeFalsy()
expect(accessHelper.isAtLeastRole(orgId, "Role A")).toBeTruthy()
expect(accessHelper.isAtLeastRole(fakeOrgId, "Role A")).toBeFalsy()
expect(accessHelper.isAtLeastRole(orgId, "Role B")).toBeTruthy()
expect(accessHelper.isAtLeastRole(orgId, "Role C")).toBeTruthy()

// org has the permissions "read" and "write"
expect(accessHelper.hasPermission(orgId, "")).toBeFalsy()
expect(accessHelper.hasPermission(orgId, "read")).toBeTruthy()
expect(accessHelper.hasPermission(fakeOrgId, "read")).toBeFalsy()
expect(accessHelper.hasPermission(orgId, "write")).toBeTruthy()
expect(accessHelper.hasPermission(orgId, "delete")).toBeFalsy()

// org has the permissions "read" and "write"
expect(accessHelper.hasAllPermissions(orgId, [])).toBeTruthy()
expect(accessHelper.hasAllPermissions(orgId, [""])).toBeFalsy()
expect(accessHelper.hasAllPermissions(orgId, ["read"])).toBeTruthy()
expect(accessHelper.hasAllPermissions(fakeOrgId, ["read"])).toBeFalsy()
expect(accessHelper.hasAllPermissions(orgId, ["write"])).toBeTruthy()
expect(accessHelper.hasAllPermissions(orgId, ["delete"])).toBeFalsy()
expect(accessHelper.hasAllPermissions(orgId, ["read", "write"])).toBeTruthy()
expect(accessHelper.hasAllPermissions(orgId, ["read", "delete"])).toBeFalsy()
expect(accessHelper.hasAllPermissions(orgId, ["write", "delete"])).toBeFalsy()
expect(accessHelper.hasAllPermissions(orgId, ["read", "write", "delete"])).toBeFalsy()
})
27 changes: 25 additions & 2 deletions src/tests/test_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @jest-environment jsdom
*/
import { v4 as uuidv4 } from "uuid"
import { OrgRoleStructure } from "../org"

export function createOrgIdToOrgMemberInfo(orgs) {
let orgIdToOrgMemberInfo = {}
Expand All @@ -11,10 +12,14 @@ export function createOrgIdToOrgMemberInfo(orgs) {
return orgIdToOrgMemberInfo
}

export function createOrgs(numOrgs) {
export function createOrgs(numOrgs, multiRole = false) {
let orgs = []
for (let i = 0; i < numOrgs; i++) {
orgs.push(createOrg())
if (multiRole) {
orgs.push(createOrgWithMultiRoles())
} else {
orgs.push(createOrg())
}
}
return orgs
}
Expand All @@ -35,6 +40,24 @@ export function createOrg() {
}
}

export function createOrgWithMultiRoles() {
const orgName = randomString()
const urlSafeOrgName = orgName.toLowerCase()
return {
orgId: uuidv4(),
orgName,
orgMetadata: {
hello: "world",
},
urlSafeOrgName,
orgRoleStructure: OrgRoleStructure.MultiRole,
userAssignedRole: "Role A",
userInheritedRolesPlusCurrentRole: ["Role A"],
userPermissions: ["read", "write"],
userAssignedAdditionalRoles: ["Role B", "Role C"],
}
}

function randomString() {
return (Math.random() + 1).toString(36).substring(3)
}
Expand Down
108 changes: 108 additions & 0 deletions src/user.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OrgRoleStructure } from "./org"
import { UserClass, OrgMemberInfoClass } from "./user"

const mockUserOrgInfo = new OrgMemberInfoClass(
Expand Down Expand Up @@ -100,3 +101,110 @@ describe("User", () => {
})
})
})

const mockUserOrgInfoMultiRole = new OrgMemberInfoClass(
"mockOrgId",
"Mock Org Name",
{},
"mock-org-name",
"Role A",
["Role A"],
["user::create", "user::delete"],
OrgRoleStructure.MultiRole,
["Role B", "Role C"],
)

const mockUserMultiRole = new UserClass(
{
userId: "userId",
email: "email",
createdAt: 12345678,
firstName: "firstName",
lastName: "lastName",
username: "username",
legacyUserId: "legacyUserId",
impersonatorUserId: "impersonatorUserId",
properties: {
property: "value",
},
},
{
mockOrgId: mockUserOrgInfoMultiRole,
}
)

describe("User multi-role", () => {
describe("User Class multi-role", () => {
it("should get an org", () => {
expect(mockUserMultiRole.getOrg("mockOrgId")).toEqual(mockUserOrgInfoMultiRole)
expect(mockUserMultiRole.getOrg("mockOrgId2")).toBeUndefined()
})
it("should get an org by name", () => {
expect(mockUserMultiRole.getOrgByName("Mock Org Name")).toEqual(mockUserOrgInfoMultiRole)
expect(mockUserMultiRole.getOrgByName("Mock Org Name 2")).toBeUndefined()
})
it("should get a user property", () => {
expect(mockUserMultiRole.getUserProperty("property")).toEqual("value")
expect(mockUserMultiRole.getUserProperty("property2")).toBeUndefined()
})
it("should get all orgs", () => {
expect(mockUserMultiRole.getOrgs()).toEqual([mockUserOrgInfoMultiRole])
})
it("should ensure the user is a certain role", () => {
expect(mockUserMultiRole.isRole("mockOrgId", "Role A")).toEqual(true)
expect(mockUserMultiRole.isRole("mockOrgId", "Role B")).toEqual(true)
expect(mockUserMultiRole.isRole("mockOrgId", "Role C")).toEqual(true)
expect(mockUserMultiRole.isRole("mockOrgId", "Role D")).toEqual(false)
expect(mockUserMultiRole.isRole("mockOrgId2", "Role A")).toEqual(false)
})
it("should ensure the user is at least a certain role", () => {
expect(mockUserMultiRole.isAtLeastRole("mockOrgId", "Role A")).toEqual(true)
expect(mockUserMultiRole.isAtLeastRole("mockOrgId", "Role B")).toEqual(true)
expect(mockUserMultiRole.isAtLeastRole("mockOrgId", "Role C")).toEqual(true)
expect(mockUserMultiRole.isAtLeastRole("mockOrgId", "Role D")).toEqual(false)
expect(mockUserMultiRole.isAtLeastRole("mockOrgId2", "Role A")).toEqual(false)
})
it("should ensure the user has a permission", () => {
expect(mockUserMultiRole.hasPermission("mockOrgId", "user::create")).toEqual(true)
expect(mockUserMultiRole.hasPermission("mockOrgId", "user::delete")).toEqual(true)
expect(mockUserMultiRole.hasPermission("mockOrgId", "user::update")).toEqual(false)
expect(mockUserMultiRole.hasPermission("mockOrgId2", "user::create")).toEqual(false)
})
it("should ensure the user has all permissions", () => {
expect(mockUserMultiRole.hasAllPermissions("mockOrgId", ["user::create", "user::delete"])).toEqual(true)
expect(mockUserMultiRole.hasAllPermissions("mockOrgId", ["user::create", "user::update"])).toEqual(false)
expect(mockUserMultiRole.hasAllPermissions("mockOrgId2", ["user::create", "user::delete"])).toEqual(false)
})
it("should parse a user from JSON string", () => {
expect(UserClass.fromJSON(JSON.stringify(mockUser))).toEqual(mockUser)
expect(() => UserClass.fromJSON("invalid json")).toThrowError()
})
})
describe("UserOrgInfo Class multi-role", () => {
it("should validate a role", () => {
expect(mockUserOrgInfoMultiRole.isRole("Role A")).toEqual(true)
expect(mockUserOrgInfoMultiRole.isRole("Role B")).toEqual(true)
expect(mockUserOrgInfoMultiRole.isRole("Role C")).toEqual(true)
expect(mockUserOrgInfoMultiRole.isRole("Role D")).toEqual(false)
})
it("should validate a role is at least a certain role", () => {
expect(mockUserOrgInfoMultiRole.isAtLeastRole("Role A")).toEqual(true)
expect(mockUserOrgInfoMultiRole.isAtLeastRole("Role B")).toEqual(true)
expect(mockUserOrgInfoMultiRole.isAtLeastRole("Role C")).toEqual(true)
expect(mockUserOrgInfoMultiRole.isAtLeastRole("Role D")).toEqual(false)
})
it("should validate a permission", () => {
expect(mockUserOrgInfoMultiRole.hasPermission("user::create")).toEqual(true)
expect(mockUserOrgInfoMultiRole.hasPermission("user::delete")).toEqual(true)
expect(mockUserOrgInfoMultiRole.hasPermission("user::update")).toEqual(false)
})
it("should validate all permissions", () => {
expect(mockUserOrgInfoMultiRole.hasAllPermissions(["user::create", "user::delete"])).toEqual(true)
expect(mockUserOrgInfoMultiRole.hasAllPermissions(["user::create", "user::update"])).toEqual(false)
})
it("should parse a org member info from JSON string", () => {
expect(OrgMemberInfoClass.fromJSON(JSON.stringify(mockUserOrgInfoMultiRole))).toEqual(mockUserOrgInfoMultiRole)
expect(() => OrgMemberInfoClass.fromJSON("invalid json")).toThrowError()
})
})
})
30 changes: 24 additions & 6 deletions src/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OrgIdToOrgMemberInfo } from "./org"
import { OrgIdToOrgMemberInfo, OrgRoleStructure } from "./org"

export type UserProperties = { [key: string]: unknown }

Expand Down Expand Up @@ -179,10 +179,12 @@ export class OrgMemberInfoClass {
public orgName: string
public orgMetadata: { [key: string]: any }
public urlSafeOrgName: string
public orgRoleStructure: OrgRoleStructure

public userAssignedRole: string
public userInheritedRolesPlusCurrentRole: string[]
public userPermissions: string[]
public userAssignedAdditionalRoles: string[]

constructor(
orgId: string,
Expand All @@ -191,25 +193,37 @@ export class OrgMemberInfoClass {
urlSafeOrgName: string,
userAssignedRole: string,
userInheritedRolesPlusCurrentRole: string[],
userPermissions: string[]
userPermissions: string[],
orgRoleStructure?: OrgRoleStructure,
userAssignedAdditionalRoles?: string[]
) {
this.orgId = orgId
this.orgName = orgName
this.orgMetadata = orgMetadata
this.urlSafeOrgName = urlSafeOrgName
this.orgRoleStructure = orgRoleStructure ?? OrgRoleStructure.SingleRole

this.userAssignedRole = userAssignedRole
this.userInheritedRolesPlusCurrentRole = userInheritedRolesPlusCurrentRole
this.userPermissions = userPermissions
this.userAssignedAdditionalRoles = userAssignedAdditionalRoles ?? []
}

// validation methods
public isRole(role: string): boolean {
return this.userAssignedRole === role
if (this.orgRoleStructure === OrgRoleStructure.MultiRole) {
return this.userAssignedRole === role || this.userAssignedAdditionalRoles.includes(role)
} else {
return this.userAssignedRole === role
}
}

public isAtLeastRole(role: string): boolean {
return this.userInheritedRolesPlusCurrentRole.includes(role)
if (this.orgRoleStructure === OrgRoleStructure.MultiRole) {
return this.userAssignedRole === role || this.userAssignedAdditionalRoles.includes(role)
} else {
return this.userInheritedRolesPlusCurrentRole.includes(role)
}
}

public hasPermission(permission: string): boolean {
Expand All @@ -230,7 +244,9 @@ export class OrgMemberInfoClass {
obj.urlSafeOrgName,
obj.userAssignedRole,
obj.userInheritedRolesPlusCurrentRole,
obj.userPermissions
obj.userPermissions,
obj.orgRoleStructure,
obj.userAssignedAdditionalRoles
)
} catch (e) {
console.error(
Expand All @@ -257,7 +273,9 @@ export function convertOrgIdToOrgMemberInfo(
orgMemberInfo.urlSafeOrgName,
orgMemberInfo.userAssignedRole,
orgMemberInfo.userInheritedRolesPlusCurrentRole,
orgMemberInfo.userPermissions
orgMemberInfo.userPermissions,
orgMemberInfo.orgRoleStructure,
orgMemberInfo.userAssignedAdditionalRoles
)
}
return orgIdToUserOrgInfo
Expand Down