mirror of https://github.com/wg-easy/wg-easy
Browse Source
* wip: add abac * wip: add admin abac * add me abac * fix type issue * move from role check avoid authStore.userData?.role === roles.ADMINpull/1661/head
committed by
GitHub
44 changed files with 441 additions and 130 deletions
@ -1,4 +1,4 @@ |
|||
export default definePermissionEventHandler(actions.ADMIN, async () => { |
|||
export default definePermissionEventHandler('admin', 'any', async () => { |
|||
const generalConfig = await Database.general.getConfig(); |
|||
return generalConfig; |
|||
}); |
|||
|
@ -1,4 +1,4 @@ |
|||
export default definePermissionEventHandler(actions.ADMIN, async () => { |
|||
export default definePermissionEventHandler('admin', 'any', async () => { |
|||
const hooks = await Database.hooks.get(); |
|||
return hooks; |
|||
}); |
|||
|
@ -1,4 +1,4 @@ |
|||
export default definePermissionEventHandler(actions.ADMIN, async () => { |
|||
export default definePermissionEventHandler('admin', 'any', async () => { |
|||
const userConfig = await Database.userConfigs.get(); |
|||
return userConfig; |
|||
}); |
|||
|
@ -1,3 +1,6 @@ |
|||
export default definePermissionEventHandler(actions.CLIENT, () => { |
|||
return WireGuard.getClients(); |
|||
export default definePermissionEventHandler('clients', 'custom', ({ user }) => { |
|||
if (user.role === roles.ADMIN) { |
|||
return WireGuard.getAllClients(); |
|||
} |
|||
return WireGuard.getClientsForUser(user.id); |
|||
}); |
|||
|
@ -1,9 +0,0 @@ |
|||
export default definePermissionEventHandler( |
|||
actions.ADMIN, |
|||
async (/*{ event }*/) => { |
|||
/*const config = await WireGuard.backupConfiguration(); |
|||
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"'); |
|||
setHeader(event, 'Content-Type', 'text/json'); |
|||
return config;*/ |
|||
} |
|||
); |
@ -1,8 +0,0 @@ |
|||
export default definePermissionEventHandler( |
|||
actions.ADMIN, |
|||
async (/*{ event }*/) => { |
|||
/*const { file } = await readValidatedBody(event, validateZod(fileType)); |
|||
await WireGuard.restoreConfiguration(file); |
|||
return { success: true };*/ |
|||
} |
|||
); |
@ -1,31 +1,158 @@ |
|||
// TODO: implement ABAC
|
|||
import type { ClientType } from '#db/repositories/client/types'; |
|||
import type { UserType } from '#db/repositories/user/types'; |
|||
|
|||
export const actions = { |
|||
ADMIN: 'ADMIN', |
|||
CLIENT: 'CLIENT', |
|||
} as const; |
|||
type BrandedRole = { |
|||
readonly __role: unique symbol; |
|||
}; |
|||
|
|||
export type Role = number & { readonly __role: unique symbol }; |
|||
export type Role = number & BrandedRole; |
|||
|
|||
export const roles = { |
|||
ADMIN: 1 as Role, |
|||
CLIENT: 2 as Role, |
|||
} as const; |
|||
|
|||
export type Action = keyof typeof actions; |
|||
type Roles = keyof typeof roles; |
|||
|
|||
/** |
|||
* convert role to key |
|||
* @example roleToKey(roles.ADMIN) => 'ADMIN' |
|||
*/ |
|||
function roleToKey(role: Role) { |
|||
const roleKey = Object.keys(roles).find( |
|||
(key) => roles[key as Roles] === role |
|||
); |
|||
|
|||
if (roleKey === undefined) { |
|||
throw new Error('Invalid role'); |
|||
} |
|||
|
|||
return roleKey as Roles; |
|||
} |
|||
|
|||
type MATRIX = { |
|||
readonly [key in keyof typeof actions]: readonly (typeof roles)[keyof typeof roles][]; |
|||
// TODO: https://github.com/nitrojs/nitro/issues/2758#issuecomment-2650531472
|
|||
|
|||
type BrandedNumber = { |
|||
toString: unknown; |
|||
toFixed: unknown; |
|||
toExponential: unknown; |
|||
toPrecision: unknown; |
|||
valueOf: unknown; |
|||
toLocaleString: unknown; |
|||
} & BrandedRole; |
|||
|
|||
type SharedUserType = |
|||
| Pick<UserType, 'id' | 'role'> |
|||
| (Pick<UserType, 'id'> & { role: BrandedNumber }); |
|||
|
|||
type PermissionCheck<Key extends keyof Permissions> = |
|||
| boolean |
|||
| ((user: SharedUserType, data: Permissions[Key]['dataType']) => boolean); |
|||
|
|||
type RolesWithPermissions = { |
|||
[R in Roles]: { |
|||
[Key in keyof Permissions]: { |
|||
[Action in Permissions[Key]['action']]: PermissionCheck<Key>; |
|||
}; |
|||
}; |
|||
}; |
|||
|
|||
export const MATRIX: MATRIX = { |
|||
[actions.ADMIN]: [roles.ADMIN] as const, |
|||
[actions.CLIENT]: [roles.CLIENT, roles.ADMIN] as const, |
|||
} as const; |
|||
export type Permissions = { |
|||
clients: { |
|||
dataType: ClientType; |
|||
action: 'view' | 'create' | 'update' | 'delete' | 'custom'; |
|||
}; |
|||
admin: { |
|||
dataType: never; |
|||
action: 'any'; |
|||
}; |
|||
me: { |
|||
dataType: UserType; |
|||
action: 'update'; |
|||
}; |
|||
}; |
|||
|
|||
export const ROLES = { |
|||
ADMIN: { |
|||
clients: { |
|||
view: true, |
|||
create: true, |
|||
update: true, |
|||
delete: true, |
|||
custom: true, |
|||
}, |
|||
admin: { |
|||
any: true, |
|||
}, |
|||
me: { |
|||
update: (loggedIn, toChange) => loggedIn.id === toChange.id, |
|||
}, |
|||
}, |
|||
CLIENT: { |
|||
clients: { |
|||
view: (user, client) => user.id === client.userId, |
|||
create: false, |
|||
update: (user, client) => user.id === client.userId, |
|||
delete: (user, client) => user.id === client.userId, |
|||
custom: true, |
|||
}, |
|||
admin: { |
|||
any: false, |
|||
}, |
|||
me: { |
|||
update: (loggedIn, toChange) => loggedIn.id === toChange.id, |
|||
}, |
|||
}, |
|||
} as const satisfies RolesWithPermissions; |
|||
|
|||
export function hasPermissions<Resource extends keyof Permissions>( |
|||
user: SharedUserType, |
|||
resource: Resource, |
|||
action: Permissions[Resource]['action'], |
|||
data?: Permissions[Resource]['dataType'] |
|||
) { |
|||
const permission = (ROLES as RolesWithPermissions)[ |
|||
// TODO: remove typecast
|
|||
roleToKey(user.role as Role) |
|||
][resource][action]; |
|||
|
|||
if (typeof permission === 'boolean') { |
|||
return permission; |
|||
} |
|||
|
|||
export const checkPermissions = (action: Action, user: { role: Role }) => { |
|||
if (!MATRIX[action].includes(user.role)) { |
|||
if (data === undefined) { |
|||
return false; |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
return permission(user, data); |
|||
} |
|||
|
|||
export function hasPermissionsWithData<Resource extends keyof Permissions>( |
|||
user: UserType, |
|||
resource: Resource, |
|||
action: Permissions[Resource]['action'] |
|||
) { |
|||
let checked = false; |
|||
return { |
|||
check(data?: Permissions[Resource]['dataType']) { |
|||
checked = true; |
|||
const isAllowed = hasPermissions(user, resource, action, data); |
|||
|
|||
if (!isAllowed) { |
|||
throw new Error('Permission denied'); |
|||
} |
|||
|
|||
return isAllowed; |
|||
}, |
|||
isBoolean() { |
|||
return ( |
|||
typeof (ROLES as RolesWithPermissions)[roleToKey(user.role)][resource][ |
|||
action |
|||
] === 'boolean' |
|||
); |
|||
}, |
|||
get checked() { |
|||
return checked; |
|||
}, |
|||
}; |
|||
} |
|||
|
Loading…
Reference in new issue