Skip to content
Closed
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
3 changes: 3 additions & 0 deletions apps/client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useServiceStore } from '@/stores/services-monitor.js'
import { swaggerUiPath } from '@cpn-console/shared'
import ReloadPrompt from './components/ReloadPrompt.vue'
import { useAdminRoleStore } from './stores/admin-role.js'
import { usePermissionsStore } from './stores/permissions.js'

import { useProjectStore } from './stores/project.js'
import { useSnackbarStore } from './stores/snackbar.js'
Expand All @@ -16,6 +17,7 @@ const systemStore = useSystemSettingsStore()
const projectStore = useProjectStore()
const userStore = useUserStore()
const adminRoleStore = useAdminRoleStore()
const permissionsStore = usePermissionsStore()

const isLoggedIn = ref<boolean | undefined>(keycloak.authenticated)

Expand Down Expand Up @@ -46,6 +48,7 @@ onBeforeMount(async () => {
})
watch(userStore, async () => {
if (userStore.isLoggedIn) {
await permissionsStore.fetchPermissions()
if (!adminRoleStore.roles.length) {
await adminRoleStore.listRoles()
}
Expand Down
14 changes: 9 additions & 5 deletions apps/client/src/components/AdminRoleForm.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script lang="ts" setup>
import { computed, onBeforeMount, ref } from 'vue'
import type { AdminPermsKeys, LettersQuery, SharedZodError, User } from '@cpn-console/shared'
import { ADMIN_PERMS, RoleSchema, adminPermsDetails, shallowEqual } from '@cpn-console/shared'
import { RoleSchema, shallowEqual } from '@cpn-console/shared'
import pDebounce from 'p-debounce'
import { storeToRefs } from 'pinia'
import SuggestionInput from './SuggestionInput.vue'
import { useUsersStore } from '@/stores/users.js'
import { usePermissionsStore } from '@/stores/permissions.js'
import { clickInDialog, getRandomId } from '@/utils/func.js'

const props = withDefaults(defineProps<{
Expand All @@ -22,6 +24,8 @@ const emits = defineEmits<{
cancel: []
}>()
const usersStore = useUsersStore()
const permissionsStore = usePermissionsStore()
const { adminPermsDetails, adminPerms } = storeToRefs(permissionsStore)
const newUserInputKey = ref(getRandomId('input'))

const role = ref({
Expand Down Expand Up @@ -51,9 +55,9 @@ const selectedTabIndex = ref(initialSelectedIndex)

function updateChecked(checked: boolean, name: AdminPermsKeys) {
if (checked) {
role.value.permissions |= ADMIN_PERMS[name]
role.value.permissions |= adminPerms.value[name]
} else {
role.value.permissions &= ~ADMIN_PERMS[name]
role.value.permissions &= ~adminPerms.value[name]
}
}

Expand Down Expand Up @@ -157,12 +161,12 @@ function closeModal() {
<DsfrCheckbox
v-for="perm in scope.perms"
:key="perm.key"
:model-value="!!(ADMIN_PERMS[perm.key] & role.permissions)"
:model-value="!!(adminPerms[perm.key] & role.permissions)"
:data-testid="`${perm.key}-cbx`"
:label="perm.label"
:hint="perm?.hint"
:name="perm.key"
:disabled="role.permissions & ADMIN_PERMS.MANAGE && perm.key !== 'MANAGE'"
:disabled="!!(role.permissions & adminPerms.MANAGE) && perm.key !== 'MANAGE'"
@update:model-value="(checked: boolean) => updateChecked(checked, perm.key)"
/>
</div>
Expand Down
14 changes: 10 additions & 4 deletions apps/client/src/components/ProjectRoleForm.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import type { Member, ProjectV2, RoleBigint } from '@cpn-console/shared'
import { PROJECT_PERMS, projectPermsDetails, shallowEqual } from '@cpn-console/shared'
import { shallowEqual } from '@cpn-console/shared'
import { storeToRefs } from 'pinia'
import { usePermissionsStore } from '@/stores/permissions.js'

const props = defineProps<{
id: string
Expand All @@ -18,6 +20,10 @@ defineEmits<{
save: [value: Omit<RoleBigint, 'position'>]
cancel: []
}>()

const permissionsStore = usePermissionsStore()
const { projectPermsDetails, projectPerms } = storeToRefs(permissionsStore)

const router = useRouter()
const role = ref({
...props,
Expand Down Expand Up @@ -82,12 +88,12 @@ function updateChecked(checked: boolean, value: bigint) {
v-for="perm in scope.perms"
:id="`${perm.key}-cbx`"
:key="perm.key"
:model-value="!!(PROJECT_PERMS[perm.key] & role.permissions)"
:model-value="!!(projectPerms[perm.key] & role.permissions)"
:label="perm?.label"
:hint="perm?.hint"
:name="perm.key"
:disabled="role.permissions & PROJECT_PERMS.MANAGE && perm.key !== 'MANAGE'"
@update:model-value="(checked: boolean) => updateChecked(checked, PROJECT_PERMS[perm.key])"
:disabled="!!(role.permissions & projectPerms.MANAGE) && perm.key !== 'MANAGE'"
@update:model-value="(checked: boolean) => updateChecked(checked, projectPerms[perm.key])"
/>
</div>
<DsfrButton
Expand Down
43 changes: 43 additions & 0 deletions apps/client/src/stores/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { apiClient, extractData } from '@/api/xhr-client.js'
import {
type PermDetails,
type ProjectPermsKeys,
type AdminPermsKeys,
projectPermsDetails as defaultProjectPermsDetails,
adminPermsDetails as defaultAdminPermsDetails,
PROJECT_PERMS as defaultProjectPerms,
ADMIN_PERMS as defaultAdminPerms,
} from '@cpn-console/shared'

export const usePermissionsStore = defineStore('permissions', () => {
const projectPermsDetails = ref<PermDetails<ProjectPermsKeys>>(defaultProjectPermsDetails)
const adminPermsDetails = ref<PermDetails<AdminPermsKeys>>(defaultAdminPermsDetails)
const projectPerms = ref<Record<string, bigint>>(defaultProjectPerms)
const adminPerms = ref<Record<string, bigint>>(defaultAdminPerms)

const fetchPermissions = async () => {
const data = await apiClient.System.getConf().then(response => extractData(response, 200))
// @ts-ignore
projectPermsDetails.value = data.projectPermsDetails
// @ts-ignore
adminPermsDetails.value = data.adminPermsDetails

// Parse BigInts
projectPerms.value = Object.fromEntries(
Object.entries(data.projectPerms).map(([k, v]) => [k, BigInt(v as string)]),
)
adminPerms.value = Object.fromEntries(
Object.entries(data.adminPerms).map(([k, v]) => [k, BigInt(v as string)]),
)
}

return {
projectPermsDetails,
adminPermsDetails,
projectPerms,
adminPerms,
fetchPermissions,
}
})
20 changes: 19 additions & 1 deletion apps/server/src/resources/system/router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { systemContract } from '@cpn-console/shared'
import { ADMIN_PERMS, PROJECT_PERMS, adminPermsDetails, projectPermsDetails, systemContract } from '@cpn-console/shared'
import { serverInstance } from '@/app.js'
import { appVersion } from '@/utils/env.js'

Expand All @@ -17,5 +17,23 @@ export function systemRouter() {
status: 'OK',
},
}),

getConf: async () => {
const projectPerms = Object.fromEntries(
Object.entries(PROJECT_PERMS).map(([k, v]) => [k, v.toString()]),
)
const adminPerms = Object.fromEntries(
Object.entries(ADMIN_PERMS).map(([k, v]) => [k, v.toString()]),
)
return {
status: 200,
body: {
projectPermsDetails,
adminPermsDetails,
projectPerms,
adminPerms,
},
}
},
})
}
2 changes: 1 addition & 1 deletion packages/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ declare module '@cpn-console/hooks' {
```json
// package.json
{
"devDepencies": {
"peerDependencies": {
"my_plugin": "1.2.3"
}
}
Expand Down
13 changes: 12 additions & 1 deletion packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Monitor } from '@cpn-console/shared'
import type { PluginApi } from './utils/utils.js'
import { objectEntries } from './utils/utils.js'
import * as hooks from './hooks/index.js'
import { type ServiceInfos, servicesInfos } from './services.js'
import type { HookStepsNames, StepCall } from './hooks/hook.js'
import { addPlugin, editStrippers } from './config.js'
import { addAdminPerms, addProjectPerms, type AdminPermsKeys, type Monitor, type PermDetails, type ProjectPermsKeys } from '@cpn-console/shared'

export * from './utils/logger.js'

Expand All @@ -26,6 +26,10 @@ export interface Plugin {
subscribedHooks: PluginsFunctions
monitor?: Monitor
start?: (options: unknown) => void
permissions?: {
project?: { perms: Record<string, bigint>, details: PermDetails<ProjectPermsKeys> }
admin?: { perms: Record<string, bigint>, details: PermDetails<AdminPermsKeys> }
}
}

export type RegisterFn = (plugin: Plugin) => void
Expand All @@ -52,6 +56,13 @@ function pluginManager(options: PluginManagerOptions): PluginManager {
addPlugin(plugin.infos.name, plugin.infos.config, editStrippers)
}

if (plugin.permissions?.project) {
addProjectPerms(plugin.permissions.project.perms, plugin.permissions.project.details)
}
if (plugin.permissions?.admin) {
addAdminPerms(plugin.permissions.admin.perms, plugin.permissions.admin.details)
}

if (plugin.infos.to && config.mockExternalServices)
plugin.infos.to = () => [{ name: 'Lien', to: 'https://theuselessweb.com/' }]
if (plugin.start && options.startPlugins)
Expand Down
30 changes: 30 additions & 0 deletions packages/shared/src/contracts/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,36 @@ export const systemContract = contractInstance.router({
500: ErrorSchema,
},
},

getConf: {
method: 'GET',
path: '/conf',
summary: 'Get configuration',
description: 'Retrieve system configuration including permissions.',
responses: {
200: z.object({
projectPermsDetails: z.array(z.object({
name: z.string(),
perms: z.array(z.object({
key: z.string(),
label: z.string(),
hint: z.string().optional(),
})),
})),
adminPermsDetails: z.array(z.object({
name: z.string(),
perms: z.array(z.object({
key: z.string(),
label: z.string(),
hint: z.string().optional(),
})),
})),
projectPerms: z.record(z.string()),
adminPerms: z.record(z.string()),
}),
500: ErrorSchema,
},
},
}, {
pathPrefix: `${apiPrefix}`,
})
Expand Down
52 changes: 44 additions & 8 deletions packages/shared/src/utils/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function permissionsParser(a: Record<string, bigint>) {
const bit = (position: bigint) => 1n << position

// Be very careful and think to apply corresponding updates in database if you modify these values, You'll have to do binary updates in SQL, good luck !
export const PROJECT_PERMS = { // project permissions
export const PROJECT_PERMS: Record<string, bigint> = { // project permissions
GUEST: bit(0n),
MANAGE: bit(1n),
MANAGE_MEMBERS: bit(2n),
Expand All @@ -57,12 +57,12 @@ export const PROJECT_PERMS = { // project permissions
}

// Be very careful and think to apply corresponding updates in database if you modify these values, You'll have to do binary updates in SQL, good luck !
export const ADMIN_PERMS = { // admin permissions
export const ADMIN_PERMS: Record<string, bigint> = { // admin permissions
MANAGE: bit(1n),
}

export type ProjectPermsKeys = keyof typeof PROJECT_PERMS
export type AdminPermsKeys = keyof typeof ADMIN_PERMS
export type ProjectPermsKeys = keyof typeof PROJECT_PERMS | string
export type AdminPermsKeys = keyof typeof ADMIN_PERMS | string

permissionsParser(ADMIN_PERMS)
permissionsParser(PROJECT_PERMS)
Expand Down Expand Up @@ -102,11 +102,11 @@ export const ProjectAuthorized = {
|| !!(toBigInt(perms.projectPermissions) & (PROJECT_PERMS.SEE_SECRETS | PROJECT_PERMS.MANAGE)),
} as const

interface ScopePerm<T extends string> {
export interface ScopePerm<T extends string> {
name: string
perms: Array<{ key: T, label: string, hint?: string }>
}
type PermDetails<T extends string> = Array<ScopePerm<T>>
export type PermDetails<T extends string> = Array<ScopePerm<T>>

export const projectPermsDetails: PermDetails<ProjectPermsKeys> = [{
name: 'Projet',
Expand Down Expand Up @@ -159,7 +159,31 @@ export const projectPermsDetails: PermDetails<ProjectPermsKeys> = [{
hint: 'Permet de visualiser tous les dépôts et leurs configurations',
},
],
}] as const
}, {
name: 'Rôles',
perms: [
{
key: 'REPORTER',
label: 'Reporter',
hint: 'Rôle Reporter',
},
{
key: 'DEVELOPER',
label: 'Developer',
hint: 'Rôle Developer',
},
{
key: 'MAINTAINER',
label: 'Maintainer',
hint: 'Rôle Maintainer',
},
{
key: 'OWNER',
label: 'Owner',
hint: 'Rôle Owner',
},
],
}]

export const adminPermsDetails: PermDetails<AdminPermsKeys> = [{
name: 'Global',
Expand All @@ -168,7 +192,19 @@ export const adminPermsDetails: PermDetails<AdminPermsKeys> = [{
label: 'Administration globale',
hint: 'Administration globale de toute la console et de ses ressources',
}],
}] as const
}]

export function addProjectPerms(perms: Record<string, bigint>, details: PermDetails<ProjectPermsKeys>) {
Object.assign(PROJECT_PERMS, perms)
permissionsParser(PROJECT_PERMS)
projectPermsDetails.push(...details)
}

export function addAdminPerms(perms: Record<string, bigint>, details: PermDetails<AdminPermsKeys>) {
Object.assign(ADMIN_PERMS, perms)
permissionsParser(ADMIN_PERMS)
adminPermsDetails.push(...details)
}

export function getAdminPermLabelsByValue(value: bigint | string) {
value = typeof value === 'bigint' ? value : BigInt(value)
Expand Down