Skip to content

Instantly share code, notes, and snippets.

@BLamy
Last active July 29, 2023 00:35
Show Gist options
  • Save BLamy/49f9632ea4180377464b3bb331c9f15b to your computer and use it in GitHub Desktop.
Save BLamy/49f9632ea4180377464b3bb331c9f15b to your computer and use it in GitHub Desktop.
Typescript Permission System
// https://tsplay.dev/w2kJrN
import { Brand } from 'ts-brand'
import { z } from 'zod'
//------------------------------------------------
// Permission Levels
enum PermissionLevelEnum {
OWNER_ONLY = "OWNER_ONLY",
COMPANY_ONLY = "COMPANY_ONLY",
PUBLIC = "PUBLIC",
}
const PermissionLevel = z.nativeEnum(PermissionLevelEnum)
type PermissionLevel = z.infer<typeof PermissionLevel>
//------------------------------------------------
// UUID
type UUID = Brand<'UUID', string>;
const UUID = z.string().refine(isUUID);
function isUUID(id: string): id is UUID {
const UUIDRegExp = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
return UUIDRegExp.test(id);
}
//------------------------------------------------
// CompanyId
type CompanyId = Brand<'CompanyId', UUID>;
const CompanyId = z.string().refine(isCompanyId)
function isCompanyId(maybeCompanyId: string): maybeCompanyId is CompanyId {
return isUUID(maybeCompanyId) && companiesById.hasOwnProperty(maybeCompanyId);
}
// Company
const Company = z.object({
id: CompanyId,
name: z.string(),
})
type Company = z.infer<typeof Company>
function isCompany(maybeCompany: unknown): maybeCompany is Company {
return Company.safeParse(maybeCompany).success
}
const companies = [
{ id: "a0a0939e-feb1-4923-943e-72562240197c", name: "company 1" },
{ id: "69783ec6-c31e-4848-a2f2-14756b9a9001", name: "company 2" },
].filter(isCompany)
const companiesById: Record<CompanyId, Company> = Object.fromEntries(companies.map(company => [company.id, company]))
//------------------------------------------------
// UserId
type UserId = Brand<'UserId', UUID>;
const UserId = z.string().refine(isUserId)
function isUserId(maybeUserId: string): maybeUserId is UserId {
return isUUID(maybeUserId) && usersById.hasOwnProperty(maybeUserId);
}
// User
const User = z.object({
id: UserId,
name: z.string(),
companyId: CompanyId,
})
type User = z.infer<typeof User>
function isUser(maybeUser: unknown): maybeUser is User {
return User.safeParse(maybeUser).success
}
const users = [
// ^?
{ id: "5c17178a-d0eb-4dd9-ad99-7bdc9be6d60d", name: "owns no files", companyId: "a0a0939e-feb1-4923-943e-72562240197c" },
{ id: "2fd50c57-b023-4df9-aa8a-d1f2b0191461", name: "current user", companyId: "69783ec6-c31e-4848-a2f2-14756b9a9001" },
{ id: "c8936de5-22a4-4b25-9510-26114074dbc4", name: "owns some private files", companyId: "69783ec6-c31e-4848-a2f2-14756b9a9001" },
{ id: "d9ee1fa1-9383-46bf-8931-ccd7da7b8b5e", name: "owns some public files", companyId: "a0a0939e-feb1-4923-943e-72562240197c" }
].filter(isUser)
const usersById: Record<UserId, User> = Object.fromEntries(users.map(user => [user.id, user]))
// CurrentUser
type CurrentUser = Brand<User, "CurrentUser">
const CurrentUser = User.refine(isCurrentUser)
function isCurrentUser(maybeCurrentUser: User): maybeCurrentUser is CurrentUser {
return maybeCurrentUser.name === "current user"
}
//------------------------------------------------
// FileId
type FileId = Brand<'FileId', UUID>;
const FileId = z.string().refine(isFileId)
function isFileId(maybeFileId: string): maybeFileId is FileId {
return isUUID(maybeFileId) && filesById.hasOwnProperty(maybeFileId);
}
// File
const File = z.object({
fileId: FileId,
ownerId: UserId,
name: z.string(),
permissionLevel: PermissionLevel
})
type File = z.infer<typeof File>
function isFile(maybeFile: unknown): maybeFile is File {
return File.safeParse(maybeFile).success
}
const files = [
// ^?
{ ownerId: "a0a0939e-feb1-4923-943e-72562240197c", name: "can view file because it's my own", permissionLevel: PermissionLevelEnum.OWNER_ONLY },
{ ownerId: "69783ec6-c31e-4848-a2f2-14756b9a9001", name: "asdf", permissionLevel: PermissionLevelEnum.COMPANY_ONLY },
{ ownerId: "69783ec6-c31e-4848-a2f2-14756b9a9001", name: "asdf", permissionLevel: PermissionLevelEnum.PUBLIC }
].filter(isFile)
const filesById: Record<FileId, File> = Object.fromEntries(files.map(file => [file.fileId, file]))
//------------------------------------------------
// File Access Control
// Unauthorized
const CurrentUserUnauthorizedFile = z.object({
currentUser: CurrentUser,
file: File
})
type CurrentUserUnauthorizedFile = z.infer<typeof CurrentUserUnauthorizedFile>
// Authorized
type CurrentUserAuthorizedFileAccess = Brand<CurrentUserUnauthorizedFile, "AuthorizedFileAccess">
const CurrentUserAuthorizedFileAccess = CurrentUserUnauthorizedFile.refine(isCurrentUserAuthorizedForFile)
function isCurrentUserAuthorizedForFile(props: CurrentUserUnauthorizedFile): props is CurrentUserAuthorizedFileAccess {
const fileOwnedByCurrentUser = props.file.ownerId === props.currentUser.id
const resolvedFileOwner = (usersById as Record<string, User>)[props.file.ownerId]
const fileInSameCompanyAsCurrentUser = resolvedFileOwner && resolvedFileOwner.companyId === props.currentUser.companyId
return (props.file.permissionLevel === PermissionLevelEnum.OWNER_ONLY && fileOwnedByCurrentUser)
|| (props.file.permissionLevel === PermissionLevelEnum.COMPANY_ONLY && fileInSameCompanyAsCurrentUser)
|| props.file.permissionLevel === PermissionLevelEnum.PUBLIC
}
//------------------------------------------------
// Usage Demo
// Using type gaurds on if statemnts
const maybeCurrentUser = users[1];
if (isCurrentUser(maybeCurrentUser)) {
const currentUserUnauthorizedFile = {
currentUser: maybeCurrentUser,
// ^?
file: files[0]
// ^?
}
if (isCurrentUserAuthorizedForFile(currentUserUnauthorizedFile)) {
const currentUserAuthorizedFile = currentUserUnauthorizedFile
// ^?
}
}
// Using zod to assert
const currentUserAuthorizedFile = CurrentUserAuthorizedFileAccess.parse({
// ^?
currentUser: users[1],
// ^?
file: files[0]
//^?
})
@BLamy
Copy link
Author

BLamy commented Jul 29, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment