From e85fa1a3928ca7968449f19f2d8b99a7212a0488 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 24 Apr 2026 09:24:09 +0200 Subject: [PATCH 1/5] feat(frontend): ufo access keys --- .../setup/MissionControlAccessKeys.svelte | 7 +- .../access-keys/AccessKeyDelete.svelte | 4 +- .../modules/access-keys/AccessKeys.svelte | 13 +-- .../orbiter/setup/OrbiterAccessKeys.svelte | 7 +- .../setup/SatelliteAccessKeys.svelte | 10 +- .../ufos/setup/UfoAccessKeys.svelte | 103 ++++++++++++++++++ .../components/ufos/setup/UfoSettings.svelte | 5 +- .../access-keys/_key.list.services.ts | 18 +++ .../mission-control.key.list.services.ts | 13 +++ .../access-keys/orbiter.key.list.services.ts | 13 +++ .../satellites.key.list.services.ts | 13 +++ .../services/access-keys/ufo.key.services.ts | 32 ++++++ src/frontend/src/lib/types/access-keys.ts | 10 ++ 13 files changed, 226 insertions(+), 22 deletions(-) create mode 100644 src/frontend/src/lib/components/ufos/setup/UfoAccessKeys.svelte create mode 100644 src/frontend/src/lib/services/access-keys/_key.list.services.ts create mode 100644 src/frontend/src/lib/services/access-keys/mission-control.key.list.services.ts create mode 100644 src/frontend/src/lib/services/access-keys/orbiter.key.list.services.ts create mode 100644 src/frontend/src/lib/services/access-keys/satellites.key.list.services.ts create mode 100644 src/frontend/src/lib/services/access-keys/ufo.key.services.ts diff --git a/src/frontend/src/lib/components/mission-control/setup/MissionControlAccessKeys.svelte b/src/frontend/src/lib/components/mission-control/setup/MissionControlAccessKeys.svelte index ecbc2b5f68..ba61db891f 100644 --- a/src/frontend/src/lib/components/mission-control/setup/MissionControlAccessKeys.svelte +++ b/src/frontend/src/lib/components/mission-control/setup/MissionControlAccessKeys.svelte @@ -2,9 +2,9 @@ import { nonNullish } from '@dfinity/utils'; import type { Principal } from '@icp-sdk/core/principal'; import type { MissionControlDid } from '$declarations'; - import { listMissionControlControllers } from '$lib/api/mission-control.api'; import AccessKeys from '$lib/components/modules/access-keys/AccessKeys.svelte'; import { authIdentity } from '$lib/derived/auth.derived'; + import { listMissionControlControllers } from '$lib/services/access-keys/mission-control.key.list.services'; import { addMissionControlAccessKey, removeMissionControlAccessKey @@ -13,7 +13,8 @@ import type { AddAccessKeyResult, AddAccessKeyParams, - AccessKeyIdParam + AccessKeyIdParam, + AccessKeyUi } from '$lib/types/access-keys'; import type { MissionControlId } from '$lib/types/mission-control'; @@ -23,7 +24,7 @@ let { missionControlId }: Props = $props(); - const list = (): Promise<[Principal, MissionControlDid.AccessKey][]> => + const list = (): Promise<[Principal, AccessKeyUi][]> => listMissionControlControllers({ missionControlId, identity: $authIdentity }); const remove = async (accessKey: AccessKeyIdParam): Promise => diff --git a/src/frontend/src/lib/components/modules/access-keys/AccessKeyDelete.svelte b/src/frontend/src/lib/components/modules/access-keys/AccessKeyDelete.svelte index dc92122573..a9673620a3 100644 --- a/src/frontend/src/lib/components/modules/access-keys/AccessKeyDelete.svelte +++ b/src/frontend/src/lib/components/modules/access-keys/AccessKeyDelete.svelte @@ -7,11 +7,11 @@ import { busy } from '$lib/stores/app/busy.store'; import { i18n } from '$lib/stores/app/i18n.store'; import { toasts } from '$lib/stores/app/toasts.store'; - import type { AccessKeyIdParam, AddAccessKeyResult } from '$lib/types/access-keys'; + import type { AccessKeyIdParam, AccessKeyUi, AddAccessKeyResult } from '$lib/types/access-keys'; interface Props { visible?: boolean; - selectedController: [Principal, SatelliteDid.AccessKey | undefined] | undefined; + selectedController: [Principal, AccessKeyUi | undefined] | undefined; remove: (params: AccessKeyIdParam) => Promise; load: () => Promise; } diff --git a/src/frontend/src/lib/components/modules/access-keys/AccessKeys.svelte b/src/frontend/src/lib/components/modules/access-keys/AccessKeys.svelte index d43103f989..0eea4624bb 100644 --- a/src/frontend/src/lib/components/modules/access-keys/AccessKeys.svelte +++ b/src/frontend/src/lib/components/modules/access-keys/AccessKeys.svelte @@ -15,23 +15,24 @@ import type { AddAccessKeyResult, AddAccessKeyParams, - AccessKeyIdParam + AccessKeyIdParam, + AccessKeyUi } from '$lib/types/access-keys'; import type { CanisterSegmentWithLabel } from '$lib/types/canister'; import { metadataProfile } from '$lib/utils/metadata.utils'; interface Props { - list: () => Promise<[Principal, MissionControlDid.AccessKey][]>; + list: () => Promise<[Principal, AccessKeyUi][]>; remove: (params: AccessKeyIdParam) => Promise; add: (params: AddAccessKeyParams) => Promise; segment: CanisterSegmentWithLabel; // The canister and user are controllers of the mission control but not added in its state per default - extraControllers?: [Principal, MissionControlDid.AccessKey][]; + extraControllers?: [Principal, AccessKeyUi][]; } let { list, remove, add, segment, extraControllers = [] }: Props = $props(); - let controllers = $state<[Principal, MissionControlDid.AccessKey][]>([]); + let controllers = $state<[Principal, AccessKeyUi][]>([]); const load = async () => { try { @@ -52,9 +53,7 @@ let visibleDelete = $state(false); let visibleInfo = $state(false); - let selectedController = $state<[Principal, MissionControlDid.AccessKey | undefined] | undefined>( - undefined - ); + let selectedController = $state<[Principal, AccessKeyUi | undefined] | undefined>(undefined); const isMissionControl = (controllerId: Principal): boolean => nonNullish($missionControlId) && $missionControlId.toText() === controllerId.toText(); diff --git a/src/frontend/src/lib/components/orbiter/setup/OrbiterAccessKeys.svelte b/src/frontend/src/lib/components/orbiter/setup/OrbiterAccessKeys.svelte index f004348d1c..1c4245d4b9 100644 --- a/src/frontend/src/lib/components/orbiter/setup/OrbiterAccessKeys.svelte +++ b/src/frontend/src/lib/components/orbiter/setup/OrbiterAccessKeys.svelte @@ -2,12 +2,12 @@ import type { Principal } from '@icp-sdk/core/principal'; import type { MissionControlDid } from '$declarations'; import { deleteOrbitersController, setOrbitersController } from '$lib/api/mission-control.api'; - import { listOrbiterControllers } from '$lib/api/orbiter.api'; import AccessKeys from '$lib/components/modules/access-keys/AccessKeys.svelte'; import { authIdentity } from '$lib/derived/auth.derived'; import { missionControlId } from '$lib/derived/console/account.mission-control.derived'; import { addAccessKey, removeAccessKey } from '$lib/services/access-keys/access-keys.services'; import { addOrbiterAccessKey } from '$lib/services/access-keys/orbiter.key.add.services'; + import { listOrbiterControllers } from '$lib/services/access-keys/orbiter.key.list.services'; import { removeOrbiterAccessKey } from '$lib/services/access-keys/orbiter.key.remove.services'; import { i18n } from '$lib/stores/app/i18n.store'; import type { @@ -15,7 +15,8 @@ AddAccessKeyParams, AccessKeyWithDevFn, AccessKeyWithMissionControlFn, - AccessKeyIdParam + AccessKeyIdParam, + AccessKeyUi } from '$lib/types/access-keys'; interface Props { @@ -24,7 +25,7 @@ let { orbiterId }: Props = $props(); - const list = (): Promise<[Principal, MissionControlDid.AccessKey][]> => + const list = (): Promise<[Principal, AccessKeyUi][]> => listOrbiterControllers({ orbiterId, identity: $authIdentity }); const remove = async (accessKey: AccessKeyIdParam): Promise => { diff --git a/src/frontend/src/lib/components/satellites/setup/SatelliteAccessKeys.svelte b/src/frontend/src/lib/components/satellites/setup/SatelliteAccessKeys.svelte index 66b1b5e4db..90c078168a 100644 --- a/src/frontend/src/lib/components/satellites/setup/SatelliteAccessKeys.svelte +++ b/src/frontend/src/lib/components/satellites/setup/SatelliteAccessKeys.svelte @@ -1,16 +1,15 @@ + + diff --git a/src/frontend/src/lib/components/ufos/setup/UfoSettings.svelte b/src/frontend/src/lib/components/ufos/setup/UfoSettings.svelte index 89fa8dd9d1..9eae5a6235 100644 --- a/src/frontend/src/lib/components/ufos/setup/UfoSettings.svelte +++ b/src/frontend/src/lib/components/ufos/setup/UfoSettings.svelte @@ -1,9 +1,8 @@ diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index fd93a7752b..e2ddf21eb6 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -660,6 +660,7 @@ "controllers_no_selection": "No access key to delete selected.", "controllers_add": "Unexpected error(s) while adding the access key.", "controllers_delete": "Unexpected error(s) while deleting the access key.", + "ufo_controller_not_admin": "Only access keys with admin privileges can be added to a UFO.", "data_delete": "Unexpected error(s) while deleting.", "key_invalid": "Key is invalid or empty.", "full_path_invalid": "Key (full_path) is invalid or empty.", diff --git a/src/frontend/src/lib/i18n/zh-cn.json b/src/frontend/src/lib/i18n/zh-cn.json index 0cfa3f412a..f368f225f3 100644 --- a/src/frontend/src/lib/i18n/zh-cn.json +++ b/src/frontend/src/lib/i18n/zh-cn.json @@ -662,6 +662,7 @@ "controllers_no_selection": "未选择要删除的密钥。", "controllers_add": "添加访问密钥时发生意外错误。", "controllers_delete": "删除密钥时发生意外错误。", + "ufo_controller_not_admin": "只有具有管理员权限的访问密钥才能添加到 UFO。", "data_delete": "删除时发生意外错误。", "key_invalid": "密钥无效或为空。", "full_path_invalid": "密钥(full_path)无效或为空。", diff --git a/src/frontend/src/lib/services/access-keys/ufo.key.services.ts b/src/frontend/src/lib/services/access-keys/ufo.key.services.ts index 296e1fec81..4c5d419505 100644 --- a/src/frontend/src/lib/services/access-keys/ufo.key.services.ts +++ b/src/frontend/src/lib/services/access-keys/ufo.key.services.ts @@ -1,9 +1,13 @@ import { canisterStatus } from '$lib/api/ic.api'; -import type { AccessKeyUi } from '$lib/types/access-keys'; +import { setAdminController } from '$lib/services/access-keys/key.admin.services'; +import { i18n } from '$lib/stores/app/i18n.store'; +import { toasts } from '$lib/stores/app/toasts.store'; +import type { AccessKeyIdParam, AccessKeyUi, AddAccessKeyParams, AddAccessKeyResult } from '$lib/types/access-keys'; import type { NullishIdentity } from '$lib/types/itentity'; import type { UfoId } from '$lib/types/ufo'; -import { assertNonNullish, toNullable } from '@dfinity/utils'; +import { assertNonNullish, isNullish, toNullable } from '@dfinity/utils'; import type { Principal } from '@icp-sdk/core/principal'; +import { get } from 'svelte/store'; export const listUfoControllers = async ({ ufoId, @@ -30,3 +34,74 @@ export const listUfoControllers = async ({ } ]); }; + +export const addUfoController = async ({ + identity, + accessKey: { scope, ...accessKeyRest }, + ufoId +}: { + identity: NullishIdentity; + accessKey: AddAccessKeyParams; + ufoId: UfoId; +}): Promise => { + // TODO: indentity check service + if (isNullish(identity) || isNullish(identity?.getPrincipal())) { + toasts.error({ text: get(i18n).core.not_logged_in }); + return { result: 'error' }; + } + + if (scope !== 'admin') { + toasts.error({ text: get(i18n).errors.ufo_controller_not_admin }); + return { result: 'error' }; + } + + try { + await setAdminController({ + ...accessKeyRest, + canisterId: ufoId, + identity + }); + + return { result: 'ok' }; + } catch (err: unknown) { + toasts.error({ + text: get(i18n).errors.controllers_add, + detail: err + }); + + return { result: 'error', err }; + } +}; + +export const removeUfoController = async ({ + identity, + accessKey: { accessKeyId }, + ufoId +}: { + identity: NullishIdentity; + accessKey: AccessKeyIdParam; + ufoId: UfoId; +}): Promise => { + // TODO: indentity check service + if (isNullish(identity) || isNullish(identity?.getPrincipal())) { + toasts.error({ text: get(i18n).core.not_logged_in }); + return { result: 'error' }; + } + + try { + await setAdminController({ + ...accessKeyRest, + canisterId: ufoId, + identity + }); + + return { result: 'ok' }; + } catch (err: unknown) { + toasts.error({ + text: get(i18n).errors.controllers_delete, + detail: err + }); + + return { result: 'error', err }; + } +}; diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index 141146f5cc..5b46b04554 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -678,6 +678,7 @@ interface I18nErrors { controllers_no_selection: string; controllers_add: string; controllers_delete: string; + ufo_controller_not_admin: string; data_delete: string; key_invalid: string; full_path_invalid: string; From 00602e6001f4b9c3bc69ee34b767034b0c07ce3f Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 24 Apr 2026 09:55:28 +0200 Subject: [PATCH 3/5] feat: remove controller --- .../ufos/setup/UfoAccessKeys.svelte | 32 +++------ .../services/access-keys/ufo.key.services.ts | 65 +++++++++++++++++-- 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/src/frontend/src/lib/components/ufos/setup/UfoAccessKeys.svelte b/src/frontend/src/lib/components/ufos/setup/UfoAccessKeys.svelte index 24ae257686..02f08afede 100644 --- a/src/frontend/src/lib/components/ufos/setup/UfoAccessKeys.svelte +++ b/src/frontend/src/lib/components/ufos/setup/UfoAccessKeys.svelte @@ -12,7 +12,11 @@ import { addAccessKey, removeAccessKey } from '$lib/services/access-keys/access-keys.services'; import { addSatellitesAccessKey } from '$lib/services/access-keys/satellites.key.add.services'; import { removeSatellitesAccessKey } from '$lib/services/access-keys/satellites.key.remove.services'; - import { addUfoController, listUfoControllers } from '$lib/services/access-keys/ufo.key.services'; + import { + addUfoController, + listUfoControllers, + removeUfoController + } from '$lib/services/access-keys/ufo.key.services'; import { i18n } from '$lib/stores/app/i18n.store'; import type { AddAccessKeyResult, @@ -35,30 +39,10 @@ listUfoControllers({ ufoId: ufo.ufo_id, identity: $authIdentity }); const remove = async (accessKey: AccessKeyIdParam): Promise => { - const satelliteIds = [satellite.satellite_id]; - - const removeAccessKeyWithMissionControlFn: AccessKeyWithMissionControlFn = async (params) => { - await deleteSatellitesController({ - ...params, - ...accessKey, - satelliteIds - }); - }; - - const removeAccessKeyWithDevFn: AccessKeyWithDevFn = async (params) => { - await removeSatellitesAccessKey({ - ...accessKey, - ...params, - satelliteIds - }); - }; - - return await removeAccessKey({ + return await removeUfoController({ identity: $authIdentity, - missionControlId: $missionControlId, - accessKey, - removeAccessKeyWithMissionControlFn, - removeAccessKeyWithDevFn + ufoId: ufo.ufo_id, + accessKey }); }; diff --git a/src/frontend/src/lib/services/access-keys/ufo.key.services.ts b/src/frontend/src/lib/services/access-keys/ufo.key.services.ts index 4c5d419505..7c103cde52 100644 --- a/src/frontend/src/lib/services/access-keys/ufo.key.services.ts +++ b/src/frontend/src/lib/services/access-keys/ufo.key.services.ts @@ -1,12 +1,19 @@ -import { canisterStatus } from '$lib/api/ic.api'; +import type { ICDid } from '$declarations'; +import { canisterStatus, canisterUpdateSettings } from '$lib/api/ic.api'; import { setAdminController } from '$lib/services/access-keys/key.admin.services'; import { i18n } from '$lib/stores/app/i18n.store'; import { toasts } from '$lib/stores/app/toasts.store'; -import type { AccessKeyIdParam, AccessKeyUi, AddAccessKeyParams, AddAccessKeyResult } from '$lib/types/access-keys'; +import type { + AccessKeyIdParam, + AccessKeyUi, + AddAccessKeyParams, + AddAccessKeyResult +} from '$lib/types/access-keys'; import type { NullishIdentity } from '$lib/types/itentity'; import type { UfoId } from '$lib/types/ufo'; import { assertNonNullish, isNullish, toNullable } from '@dfinity/utils'; -import type { Principal } from '@icp-sdk/core/principal'; +import type { Identity } from '@icp-sdk/core/agent'; +import { Principal } from '@icp-sdk/core/principal'; import { get } from 'svelte/store'; export const listUfoControllers = async ({ @@ -89,8 +96,12 @@ export const removeUfoController = async ({ } try { - await setAdminController({ - ...accessKeyRest, + const controllerId = Principal.isPrincipal(accessKeyId) + ? accessKeyId + : Principal.fromText(accessKeyId); + + await removeController({ + controllerId, canisterId: ufoId, identity }); @@ -105,3 +116,47 @@ export const removeUfoController = async ({ return { result: 'error', err }; } }; + +const removeController = async ({ + controllerId, + canisterId, + identity +}: { + controllerId: Principal; + canisterId: Principal; + identity: Identity; +}): Promise<{ result: 'ok' | 'skip' } | { result: 'reject'; reason: string }> => { + const { + settings: { controllers: currentControllers } + } = await canisterStatus({ + identity, + canisterId: canisterId.toText(), + // We need to be sure the list of controllers we get is correct here + // as we are using those to update the settings. + certified: true + }); + + const remainingControllers = currentControllers.filter( + (cId) => cId.toText() !== controllerId.toText() + ); + + const updateSettings: ICDid.canister_settings = { + environment_variables: toNullable(), + compute_allocation: toNullable(), + freezing_threshold: toNullable(), + log_visibility: toNullable(), + memory_allocation: toNullable(), + reserved_cycles_limit: toNullable(), + wasm_memory_limit: toNullable(), + wasm_memory_threshold: toNullable(), + controllers: toNullable(remainingControllers) + }; + + await canisterUpdateSettings({ + canisterId, + identity, + settings: updateSettings + }); + + return { result: 'ok' }; +}; From cfddeb805314594f4ab9c59f0ff3efa7e91b255a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 24 Apr 2026 09:58:28 +0200 Subject: [PATCH 4/5] feat: always admin --- .../lib/components/modals/setup/AccessKeyCreateModal.svelte | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/lib/components/modals/setup/AccessKeyCreateModal.svelte b/src/frontend/src/lib/components/modals/setup/AccessKeyCreateModal.svelte index d164b6e39c..c0961d3051 100644 --- a/src/frontend/src/lib/components/modals/setup/AccessKeyCreateModal.svelte +++ b/src/frontend/src/lib/components/modals/setup/AccessKeyCreateModal.svelte @@ -26,7 +26,8 @@ let step: 'init' | 'in_progress' | 'ready' | 'error' = $state('init'); let accessKeyId = $state(''); - let scope = $state('write'); + // svelte-ignore state_referenced_locally + let scope = $state(segment.segment === 'ufo' ? 'admin' : 'write'); let identity: string | undefined = $state(); const initAccessKey = (): string | undefined => { @@ -180,7 +181,7 @@ {#snippet label()} {$i18n.controllers.scope} {/snippet} - From 331503a242ba800d9b648202a55593c03ffea019 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 24 Apr 2026 10:03:45 +0200 Subject: [PATCH 5/5] chore: fmt --- .../modals/setup/AccessKeyCreateModal.svelte | 2 +- .../access-keys/AccessKeyDelete.svelte | 1 - .../modules/access-keys/AccessKeys.svelte | 1 - .../orbiter/setup/OrbiterAccessKeys.svelte | 1 - .../ufos/setup/UfoAccessKeys.svelte | 23 ++++--------------- 5 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/frontend/src/lib/components/modals/setup/AccessKeyCreateModal.svelte b/src/frontend/src/lib/components/modals/setup/AccessKeyCreateModal.svelte index c0961d3051..ee6404f299 100644 --- a/src/frontend/src/lib/components/modals/setup/AccessKeyCreateModal.svelte +++ b/src/frontend/src/lib/components/modals/setup/AccessKeyCreateModal.svelte @@ -181,7 +181,7 @@ {#snippet label()} {$i18n.controllers.scope} {/snippet} - diff --git a/src/frontend/src/lib/components/modules/access-keys/AccessKeyDelete.svelte b/src/frontend/src/lib/components/modules/access-keys/AccessKeyDelete.svelte index a9673620a3..2e414e085b 100644 --- a/src/frontend/src/lib/components/modules/access-keys/AccessKeyDelete.svelte +++ b/src/frontend/src/lib/components/modules/access-keys/AccessKeyDelete.svelte @@ -1,7 +1,6 @@