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(); |
const generalConfig = await Database.general.getConfig(); |
||||
return generalConfig; |
return generalConfig; |
||||
}); |
}); |
||||
|
@ -1,4 +1,4 @@ |
|||||
export default definePermissionEventHandler(actions.ADMIN, async () => { |
export default definePermissionEventHandler('admin', 'any', async () => { |
||||
const hooks = await Database.hooks.get(); |
const hooks = await Database.hooks.get(); |
||||
return hooks; |
return hooks; |
||||
}); |
}); |
||||
|
@ -1,4 +1,4 @@ |
|||||
export default definePermissionEventHandler(actions.ADMIN, async () => { |
export default definePermissionEventHandler('admin', 'any', async () => { |
||||
const userConfig = await Database.userConfigs.get(); |
const userConfig = await Database.userConfigs.get(); |
||||
return userConfig; |
return userConfig; |
||||
}); |
}); |
||||
|
@ -1,3 +1,6 @@ |
|||||
export default definePermissionEventHandler(actions.CLIENT, () => { |
export default definePermissionEventHandler('clients', 'custom', ({ user }) => { |
||||
return WireGuard.getClients(); |
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 = { |
type BrandedRole = { |
||||
ADMIN: 'ADMIN', |
readonly __role: unique symbol; |
||||
CLIENT: 'CLIENT', |
}; |
||||
} as const; |
|
||||
|
|
||||
export type Role = number & { readonly __role: unique symbol }; |
export type Role = number & BrandedRole; |
||||
|
|
||||
export const roles = { |
export const roles = { |
||||
ADMIN: 1 as Role, |
ADMIN: 1 as Role, |
||||
CLIENT: 2 as Role, |
CLIENT: 2 as Role, |
||||
} as const; |
} 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; |
||||
|
} |
||||
|
|
||||
|
// TODO: https://github.com/nitrojs/nitro/issues/2758#issuecomment-2650531472
|
||||
|
|
||||
type MATRIX = { |
type BrandedNumber = { |
||||
readonly [key in keyof typeof actions]: readonly (typeof roles)[keyof typeof roles][]; |
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 = { |
export type Permissions = { |
||||
[actions.ADMIN]: [roles.ADMIN] as const, |
clients: { |
||||
[actions.CLIENT]: [roles.CLIENT, roles.ADMIN] as const, |
dataType: ClientType; |
||||
} as const; |
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 (data === undefined) { |
||||
if (!MATRIX[action].includes(user.role)) { |
|
||||
return false; |
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