Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add canReadChannel util #1123

Merged
merged 3 commits into from
Jul 20, 2023
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
312 changes: 133 additions & 179 deletions packages/discussions/src/utils/channel-permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,14 @@ type PermissionsByAclCategoryMap = {
[key in AclCategory]?: IChannelAclPermission[];
};

enum ChannelAction {
READ_POSTS = "readPosts",
WRITE_POSTS = "writePosts",
MODERATE_CHANNEL = "moderateChannel",
}

export class ChannelPermission {
private readonly ALLOWED_GROUP_MEMBER_TYPES = ["owner", "admin", "member"];
private readonly ADMIN_GROUP_MEMBER_TYPES = ["owner", "admin"];

private readonly ALLOWED_ROLES_FOR_POSTING = Object.values(Role).filter(
(role) => role !== Role.READ
);
private readonly ALLOWED_ROLES_FOR_MODERATION = [
Role.MODERATE,
Role.MANAGE,
Role.OWNER,
];

private isChannelAclEmpty: boolean;
private permissionsByCategory: PermissionsByAclCategoryMap;
private channelCreator: string;
Expand All @@ -43,8 +38,8 @@ export class ChannelPermission {
});
}

canPostToChannel(user: IDiscussionsUser) {
if (this.canAnyUserWrite()) {
canPostToChannel(user: IDiscussionsUser): boolean {
if (this.canAnyUser(ChannelAction.WRITE_POSTS)) {
return true;
}

Expand All @@ -53,14 +48,14 @@ export class ChannelPermission {
}

return (
this.canAnyAuthenticatedUserWrite() ||
this.isUserAWriteUser(user) ||
this.isUserPartOfWriteGroup(user) ||
this.isUserPartOfWriteOrg(user)
this.canAnyAuthenticatedUser(ChannelAction.WRITE_POSTS) ||
this.canSomeUser(ChannelAction.WRITE_POSTS, user) ||
this.canSomeUserGroup(ChannelAction.WRITE_POSTS, user) ||
this.canSomeUserOrg(ChannelAction.WRITE_POSTS, user)
);
}

canCreateChannel(user: IDiscussionsUser) {
canCreateChannel(user: IDiscussionsUser): boolean {
if (this.isUserUnAuthenticated(user) || this.isChannelAclEmpty) {
return false;
}
Expand All @@ -74,19 +69,96 @@ export class ChannelPermission {
);
}

canModerateChannel(user: IDiscussionsUser) {
canModerateChannel(user: IDiscussionsUser): boolean {
if (this.isUserUnAuthenticated(user)) {
return false;
}

return (
user.username === this.channelCreator ||
this.isUserAModeratorUser(user) ||
this.isUserPartOfModeratorGroup(user) ||
this.isUserPartOfModeratorOrg(user)
this.canSomeUser(ChannelAction.MODERATE_CHANNEL, user) ||
this.canSomeUserGroup(ChannelAction.MODERATE_CHANNEL, user) ||
this.canSomeUserOrg(ChannelAction.MODERATE_CHANNEL, user)
);
}

canReadChannel(user: IDiscussionsUser): boolean {
if (this.canAnyUser(ChannelAction.READ_POSTS)) {
return true;
}

if (this.isUserUnAuthenticated(user)) {
return false;
}

return (
this.canAnyAuthenticatedUser(ChannelAction.READ_POSTS) ||
this.canSomeUser(ChannelAction.READ_POSTS, user) ||
this.canSomeUserGroup(ChannelAction.READ_POSTS, user) ||
this.canSomeUserOrg(ChannelAction.READ_POSTS, user)
);
}

private canAnyUser(action: ChannelAction): boolean {
const anonymousUserRole =
this.permissionsByCategory[AclCategory.ANONYMOUS_USER]?.[0].role;

return channelActionLookup(action).includes(anonymousUserRole);
}

private canAnyAuthenticatedUser(action: ChannelAction): boolean {
const role =
this.permissionsByCategory[AclCategory.AUTHENTICATED_USER]?.[0].role;
return channelActionLookup(action).includes(role);
}

private canSomeUser(action: ChannelAction, user: IDiscussionsUser) {
const userPermissions = this.permissionsByCategory[AclCategory.USER] ?? [];
const username = user.username;

return userPermissions.some((permission) => {
const { role, key } = permission;

return key === username && channelActionLookup(action).includes(role);
});
}

private canSomeUserGroup(action: ChannelAction, user: IDiscussionsUser) {
const groupAccessControls =
this.permissionsByCategory[AclCategory.GROUP] ?? [];

const userGroupsById = this.mapUserGroupsById(user.groups);

return groupAccessControls.some((permission) => {
const group = userGroupsById[permission.key];

if (!group || !isGroupDiscussable(group)) {
return false;
}

return (
doesPermissionAllowGroupMemberType(permission, group) &&
channelActionLookup(action).includes(permission.role)
);
});
}

private canSomeUserOrg(action: ChannelAction, user: IDiscussionsUser) {
const orgPermissions = this.permissionsByCategory[AclCategory.ORG] ?? [];

return orgPermissions.some((permission) => {
const { key } = permission;
if (permission.key !== user.orgId) {
return false;
}

return (
doesPermissionAllowOrgRole(permission, user.role) &&
channelActionLookup(action).includes(permission.role)
);
});
}

/**
* canCreateChannelHelpers
*/
Expand Down Expand Up @@ -118,7 +190,7 @@ export class ChannelPermission {
return (
userGroup &&
this.isMemberTypeAuthorized(userGroup) &&
this.isGroupDiscussable(userGroup)
isGroupDiscussable(userGroup)
);
});
}
Expand Down Expand Up @@ -153,90 +225,6 @@ export class ChannelPermission {
return !userPermissions;
}

private canAnyUserWrite() {
const role =
this.permissionsByCategory[AclCategory.ANONYMOUS_USER]?.[0].role;
return this.isAuthorizedToWritePost(role);
}

private canAnyAuthenticatedUserWrite() {
const role =
this.permissionsByCategory[AclCategory.AUTHENTICATED_USER]?.[0].role;
return this.isAuthorizedToWritePost(role);
}

private isUserAWriteUser(user: IDiscussionsUser) {
const userPermissions = this.permissionsByCategory[AclCategory.USER] ?? [];
const username = user.username;

return userPermissions.some((permission) => {
const { role, key } = permission;

return key === username && this.isAuthorizedToWritePost(role);
});
}

private isUserPartOfWriteGroup(user: IDiscussionsUser) {
const groupPermissions =
this.permissionsByCategory[AclCategory.GROUP] ?? [];
const userGroupsById = this.mapUserGroupsById(user.groups);

return groupPermissions.some((permission) => {
const userGroup = userGroupsById[permission.key];

return (
userGroup &&
this.isMemberTypeAuthorized(userGroup) &&
this.isGroupDiscussable(userGroup) &&
(this.canAnyGroupMemberPost(permission) ||
(this.isMemberTypeAdmin(userGroup) && this.canAdminsPost(permission)))
);
});
}

private canAnyGroupMemberPost(permission: IChannelAclPermission) {
const { subCategory, role } = permission;
return (
subCategory === AclSubCategory.MEMBER &&
this.isAuthorizedToWritePost(role)
);
}

private canAdminsPost(permission: IChannelAclPermission) {
const { subCategory, role } = permission;

return (
subCategory === AclSubCategory.ADMIN && this.isAuthorizedToWritePost(role)
);
}

private isUserPartOfWriteOrg(user: IDiscussionsUser) {
const orgPermissions = this.permissionsByCategory[AclCategory.ORG] ?? [];
const { orgId: userOrgId } = user;

return orgPermissions.some((permission) => {
const { key } = permission;

return (
key === userOrgId &&
(this.canAnyOrgMemberPost(permission) ||
(isOrgAdmin(user) && this.canAdminsPost(permission)))
);
});
}

private canAnyOrgMemberPost(permission: IChannelAclPermission) {
const { subCategory, role } = permission;
return (
subCategory === AclSubCategory.MEMBER &&
this.isAuthorizedToWritePost(role)
);
}

private isAuthorizedToWritePost(role?: Role) {
return this.ALLOWED_ROLES_FOR_POSTING.includes(role);
}

private isUserUnAuthenticated(user: IDiscussionsUser) {
return user.username === null || user.username === undefined;
}
Expand All @@ -254,86 +242,52 @@ export class ChannelPermission {
} = userGroup;
return this.ALLOWED_GROUP_MEMBER_TYPES.includes(memberType);
}
}

