Skip to content
This repository was archived by the owner on Apr 19, 2023. It is now read-only.

Commit 838a8b5

Browse files
✨ Add authorization helpers
1 parent 2d4912f commit 838a8b5

File tree

9 files changed

+242
-115
lines changed

9 files changed

+242
-115
lines changed

src/crud/user.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,21 @@ export const addApprovedLocation = async (
9090
subnet,
9191
createdAt: new Date()
9292
};
93+
deleteItemFromCache(CacheCategories.APPROVE_LOCATIONS, userId);
94+
deleteItemFromCache(CacheCategories.APPROVE_LOCATION, subnet);
9395
return await query(
9496
`INSERT INTO \`approved-locations\` ${tableValues(subnetLocation)}`,
9597
Object.values(subnetLocation)
9698
);
9799
};
98100

99101
export const getUserApprovedLocations = async (userId: number) => {
100-
return await query("SELECT * FROM `approved-locations` WHERE userId = ?", [
101-
userId
102-
]);
102+
return await cachedQuery(
103+
CacheCategories.APPROVE_LOCATIONS,
104+
userId,
105+
"SELECT * FROM `approved-locations` WHERE userId = ?",
106+
[userId]
107+
);
103108
};
104109

105110
export const checkApprovedLocation = async (
@@ -108,7 +113,9 @@ export const checkApprovedLocation = async (
108113
) => {
109114
const subnet = anonymizeIpAddress(ipAddress);
110115
const approvedLocations = <ApprovedLocation[]>(
111-
await query(
116+
await cachedQuery(
117+
CacheCategories.APPROVE_LOCATION,
118+
subnet,
112119
"SELECT * FROM `approved-locations` WHERE userId = ? AND subnet = ? LIMIT 1",
113120
[userId, subnet]
114121
)

src/helpers/authorization.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { User } from "../interfaces/tables/user";
2+
import { Organization } from "../interfaces/tables/organization";
3+
import {
4+
ErrorCode,
5+
Authorizations,
6+
UserRole,
7+
MembershipRole
8+
} from "../interfaces/enum";
9+
import { getUser } from "../crud/user";
10+
import { getUserMembershipObject, getMembership } from "../crud/membership";
11+
import { getOrganization } from "../crud/organization";
12+
import { Membership } from "../interfaces/tables/memberships";
13+
14+
const canUserUser = async (
15+
user: User,
16+
action: Authorizations,
17+
target: User
18+
) => {
19+
// A super user can do anything
20+
if (user.role == UserRole.ADMIN) return true;
21+
22+
// A user can do anything to herself
23+
if (user.id === target.id) return true;
24+
25+
const userMembership = await getUserMembershipObject(user);
26+
const userOrganizationId = userMembership.organizationId;
27+
const targetMembership = await getUserMembershipObject(target);
28+
const targetOrganizationId = targetMembership.organizationId;
29+
30+
// A reseller can view/edit/delete users in her organization
31+
if (
32+
userOrganizationId === targetOrganizationId &&
33+
user.role == UserRole.RESELLER &&
34+
action != Authorizations.IMPERSONATE
35+
)
36+
return true;
37+
38+
if (action == Authorizations.READ) {
39+
// A user can read another user in the same organization, as long as they're not a basic member
40+
if (
41+
userOrganizationId === targetOrganizationId &&
42+
userMembership.role != MembershipRole.BASIC
43+
)
44+
return true;
45+
}
46+
47+
return false;
48+
};
49+
50+
const canUserOrganization = async (
51+
user: User,
52+
action: Authorizations,
53+
target: Organization
54+
) => {
55+
// A super user can do anything
56+
if (user.role == UserRole.ADMIN) return true;
57+
58+
const membership = await getUserMembershipObject(user);
59+
60+
// A non-member cannot do anything
61+
if (membership.organizationId != target.id) return false;
62+
63+
// An organization owner can do anything
64+
if (membership.role == MembershipRole.OWNER) return true;
65+
66+
// An organization admin can do anything too
67+
if (membership.role == MembershipRole.ADMIN) return true;
68+
69+
// An organization manager can do anything but delete
70+
if (
71+
membership.role == MembershipRole.MANAGER &&
72+
action != Authorizations.DELETE
73+
)
74+
return true;
75+
76+
// An organization member can read, not edit/delete/invite
77+
if (membership.role == MembershipRole.MEMBER && action == Authorizations.READ)
78+
return true;
79+
80+
return false;
81+
};
82+
83+
const canUserMembership = async (
84+
user: User,
85+
action: Authorizations,
86+
target: Membership
87+
) => {
88+
// A super user can do anything
89+
if (user.role == UserRole.ADMIN) return true;
90+
91+
// A member can do anything to herself
92+
if (user.id == target.userId) return true;
93+
94+
const membership = await getUserMembershipObject(user);
95+
96+
// A different organization member cannot edit a membership
97+
if (membership.organizationId != target.organizationId) return false;
98+
99+
// An admin, owner, or manager can edit
100+
if (
101+
[
102+
MembershipRole.OWNER,
103+
MembershipRole.ADMIN,
104+
MembershipRole.MANAGER
105+
].includes(membership.role)
106+
)
107+
return true;
108+
109+
return false;
110+
};
111+
112+
export const can = async (
113+
user: User | number,
114+
action: Authorizations,
115+
targetType: "user" | "organization" | "membership",
116+
target: User | Organization | Membership | number
117+
) => {
118+
let userObject;
119+
if (typeof user === "number") {
120+
userObject = await getUser(user);
121+
} else {
122+
userObject = user;
123+
}
124+
let targetObject;
125+
if (typeof target === "string") target = parseInt(target);
126+
if (typeof target == "number") {
127+
if (targetType === "user") {
128+
targetObject = await getUser(target);
129+
} else if (targetType === "organization") {
130+
targetObject = await getOrganization(target);
131+
} else {
132+
targetObject = await getMembership(target);
133+
}
134+
} else {
135+
targetObject = target;
136+
}
137+
if (!userObject.id) throw new Error(ErrorCode.USER_NOT_FOUND);
138+
if (targetType === "user") {
139+
return await canUserUser(userObject, action, <User>targetObject);
140+
} else if (targetType === "organization") {
141+
return await canUserOrganization(userObject, action, <Organization>(
142+
targetObject
143+
));
144+
} else {
145+
return await canUserMembership(userObject, action, <Membership>(
146+
targetObject
147+
));
148+
}
149+
};

src/helpers/cache.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@ const cache = new NodeCache({
88
checkperiod: CACHE_CHECK_PERIOD
99
});
1010

11-
const generateKey = (category: string, item: number | string) =>
11+
const generateKey = (category: CacheCategories, item: number | string) =>
1212
`${category}_${item}`;
1313

14-
export const getItemFromCache = (category: string, item: number | string) => {
14+
export const getItemFromCache = (
15+
category: CacheCategories,
16+
item: number | string
17+
) => {
1518
const key = generateKey(category, item);
1619
return cache.get(key);
1720
};
1821

1922
export const storeItemInCache = (
20-
category: string,
23+
category: CacheCategories,
2124
item: number | string,
2225
value: any
2326
) => {
@@ -26,7 +29,7 @@ export const storeItemInCache = (
2629
};
2730

2831
export const deleteItemFromCache = (
29-
category: string,
32+
category: CacheCategories,
3033
item: number | string
3134
) => {
3235
const key = generateKey(category, item);

src/helpers/jwt.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,28 +64,35 @@ export const refreshToken = (id: number) =>
6464

6565
export const getLoginResponse = async (
6666
user: User,
67-
type: EventType,
68-
strategy: string,
69-
locals: Locals
67+
type?: EventType,
68+
strategy?: string,
69+
locals?: Locals
7070
) => {
7171
if (!user.id) throw new Error(ErrorCode.USER_NOT_FOUND);
7272
const verifiedEmails = await getUserVerifiedEmails(user);
7373
if (!verifiedEmails.length) throw new Error(ErrorCode.UNVERIFIED_EMAIL);
74-
if (!(await checkApprovedLocation(user.id, locals.ipAddress))) {
75-
await mail(await getUserPrimaryEmail(user), Templates.UNAPPROVED_LOCATION, {
76-
...user,
77-
token: await approveLocationToken(user.id)
78-
});
79-
throw new Error(ErrorCode.UNAPPROVED_LOCATION);
74+
if (locals) {
75+
if (!(await checkApprovedLocation(user.id, locals.ipAddress))) {
76+
await mail(
77+
await getUserPrimaryEmail(user),
78+
Templates.UNAPPROVED_LOCATION,
79+
{
80+
...user,
81+
token: await approveLocationToken(user.id)
82+
}
83+
);
84+
throw new Error(ErrorCode.UNAPPROVED_LOCATION);
85+
}
8086
}
81-
await createEvent(
82-
{
83-
userId: user.id,
84-
type,
85-
data: { strategy }
86-
},
87-
locals
88-
);
87+
if (type && strategy && locals)
88+
await createEvent(
89+
{
90+
userId: user.id,
91+
type,
92+
data: { strategy }
93+
},
94+
locals
95+
);
8996
try {
9097
return {
9198
token: await loginToken(deleteSensitiveInfoUser(user)),

src/interfaces/enum.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,16 @@ export enum CacheCategories {
7878
USER_EVENT = "user-event",
7979
ORGANIZATION_MEMBERSHIPS = "memberships",
8080
MEMBERSHIP = "membership",
81-
ORGANIZATION = "organization"
81+
ORGANIZATION = "organization",
82+
APPROVE_LOCATIONS = "approved-locations",
83+
APPROVE_LOCATION = "approved-location"
84+
}
85+
86+
export enum Authorizations {
87+
CREATE = "create",
88+
READ = "read",
89+
UPDATE = "update",
90+
DELETE = "delete",
91+
INVITE_MEMBER = "invite-member",
92+
IMPERSONATE = "impersonate"
8293
}

src/rest/auth.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ import { createEmail, updateEmail, getEmail } from "../crud/email";
1111
import { mail } from "../helpers/mail";
1212
import {
1313
verifyToken,
14-
loginToken,
1514
passwordResetToken,
16-
refreshToken,
1715
getLoginResponse
1816
} from "../helpers/jwt";
1917
import { KeyValue, Locals } from "../interfaces/general";
@@ -24,16 +22,16 @@ import {
2422
MembershipRole,
2523
Templates,
2624
Tokens,
27-
UserRole
25+
Authorizations
2826
} from "../interfaces/enum";
2927
import { compare, hash } from "bcrypt";
30-
import { deleteSensitiveInfoUser } from "../helpers/utils";
3128
import { createMembership } from "../crud/membership";
3229
import {
3330
googleGetConnectionUrl,
3431
googleGetTokensFromCode,
3532
googleGetEmailFromToken
3633
} from "../helpers/google";
34+
import { can } from "../helpers/authorization";
3735

3836
export const validateRefreshToken = async (token: string, locals: Locals) => {
3937
const data = <User>await verifyToken(token, Tokens.REFRESH);
@@ -152,15 +150,16 @@ export const impersonate = async (
152150
tokenUserId: number,
153151
impersonateUserId: number
154152
) => {
155-
const tokenUser = await getUser(tokenUserId);
156-
if (tokenUser.role != UserRole.ADMIN)
157-
throw new Error(ErrorCode.INSUFFICIENT_PERMISSION);
158-
const user = await getUser(impersonateUserId);
159-
if (!user.id) throw new Error(ErrorCode.USER_NOT_FOUND);
160-
return {
161-
token: await loginToken(deleteSensitiveInfoUser(user)),
162-
refresh: await refreshToken(user.id)
163-
};
153+
if (
154+
await can(
155+
tokenUserId,
156+
Authorizations.IMPERSONATE,
157+
"user",
158+
impersonateUserId
159+
)
160+
)
161+
return await getLoginResponse(await getUser(impersonateUserId));
162+
throw new Error(ErrorCode.INSUFFICIENT_PERMISSION);
164163
};
165164

166165
export const approveLocation = async (token: string, locals: Locals) => {

0 commit comments

Comments
 (0)