private isMemberTypeAdmin(userGroup: IGroup) {
const {
userMembership: { memberType },
} = userGroup;
return this.ADMIN_GROUP_MEMBER_TYPES.includes(memberType);
}

private isGroupDiscussable(userGroup: IGroup) {
const { typeKeywords = [] } = userGroup;
return !typeKeywords.includes(CANNOT_DISCUSS);
}

private isAuthorizedToModerate(role: Role) {
return this.ALLOWED_ROLES_FOR_MODERATION.includes(role);
}

private isUserAModeratorUser(user: IDiscussionsUser) {
const userPermissions = this.permissionsByCategory[AclCategory.USER] ?? [];
const username = user.username;

return userPermissions.some((permission) => {
const { role, key } = permission;

return key === username && this.isAuthorizedToModerate(role);
});
}

private isUserPartOfModeratorGroup(user: IDiscussionsUser) {
const groupPermissions =
this.permissionsByCategory[AclCategory.GROUP] ?? [];
const userGroupsById = this.mapUserGroupsById(user.groups);

return groupPermissions.some((permission) => {
const userGroup = userGroupsById[permission.key];
function isGroupDiscussable(userGroup: IGroup): boolean {
const { typeKeywords = [] } = userGroup;
return !typeKeywords.includes(CANNOT_DISCUSS);
}

return (
userGroup &&
this.isMemberTypeAuthorized(userGroup) &&
(this.canAnyGroupMemberModerate(permission) ||
(this.isMemberTypeAdmin(userGroup) &&
this.canAdminsModerate(permission)))
);
});
function doesPermissionAllowGroupMemberType(
permission: IChannelAclPermission,
group: IGroup
): boolean {
if (
permission.category !== AclCategory.GROUP ||
group.userMembership.memberType === "none"
) {
return false;
}

private canAnyGroupMemberModerate(permission: IChannelAclPermission) {
const { subCategory, role } = permission;
return (
subCategory === AclSubCategory.MEMBER && this.isAuthorizedToModerate(role)
);
}
return (
// group owners and admins can do anything permissioned with SubCategory "member"
group.userMembership.memberType === "owner" ||
group.userMembership.memberType === "admin" ||
permission.subCategory === AclSubCategory.MEMBER
);
}

private canAdminsModerate(permission: IChannelAclPermission) {
const { subCategory, role } = permission;
function doesPermissionAllowOrgRole(
permission: IChannelAclPermission,
orgRole: string
): boolean {
return (
permission.category === AclCategory.ORG &&
(permission.subCategory === AclSubCategory.MEMBER ||
(permission.subCategory === "admin" && orgRole === "org_admin"))
);
}

return (
subCategory === AclSubCategory.ADMIN && this.isAuthorizedToModerate(role)
);
function channelActionLookup(action: ChannelAction): Role[] {
if (action === ChannelAction.WRITE_POSTS) {
return [Role.WRITE, Role.READWRITE, Role.MODERATE, Role.MANAGE, Role.OWNER];
}

private isUserPartOfModeratorOrg(user: IDiscussionsUser) {
const orgPermissions = this.permissionsByCategory[AclCategory.ORG] ?? [];
const { orgId: userOrgId } = user;

return orgPermissions.some((permission) => {
const { key } = permission;

return (
key === userOrgId &&
(this.canAnyOrgMemberModerate(permission) ||
(isOrgAdmin(user) && this.canAdminsModerate(permission)))
);
});
if (action === ChannelAction.MODERATE_CHANNEL) {
return [Role.MODERATE, Role.MANAGE, Role.OWNER];
}

private canAnyOrgMemberModerate(permission: IChannelAclPermission) {
const { subCategory, role } = permission;
return (
subCategory === AclSubCategory.MEMBER && this.isAuthorizedToModerate(role)
);
}
// default to read action
return [Role.READ, Role.READWRITE, Role.MODERATE, Role.MANAGE, Role.OWNER];
}
Loading