From b2ce32a07659f978f924700d6f641bf8c9fda55c Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Tue, 1 Apr 2025 14:42:50 +0200 Subject: [PATCH 01/21] NORY-59: fix database relations file --- dashboard/drizzle/relations.ts | 318 ++++++++++++++++----------------- 1 file changed, 159 insertions(+), 159 deletions(-) diff --git a/dashboard/drizzle/relations.ts b/dashboard/drizzle/relations.ts index 615b353..47aff7f 100644 --- a/dashboard/drizzle/relations.ts +++ b/dashboard/drizzle/relations.ts @@ -1,334 +1,334 @@ import { relations } from 'drizzle-orm/relations'; import { - continuityContainers, - courierMessageDispatches, - courierMessages, + continuity_containers, + courier_message_dispatches, + courier_messages, identities, - identityCredentialIdentifiers, - identityCredentials, - identityCredentialTypes, - identityLoginCodes, - identityRecoveryAddresses, - identityRecoveryCodes, - identityRecoveryTokens, - identityRegistrationCodes, - identityVerifiableAddresses, - identityVerificationCodes, - identityVerificationTokens, + identity_credential_identifiers, + identity_credential_types, + identity_credentials, + identity_login_codes, + identity_recovery_addresses, + identity_recovery_codes, + identity_recovery_tokens, + identity_registration_codes, + identity_verifiable_addresses, + identity_verification_codes, + identity_verification_tokens, networks, - selfserviceErrors, - selfserviceLoginFlows, - selfserviceRecoveryFlows, - selfserviceRegistrationFlows, - selfserviceSettingsFlows, - selfserviceVerificationFlows, - sessionDevices, + selfservice_errors, + selfservice_login_flows, + selfservice_recovery_flows, + selfservice_registration_flows, + selfservice_settings_flows, + selfservice_verification_flows, + session_devices, sessions, } from './schema'; -export const identityCredentialsRelations = relations(identityCredentials, ({ one, many }) => ({ +export const identityCredentialsRelations = relations(identity_credentials, ({ one, many }) => ({ identity: one(identities, { - fields: [identityCredentials.identityId], + fields: [identity_credentials.identity_id], references: [identities.id], }), - identityCredentialType: one(identityCredentialTypes, { - fields: [identityCredentials.identityCredentialTypeId], - references: [identityCredentialTypes.id], + identityCredentialType: one(identity_credential_types, { + fields: [identity_credentials.identity_credential_type_id], + references: [identity_credential_types.id], }), network: one(networks, { - fields: [identityCredentials.nid], + fields: [identity_credentials.nid], references: [networks.id], }), - identityCredentialIdentifiers: many(identityCredentialIdentifiers), + identityCredentialIdentifiers: many(identity_credential_identifiers), })); export const identitiesRelations = relations(identities, ({ one, many }) => ({ - identityCredentials: many(identityCredentials), + identityCredentials: many(identity_credentials), network: one(networks, { fields: [identities.nid], references: [networks.id], }), - identityVerifiableAddresses: many(identityVerifiableAddresses), - selfserviceSettingsFlows: many(selfserviceSettingsFlows), - continuityContainers: many(continuityContainers), + identityVerifiableAddresses: many(identity_verifiable_addresses), + selfserviceSettingsFlows: many(selfservice_settings_flows), + continuityContainers: many(continuity_containers), sessions: many(sessions), - identityRecoveryAddresses: many(identityRecoveryAddresses), - selfserviceRecoveryFlows: many(selfserviceRecoveryFlows), - identityRecoveryTokens: many(identityRecoveryTokens), - identityRecoveryCodes: many(identityRecoveryCodes), - identityLoginCodes: many(identityLoginCodes), + identityRecoveryAddresses: many(identity_recovery_addresses), + selfserviceRecoveryFlows: many(selfservice_recovery_flows), + identityRecoveryTokens: many(identity_recovery_tokens), + identityRecoveryCodes: many(identity_recovery_codes), + identityLoginCodes: many(identity_login_codes), })); -export const identityCredentialTypesRelations = relations(identityCredentialTypes, ({ many }) => ({ - identityCredentials: many(identityCredentials), - identityCredentialIdentifiers: many(identityCredentialIdentifiers), +export const identityCredentialTypesRelations = relations(identity_credential_types, ({ many }) => ({ + identityCredentials: many(identity_credentials), + identityCredentialIdentifiers: many(identity_credential_identifiers), })); export const networksRelations = relations(networks, ({ many }) => ({ - identityCredentials: many(identityCredentials), - selfserviceLoginFlows: many(selfserviceLoginFlows), - selfserviceRegistrationFlows: many(selfserviceRegistrationFlows), + identityCredentials: many(identity_credentials), + selfserviceLoginFlows: many(selfservice_login_flows), + selfserviceRegistrationFlows: many(selfservice_registration_flows), identities: many(identities), - identityCredentialIdentifiers: many(identityCredentialIdentifiers), - identityVerifiableAddresses: many(identityVerifiableAddresses), - courierMessages: many(courierMessages), - selfserviceErrors: many(selfserviceErrors), - selfserviceVerificationFlows: many(selfserviceVerificationFlows), - selfserviceSettingsFlows: many(selfserviceSettingsFlows), - continuityContainers: many(continuityContainers), + identityCredentialIdentifiers: many(identity_credential_identifiers), + identityVerifiableAddresses: many(identity_verifiable_addresses), + courierMessages: many(courier_messages), + selfserviceErrors: many(selfservice_errors), + selfserviceVerificationFlows: many(selfservice_verification_flows), + selfserviceSettingsFlows: many(selfservice_settings_flows), + continuityContainers: many(continuity_containers), sessions: many(sessions), - identityRecoveryAddresses: many(identityRecoveryAddresses), - identityVerificationTokens: many(identityVerificationTokens), - selfserviceRecoveryFlows: many(selfserviceRecoveryFlows), - identityRecoveryTokens: many(identityRecoveryTokens), - identityRecoveryCodes: many(identityRecoveryCodes), - sessionDevices: many(sessionDevices), - identityVerificationCodes: many(identityVerificationCodes), - courierMessageDispatches: many(courierMessageDispatches), - identityLoginCodes: many(identityLoginCodes), - identityRegistrationCodes: many(identityRegistrationCodes), + identityRecoveryAddresses: many(identity_recovery_addresses), + identityVerificationTokens: many(identity_verification_tokens), + selfserviceRecoveryFlows: many(selfservice_recovery_flows), + identityRecoveryTokens: many(identity_recovery_tokens), + identityRecoveryCodes: many(identity_recovery_codes), + sessionDevices: many(session_devices), + identityVerificationCodes: many(identity_verification_codes), + courierMessageDispatches: many(courier_message_dispatches), + identityLoginCodes: many(identity_login_codes), + identityRegistrationCodes: many(identity_registration_codes), })); -export const selfserviceLoginFlowsRelations = relations(selfserviceLoginFlows, ({ one, many }) => ({ +export const selfserviceLoginFlowsRelations = relations(selfservice_login_flows, ({ one, many }) => ({ network: one(networks, { - fields: [selfserviceLoginFlows.nid], + fields: [selfservice_login_flows.nid], references: [networks.id], }), - identityLoginCodes: many(identityLoginCodes), + identityLoginCodes: many(identity_login_codes), })); -export const selfserviceRegistrationFlowsRelations = relations(selfserviceRegistrationFlows, ({ one, many }) => ({ +export const selfserviceRegistrationFlowsRelations = relations(selfservice_registration_flows, ({ one, many }) => ({ network: one(networks, { - fields: [selfserviceRegistrationFlows.nid], + fields: [selfservice_registration_flows.nid], references: [networks.id], }), - identityRegistrationCodes: many(identityRegistrationCodes), + identityRegistrationCodes: many(identity_registration_codes), })); -export const identityCredentialIdentifiersRelations = relations(identityCredentialIdentifiers, ({ one }) => ({ - identityCredential: one(identityCredentials, { - fields: [identityCredentialIdentifiers.identityCredentialId], - references: [identityCredentials.id], +export const identityCredentialIdentifiersRelations = relations(identity_credential_identifiers, ({ one }) => ({ + identityCredential: one(identity_credentials, { + fields: [identity_credential_identifiers.identity_credential_id], + references: [identity_credentials.id], }), network: one(networks, { - fields: [identityCredentialIdentifiers.nid], + fields: [identity_credential_identifiers.nid], references: [networks.id], }), - identityCredentialType: one(identityCredentialTypes, { - fields: [identityCredentialIdentifiers.identityCredentialTypeId], - references: [identityCredentialTypes.id], + identityCredentialType: one(identity_credential_types, { + fields: [identity_credential_identifiers.identity_credential_type_id], + references: [identity_credential_types.id], }), })); -export const identityVerifiableAddressesRelations = relations(identityVerifiableAddresses, ({ one, many }) => ({ +export const identityVerifiableAddressesRelations = relations(identity_verifiable_addresses, ({ one, many }) => ({ identity: one(identities, { - fields: [identityVerifiableAddresses.identityId], + fields: [identity_verifiable_addresses.identity_id], references: [identities.id], }), network: one(networks, { - fields: [identityVerifiableAddresses.nid], + fields: [identity_verifiable_addresses.nid], references: [networks.id], }), - identityVerificationTokens: many(identityVerificationTokens), - identityVerificationCodes: many(identityVerificationCodes), + identityVerificationTokens: many(identity_verification_tokens), + identityVerificationCodes: many(identity_verification_codes), })); -export const courierMessagesRelations = relations(courierMessages, ({ one, many }) => ({ +export const courierMessagesRelations = relations(courier_messages, ({ one, many }) => ({ network: one(networks, { - fields: [courierMessages.nid], + fields: [courier_messages.nid], references: [networks.id], }), - courierMessageDispatches: many(courierMessageDispatches), + courierMessageDispatches: many(courier_message_dispatches), })); -export const selfserviceErrorsRelations = relations(selfserviceErrors, ({ one }) => ({ +export const selfserviceErrorsRelations = relations(selfservice_errors, ({ one }) => ({ network: one(networks, { - fields: [selfserviceErrors.nid], + fields: [selfservice_errors.nid], references: [networks.id], }), })); -export const selfserviceVerificationFlowsRelations = relations(selfserviceVerificationFlows, ({ one, many }) => ({ +export const selfserviceVerificationFlowsRelations = relations(selfservice_verification_flows, ({ one, many }) => ({ network: one(networks, { - fields: [selfserviceVerificationFlows.nid], + fields: [selfservice_verification_flows.nid], references: [networks.id], }), - identityVerificationTokens: many(identityVerificationTokens), - identityVerificationCodes: many(identityVerificationCodes), + identityVerificationTokens: many(identity_verification_tokens), + identityVerificationCodes: many(identity_verification_codes), })); -export const selfserviceSettingsFlowsRelations = relations(selfserviceSettingsFlows, ({ one }) => ({ +export const selfserviceSettingsFlowsRelations = relations(selfservice_settings_flows, ({ one }) => ({ identity: one(identities, { - fields: [selfserviceSettingsFlows.identityId], + fields: [selfservice_settings_flows.identity_id], references: [identities.id], }), network: one(networks, { - fields: [selfserviceSettingsFlows.nid], + fields: [selfservice_settings_flows.nid], references: [networks.id], }), })); -export const continuityContainersRelations = relations(continuityContainers, ({ one }) => ({ +export const continuityContainersRelations = relations(continuity_containers, ({ one }) => ({ identity: one(identities, { - fields: [continuityContainers.identityId], + fields: [continuity_containers.identity_id], references: [identities.id], }), network: one(networks, { - fields: [continuityContainers.nid], + fields: [continuity_containers.nid], references: [networks.id], }), })); export const sessionsRelations = relations(sessions, ({ one, many }) => ({ identity: one(identities, { - fields: [sessions.identityId], + fields: [sessions.identity_id], references: [identities.id], }), network: one(networks, { fields: [sessions.nid], references: [networks.id], }), - sessionDevices: many(sessionDevices), + sessionDevices: many(session_devices), })); -export const identityRecoveryAddressesRelations = relations(identityRecoveryAddresses, ({ one, many }) => ({ +export const identityRecoveryAddressesRelations = relations(identity_recovery_addresses, ({ one, many }) => ({ identity: one(identities, { - fields: [identityRecoveryAddresses.identityId], + fields: [identity_recovery_addresses.identity_id], references: [identities.id], }), network: one(networks, { - fields: [identityRecoveryAddresses.nid], + fields: [identity_recovery_addresses.nid], references: [networks.id], }), - identityRecoveryTokens: many(identityRecoveryTokens), - identityRecoveryCodes: many(identityRecoveryCodes), + identityRecoveryTokens: many(identity_recovery_tokens), + identityRecoveryCodes: many(identity_recovery_codes), })); -export const identityVerificationTokensRelations = relations(identityVerificationTokens, ({ one }) => ({ - identityVerifiableAddress: one(identityVerifiableAddresses, { - fields: [identityVerificationTokens.identityVerifiableAddressId], - references: [identityVerifiableAddresses.id], +export const identityVerificationTokensRelations = relations(identity_verification_tokens, ({ one }) => ({ + identityVerifiableAddress: one(identity_verifiable_addresses, { + fields: [identity_verification_tokens.identity_verifiable_address_id], + references: [identity_verifiable_addresses.id], }), - selfserviceVerificationFlow: one(selfserviceVerificationFlows, { - fields: [identityVerificationTokens.selfserviceVerificationFlowId], - references: [selfserviceVerificationFlows.id], + selfserviceVerificationFlow: one(selfservice_verification_flows, { + fields: [identity_verification_tokens.selfservice_verification_flow_id], + references: [selfservice_verification_flows.id], }), network: one(networks, { - fields: [identityVerificationTokens.nid], + fields: [identity_verification_tokens.nid], references: [networks.id], }), })); -export const selfserviceRecoveryFlowsRelations = relations(selfserviceRecoveryFlows, ({ one, many }) => ({ +export const selfserviceRecoveryFlowsRelations = relations(selfservice_recovery_flows, ({ one, many }) => ({ identity: one(identities, { - fields: [selfserviceRecoveryFlows.recoveredIdentityId], + fields: [selfservice_recovery_flows.recovered_identity_id], references: [identities.id], }), network: one(networks, { - fields: [selfserviceRecoveryFlows.nid], + fields: [selfservice_recovery_flows.nid], references: [networks.id], }), - identityRecoveryTokens: many(identityRecoveryTokens), - identityRecoveryCodes: many(identityRecoveryCodes), + identityRecoveryTokens: many(identity_recovery_tokens), + identityRecoveryCodes: many(identity_recovery_codes), })); -export const identityRecoveryTokensRelations = relations(identityRecoveryTokens, ({ one }) => ({ - selfserviceRecoveryFlow: one(selfserviceRecoveryFlows, { - fields: [identityRecoveryTokens.selfserviceRecoveryFlowId], - references: [selfserviceRecoveryFlows.id], +export const identityRecoveryTokensRelations = relations(identity_recovery_tokens, ({ one }) => ({ + selfserviceRecoveryFlow: one(selfservice_recovery_flows, { + fields: [identity_recovery_tokens.selfservice_recovery_flow_id], + references: [selfservice_recovery_flows.id], }), network: one(networks, { - fields: [identityRecoveryTokens.nid], + fields: [identity_recovery_tokens.nid], references: [networks.id], }), - identityRecoveryAddress: one(identityRecoveryAddresses, { - fields: [identityRecoveryTokens.identityRecoveryAddressId], - references: [identityRecoveryAddresses.id], + identityRecoveryAddress: one(identity_recovery_addresses, { + fields: [identity_recovery_tokens.identity_recovery_address_id], + references: [identity_recovery_addresses.id], }), identity: one(identities, { - fields: [identityRecoveryTokens.identityId], + fields: [identity_recovery_tokens.identity_id], references: [identities.id], }), })); -export const identityRecoveryCodesRelations = relations(identityRecoveryCodes, ({ one }) => ({ - identityRecoveryAddress: one(identityRecoveryAddresses, { - fields: [identityRecoveryCodes.identityRecoveryAddressId], - references: [identityRecoveryAddresses.id], +export const identityRecoveryCodesRelations = relations(identity_recovery_codes, ({ one }) => ({ + identityRecoveryAddress: one(identity_recovery_addresses, { + fields: [identity_recovery_codes.identity_recovery_address_id], + references: [identity_recovery_addresses.id], }), - selfserviceRecoveryFlow: one(selfserviceRecoveryFlows, { - fields: [identityRecoveryCodes.selfserviceRecoveryFlowId], - references: [selfserviceRecoveryFlows.id], + selfserviceRecoveryFlow: one(selfservice_recovery_flows, { + fields: [identity_recovery_codes.selfservice_recovery_flow_id], + references: [selfservice_recovery_flows.id], }), identity: one(identities, { - fields: [identityRecoveryCodes.identityId], + fields: [identity_recovery_codes.identity_id], references: [identities.id], }), network: one(networks, { - fields: [identityRecoveryCodes.nid], + fields: [identity_recovery_codes.nid], references: [networks.id], }), })); -export const sessionDevicesRelations = relations(sessionDevices, ({ one }) => ({ +export const sessionDevicesRelations = relations(session_devices, ({ one }) => ({ session: one(sessions, { - fields: [sessionDevices.sessionId], + fields: [session_devices.session_id], references: [sessions.id], }), network: one(networks, { - fields: [sessionDevices.nid], + fields: [session_devices.nid], references: [networks.id], }), })); -export const identityVerificationCodesRelations = relations(identityVerificationCodes, ({ one }) => ({ - identityVerifiableAddress: one(identityVerifiableAddresses, { - fields: [identityVerificationCodes.identityVerifiableAddressId], - references: [identityVerifiableAddresses.id], +export const identityVerificationCodesRelations = relations(identity_verification_codes, ({ one }) => ({ + identityVerifiableAddress: one(identity_verifiable_addresses, { + fields: [identity_verification_codes.identity_verifiable_address_id], + references: [identity_verifiable_addresses.id], }), - selfserviceVerificationFlow: one(selfserviceVerificationFlows, { - fields: [identityVerificationCodes.selfserviceVerificationFlowId], - references: [selfserviceVerificationFlows.id], + selfserviceVerificationFlow: one(selfservice_verification_flows, { + fields: [identity_verification_codes.selfservice_verification_flow_id], + references: [selfservice_verification_flows.id], }), network: one(networks, { - fields: [identityVerificationCodes.nid], + fields: [identity_verification_codes.nid], references: [networks.id], }), })); -export const courierMessageDispatchesRelations = relations(courierMessageDispatches, ({ one }) => ({ - courierMessage: one(courierMessages, { - fields: [courierMessageDispatches.messageId], - references: [courierMessages.id], +export const courierMessageDispatchesRelations = relations(courier_message_dispatches, ({ one }) => ({ + courierMessage: one(courier_messages, { + fields: [courier_message_dispatches.message_id], + references: [courier_messages.id], }), network: one(networks, { - fields: [courierMessageDispatches.nid], + fields: [courier_message_dispatches.nid], references: [networks.id], }), })); -export const identityLoginCodesRelations = relations(identityLoginCodes, ({ one }) => ({ - selfserviceLoginFlow: one(selfserviceLoginFlows, { - fields: [identityLoginCodes.selfserviceLoginFlowId], - references: [selfserviceLoginFlows.id], +export const identityLoginCodesRelations = relations(identity_login_codes, ({ one }) => ({ + selfserviceLoginFlow: one(selfservice_login_flows, { + fields: [identity_login_codes.selfservice_login_flow_id], + references: [selfservice_login_flows.id], }), network: one(networks, { - fields: [identityLoginCodes.nid], + fields: [identity_login_codes.nid], references: [networks.id], }), identity: one(identities, { - fields: [identityLoginCodes.identityId], + fields: [identity_login_codes.identity_id], references: [identities.id], }), })); -export const identityRegistrationCodesRelations = relations(identityRegistrationCodes, ({ one }) => ({ - selfserviceRegistrationFlow: one(selfserviceRegistrationFlows, { - fields: [identityRegistrationCodes.selfserviceRegistrationFlowId], - references: [selfserviceRegistrationFlows.id], +export const identityRegistrationCodesRelations = relations(identity_registration_codes, ({ one }) => ({ + selfserviceRegistrationFlow: one(selfservice_registration_flows, { + fields: [identity_registration_codes.selfservice_registration_flow_id], + references: [selfservice_registration_flows.id], }), network: one(networks, { - fields: [identityRegistrationCodes.nid], + fields: [identity_registration_codes.nid], references: [networks.id], }), })); \ No newline at end of file From 007098ca91ffbdd1b115d48b4a01d991ed2f67dc Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 4 Apr 2025 16:00:00 +0200 Subject: [PATCH 02/21] NORY-59: add new script to create Keto relationships --- docker/ory-dev/keto-add-permission-to-role.sh | 31 +++++++++++++++++++ docker/ory-dev/keto-add-permission.sh | 26 ++++++++++++++++ docker/ory-dev/ory/keto/keto.yaml | 2 ++ 3 files changed, 59 insertions(+) create mode 100644 docker/ory-dev/keto-add-permission-to-role.sh create mode 100644 docker/ory-dev/keto-add-permission.sh diff --git a/docker/ory-dev/keto-add-permission-to-role.sh b/docker/ory-dev/keto-add-permission-to-role.sh new file mode 100644 index 0000000..9a0f7fa --- /dev/null +++ b/docker/ory-dev/keto-add-permission-to-role.sh @@ -0,0 +1,31 @@ +# this script gives the referenced identity the admin role +# make sure to provide the id of the identity + +# check if a identity id argument was provided +if [ $# -ne 4 ]; then + echo "Usage: $0 " + exit 1 +fi + +# set user id variable +OBJECT=$1 +RELATION=$2 +ROLE=$3 +ROLE_RELATION=$4 + +# execute curl to Ory Keto write endpoint +curl --request PUT \ + --url http://localhost:4467/admin/relation-tuples \ + --data '{ + "namespace": "permissions", + "object": "'"$OBJECT"'", + "relation": "'"$RELATION"'", + "subject_set": { + "namespace": "roles", + "object": "'"$ROLE"'", + "relation": "'"$ROLE_RELATION"'" + } + }' + +# write success response to terminal +echo "Added relation Permissions:$OBJECT#$RELATION@(Roles:$ROLE#$RELATION)" diff --git a/docker/ory-dev/keto-add-permission.sh b/docker/ory-dev/keto-add-permission.sh new file mode 100644 index 0000000..5812ce3 --- /dev/null +++ b/docker/ory-dev/keto-add-permission.sh @@ -0,0 +1,26 @@ +# this script gives the referenced identity the provided permission +# make sure to provide the id of the identity + +# check if a required arguments were provided +if [ $# -ne 3 ]; then + echo "Usage: $0 " + exit 1 +fi + +# set variables from input +OBJECT=$1 +RELATION=$2 +IDENTITY_ID=$3 + +# execute curl to Ory Keto write endpoint +curl --request PUT \ + --url http://localhost:4467/admin/relation-tuples \ + --data '{ + "namespace": "permissions", + "object": "'"$OBJECT"'", + "relation": "'"$RELATION"'", + "subject_id": "'"$IDENTITY_ID"'" + }' + +# write success response to terminal +echo "Added permission $OBJECT#$RELATION@$IDENTITY_ID" diff --git a/docker/ory-dev/ory/keto/keto.yaml b/docker/ory-dev/ory/keto/keto.yaml index 21dad3d..2be94ad 100644 --- a/docker/ory-dev/ory/keto/keto.yaml +++ b/docker/ory-dev/ory/keto/keto.yaml @@ -22,6 +22,8 @@ dsn: postgres://postgres:postgres@ory-postgres:5432/keto?sslmode=disable&max_con namespaces: - id: 0 name: roles + - id: 1 + name: permissions serve: read: From 7d7782a92cca885d2f70fe506a42803d33ec7565 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 4 Apr 2025 16:20:32 +0200 Subject: [PATCH 03/21] NORY-59: add authentication and authorisation actions --- dashboard/src/lib/action/authentication.ts | 106 +++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 dashboard/src/lib/action/authentication.ts diff --git a/dashboard/src/lib/action/authentication.ts b/dashboard/src/lib/action/authentication.ts new file mode 100644 index 0000000..d1d70e3 --- /dev/null +++ b/dashboard/src/lib/action/authentication.ts @@ -0,0 +1,106 @@ +'use server'; + +import { getFrontendApi, getPermissionApi } from '@/ory/sdk/server'; +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; + +export async function getSession() { + + const cookie = await cookies(); + + const frontendApi = await getFrontendApi(); + + return frontendApi + .toSession({ cookie: 'ory_kratos_session=' + cookie.get('ory_kratos_session')?.value }) + .then((response) => response.data) + .catch(() => null); +} + +export async function requireSession() { + + const session = await getSession(); + + if (!session) { + + // TODO: set return_to dynamically + const url = process.env.NEXT_PUBLIC_AUTHENTICATION_NODE_URL + + '/flow/login?return_to=' + process.env.NEXT_PUBLIC_DASHBOARD_NODE_URL; + + console.log('Intercepted request with missing session'); + console.log('Redirecting client to: ', url); + + redirect(url); + } + + return session; +} + + +export async function checkRole( + object: string, + subjectId: string, +) { + + const permissionApi = await getPermissionApi(); + return permissionApi.checkPermission({ + namespace: 'roles', + object, + relation: 'member', + subjectId, + }) + .then(({ data: { allowed } }) => allowed) + .catch(_ => false); +} + +export async function requireRole( + object: string, + subjectId: string, +) { + + const hasRole = await checkRole(object, subjectId); + + if (!hasRole) { + console.log(`Intercepted request with missing role ${object} for identity ${subjectId}`); + redirect('/unauthorised'); + } + + return hasRole; +} + + +export async function checkPermission( + object: string, + relation: string, + subjectId: string, +) { + + const permissionApi = await getPermissionApi(); + return permissionApi.checkPermission({ + namespace: 'permissions', + object, + relation, + subjectId, + }) + .then(({ data: { allowed } }) => allowed) + .catch(_ => false); +} + +export async function requirePermission( + object: string, + relation: string, + subjectId: string, +) { + + const allowed = await checkPermission( + object, + relation, + subjectId, + ); + + if (!allowed) { + console.log(`Intercepted request with insufficient permission (${object}#${relation}@${subjectId})`); + redirect('/unauthorised'); + } + + return allowed; +} From 48c14c08a244e78765644040de023001cba02aa0 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 4 Apr 2025 16:22:00 +0200 Subject: [PATCH 04/21] NORY-59: add component to display insufficient permissions --- .../components/insufficient-permission.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 dashboard/src/components/insufficient-permission.tsx diff --git a/dashboard/src/components/insufficient-permission.tsx b/dashboard/src/components/insufficient-permission.tsx new file mode 100644 index 0000000..261004a --- /dev/null +++ b/dashboard/src/components/insufficient-permission.tsx @@ -0,0 +1,36 @@ +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; + +interface InsufficientPermissionProps { + permission: string; + relation: string; + identityId: string; + classNames?: string; +} + +export default async function InsufficientPermission( + { + permission, + relation, + identityId, + classNames, + }: InsufficientPermissionProps, +) { + return ( + + + Insufficient Permission + + You are missing the permission to see this content.
+ If you think this is an error, please contact your system administrator +
+
+ + + Permission: {permission}
+ Relation: {relation}
+ Identity: {identityId} +
+
+
+ ); +} From cbc6b0517397abe71188af105928e6d8d7f77024 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 4 Apr 2025 16:22:38 +0200 Subject: [PATCH 05/21] NORY-59: move stack status requests to protected server actions --- dashboard/src/app/(inside)/page.tsx | 101 ++++++++++++----------- dashboard/src/components/status-card.tsx | 2 +- dashboard/src/lib/action/metadata.ts | 83 +++++++++++++++++++ 3 files changed, 135 insertions(+), 51 deletions(-) create mode 100644 dashboard/src/lib/action/metadata.ts diff --git a/dashboard/src/app/(inside)/page.tsx b/dashboard/src/app/(inside)/page.tsx index 604a7e2..778d24a 100644 --- a/dashboard/src/app/(inside)/page.tsx +++ b/dashboard/src/app/(inside)/page.tsx @@ -1,40 +1,20 @@ -import { getHydraMetadataApi, getKetoMetadataApi, getKratosMetadataApi } from '@/ory/sdk/server'; -import { MetadataApiReady, StatusCard } from '@/components/status-card'; +import { StatusCard } from '@/components/status-card'; +import { hydraMetadata, ketoMetadata, kratosMetadata } from '@/lib/action/metadata'; +import { checkPermission, requireRole, requireSession } from '@/lib/action/authentication'; +import InsufficientPermission from '@/components/insufficient-permission'; export default async function RootPage() { - const kratosMetadataApi = await getKratosMetadataApi(); - const kratosVersion = await kratosMetadataApi.getVersion() - .then(res => res.data.version) - .catch(() => undefined); - const kratosStatus = await fetch(process.env.ORY_KRATOS_ADMIN_URL + '/health/ready') - .then((response) => response.json() as MetadataApiReady) - .catch(() => { - return { errors: ['No instance running'] } as MetadataApiReady; - }); + const session = await requireSession(); + const identityId = session.identity!.id; + await requireRole('admin', identityId); - const hydraMetadataApi = await getHydraMetadataApi(); - const hydraVersion = await hydraMetadataApi.getVersion() - .then(res => res.data.version) - .catch(() => undefined); - const hydraStatus = await fetch(process.env.ORY_HYDRA_ADMIN_URL + '/health/ready') - .then((response) => response.json() as MetadataApiReady) - .catch(() => { - return { errors: ['No instance running'] } as MetadataApiReady; - }); - - - const ketoMetadataApi = await getKetoMetadataApi(); - const ketoVersion = await ketoMetadataApi.getVersion() - .then(res => res.data.version) - .catch(() => undefined); - const ketoStatus = await fetch(process.env.ORY_KETO_ADMIN_URL + '/health/ready') - .then((response) => response.json() as MetadataApiReady) - .catch(() => { - return { errors: ['No instance running'] } as MetadataApiReady; - }); + const pmAccessStackStatus = await checkPermission('admin.stack.status', 'access', identityId); + const kratos = pmAccessStackStatus && await kratosMetadata(); + const hydra = pmAccessStackStatus && await hydraMetadata(); + const keto = pmAccessStackStatus && await ketoMetadata(); return (
@@ -43,25 +23,46 @@ export default async function RootPage() {

See the list of all applications in your stack

- - - -
+ { + !pmAccessStackStatus && ( + + ) + } + { + kratos && ( + + ) + } + { + hydra && ( + + ) + } + { + keto && ( + + ) + }
); diff --git a/dashboard/src/components/status-card.tsx b/dashboard/src/components/status-card.tsx index d6d4038..e12bf53 100644 --- a/dashboard/src/components/status-card.tsx +++ b/dashboard/src/components/status-card.tsx @@ -50,7 +50,7 @@ export function StatusCard({ title, version, name, status, className }: StatusCa { - status.errors.map((error) => {error}) + status.errors.map((error) => {error}) } diff --git a/dashboard/src/lib/action/metadata.ts b/dashboard/src/lib/action/metadata.ts new file mode 100644 index 0000000..03d5768 --- /dev/null +++ b/dashboard/src/lib/action/metadata.ts @@ -0,0 +1,83 @@ +'use server'; + +import { getHydraMetadataApi, getKetoMetadataApi, getKratosMetadataApi } from '@/ory/sdk/server'; +import { MetadataApiReady } from '@/components/status-card'; +import { checkPermission, requireSession } from '@/lib/action/authentication'; + +export async function kratosMetadata() { + + const session = await requireSession(); + const allowed = await checkPermission('admin.stack.status', 'access', session.identity!.id); + if (!allowed) { + return; + } + + const api = await getKratosMetadataApi(); + + const version = await api.getVersion() + .then(res => res.data.version) + .catch(() => undefined); + + const status = await fetch(process.env.ORY_KRATOS_ADMIN_URL + '/health/ready') + .then((response) => response.json() as MetadataApiReady) + .catch(() => { + return { errors: ['No instance running'] } as MetadataApiReady; + }); + + return { + version, + status, + }; +} + +export async function hydraMetadata() { + + const session = await requireSession(); + const allowed = await checkPermission('admin.stack.status', 'access', session.identity!.id); + if (!allowed) { + return; + } + + const api = await getHydraMetadataApi(); + + const version = await api.getVersion() + .then(res => res.data.version) + .catch(() => undefined); + + const status = await fetch(process.env.ORY_HYDRA_ADMIN_URL + '/health/ready') + .then((response) => response.json() as MetadataApiReady) + .catch(() => { + return { errors: ['No instance running'] } as MetadataApiReady; + }); + + return { + version, + status, + }; +} + +export async function ketoMetadata() { + + const session = await requireSession(); + const allowed = await checkPermission('admin.stack.status', 'access', session.identity!.id); + if (!allowed) { + return; + } + + const api = await getKetoMetadataApi(); + + const version = await api.getVersion() + .then(res => res.data.version) + .catch(() => undefined); + + const status = await fetch(process.env.ORY_KETO_ADMIN_URL + '/health/ready') + .then((response) => response.json() as MetadataApiReady) + .catch(() => { + return { errors: ['No instance running'] } as MetadataApiReady; + }); + + return { + version, + status, + }; +} From 4f06445869da31dbf5b01a1fb18d55ff6e6f6ebd Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 4 Apr 2025 16:31:15 +0200 Subject: [PATCH 06/21] NORY-59: refactor middleware to use new authentication functions --- dashboard/src/middleware.ts | 45 ++++++++++++------------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/dashboard/src/middleware.ts b/dashboard/src/middleware.ts index 52d7714..62d78cc 100644 --- a/dashboard/src/middleware.ts +++ b/dashboard/src/middleware.ts @@ -1,47 +1,30 @@ import { NextRequest, NextResponse } from 'next/server'; -import { cookies } from 'next/headers'; -import { getFrontendApi, getPermissionApi } from '@/ory/sdk/server'; +import { checkRole, getSession } from '@/lib/action/authentication'; export async function middleware(request: NextRequest) { - const frontendApi = await getFrontendApi(); - const cookie = await cookies(); - - const session = await frontendApi - .toSession({ cookie: 'ory_kratos_session=' + cookie.get('ory_kratos_session')?.value }) - .then((response) => response.data) - .catch(() => null); + // middleware can not work with requireSession, requireRole and + // requirePermission due to the different redirect mechanisms in use! + const session = await getSession(); if (!session) { - console.log('NO SESSION'); + console.log('middleware', 'MISSING SESSION'); const url = process.env.NEXT_PUBLIC_AUTHENTICATION_NODE_URL + '/flow/login?return_to=' + process.env.NEXT_PUBLIC_DASHBOARD_NODE_URL; - console.log('REDIRECT TO', url); - - return NextResponse.redirect(url); + console.log('middleware', 'REDIRECT TO', url); + return NextResponse.redirect(url!); } - const permissionApi = await getPermissionApi(); - const isAdmin = await permissionApi.checkPermission({ - namespace: 'roles', - object: 'admin', - relation: 'member', - subjectId: session!.identity!.id, - }) - .then(({ data: { allowed } }) => { - console.log('is_admin', session!.identity!.id, allowed); - return allowed; - }) - .catch((response) => { - console.log('is_admin', session!.identity!.id, response, 'check failed'); - return false; - }); + const allowed = await checkRole( + 'admin', + session!.identity!.id, + ); - if (isAdmin) { + if (allowed) { if (request.nextUrl.pathname === '/unauthorised') { return redirect('/', 'HAS PERMISSION BUT ACCESSING /unauthorized'); } @@ -55,9 +38,9 @@ export async function middleware(request: NextRequest) { } function redirect(path: string, reason: string) { - console.log(reason); + console.log('middleware', reason); const url = `${process.env.NEXT_PUBLIC_DASHBOARD_NODE_URL}${path}`; - console.log('REDIRECT TO', url); + console.log('middleware', 'REDIRECT TO', url); return NextResponse.redirect(url!); } From 3693b0b1f9eaf882056af46fc5b03de8805319ff Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 4 Apr 2025 19:10:40 +0200 Subject: [PATCH 07/21] NORY-59: add authentication and authorisation to user page --- .../src/app/(inside)/user/data-table.tsx | 13 +++- dashboard/src/app/(inside)/user/page.tsx | 73 ++++++++++++++----- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/dashboard/src/app/(inside)/user/data-table.tsx b/dashboard/src/app/(inside)/user/data-table.tsx index 510ee6c..2baabaa 100644 --- a/dashboard/src/app/(inside)/user/data-table.tsx +++ b/dashboard/src/app/(inside)/user/data-table.tsx @@ -33,9 +33,16 @@ interface IdentityDataTableProps { data: Identity[]; page: number; query: string; + permission: { + pmEditUser: boolean; + pmBlockUser: boolean; + pmUnblockUser: boolean; + pmDeleteUser: boolean; + pmDeleteUserSession: boolean; + }; } -export function IdentityDataTable({ data, page, query }: IdentityDataTableProps) { +export function IdentityDataTable({ data, page, query, permission }: IdentityDataTableProps) { const columns: ColumnDef[] = [ { @@ -137,6 +144,7 @@ export function IdentityDataTable({ data, page, query }: IdentityDataTableProps) setCurrentIdentity(identity); setIdentitySessionVisible(true); }} + disabled={!permission.pmDeleteUserSession} className="flex items-center space-x-2 text-red-500"> Delete sessions @@ -148,6 +156,7 @@ export function IdentityDataTable({ data, page, query }: IdentityDataTableProps) setCurrentIdentity(identity); setBlockIdentityVisible(true); }} + disabled={!permission.pmBlockUser} className="flex items-center space-x-2 text-red-500"> Block identity @@ -160,6 +169,7 @@ export function IdentityDataTable({ data, page, query }: IdentityDataTableProps) setCurrentIdentity(identity); setUnblockIdentityVisible(true); }} + disabled={!permission.pmUnblockUser} className="flex items-center space-x-2 text-red-500"> Unblock identity @@ -170,6 +180,7 @@ export function IdentityDataTable({ data, page, query }: IdentityDataTableProps) setCurrentIdentity(identity); setDeleteIdentityVisible(true); }} + disabled={!permission.pmDeleteUser} className="flex items-center space-x-2 text-red-500"> Delete identity diff --git a/dashboard/src/app/(inside)/user/page.tsx b/dashboard/src/app/(inside)/user/page.tsx index efb1095..b889ecf 100644 --- a/dashboard/src/app/(inside)/user/page.tsx +++ b/dashboard/src/app/(inside)/user/page.tsx @@ -3,6 +3,8 @@ import { IdentityDataTable } from '@/app/(inside)/user/data-table'; import { SearchInput } from '@/components/search-input'; import { queryIdentities } from '@/lib/action/identity'; import { IdentityPagination } from '@/components/pagination'; +import { checkPermission, requireRole, requireSession } from '@/lib/action/authentication'; +import InsufficientPermission from '@/components/insufficient-permission'; export default async function UserPage( { @@ -12,6 +14,18 @@ export default async function UserPage( }, ) { + const session = await requireSession(); + const identityId = session.identity!.id; + + await requireRole('admin', identityId); + + const pmAccessUser = await checkPermission('admin.user', 'access', identityId); + const pmEditUser = await checkPermission('admin.user', 'edit', identityId); + const pmBlockUser = await checkPermission('admin.user', 'block', identityId); + const pmUnblockUser = await checkPermission('admin.user', 'unblock', identityId); + const pmDeleteUser = await checkPermission('admin.user', 'delete', identityId); + const pmDeleteUserSession = await checkPermission('admin.user.session', 'delete', identityId); + const params = await searchParams; const page = params.page ? Number(params.page) : 1; @@ -20,7 +34,7 @@ export default async function UserPage( let pageSize = 50; let paginationRange = 11; - const { data, itemCount, pageCount } = await queryIdentities({ page, pageSize, query }); + const users = pmAccessUser && await queryIdentities({ page, pageSize, query }); return (
@@ -31,23 +45,46 @@ export default async function UserPage(

- -
-

{itemCount} item{itemCount && itemCount > 1 ? 's' : ''} found

- -
- + { + !pmAccessUser && ( + + ) + } + { + pmAccessUser && users && ( + <> + +
+

{users.itemCount} item{users.itemCount && users.itemCount > 1 ? 's' : ''} found

+ +
+ + + ) + }
); From 86412e01336511c735988e73f1aceb44159da29d Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 4 Apr 2025 19:32:26 +0200 Subject: [PATCH 08/21] NORY-59: introduce permission constants --- dashboard/src/app/(inside)/page.tsx | 4 ++-- dashboard/src/app/(inside)/user/data-table.tsx | 7 +++---- dashboard/src/app/(inside)/user/page.tsx | 18 ++++++++---------- dashboard/src/lib/permission.ts | 16 ++++++++++++++++ 4 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 dashboard/src/lib/permission.ts diff --git a/dashboard/src/app/(inside)/page.tsx b/dashboard/src/app/(inside)/page.tsx index 778d24a..dad9358 100644 --- a/dashboard/src/app/(inside)/page.tsx +++ b/dashboard/src/app/(inside)/page.tsx @@ -10,7 +10,7 @@ export default async function RootPage() { await requireRole('admin', identityId); - const pmAccessStackStatus = await checkPermission('admin.stack.status', 'access', identityId); + const pmAccessStackStatus = await checkPermission(permission.stack.status, relation.access, identityId); const kratos = pmAccessStackStatus && await kratosMetadata(); const hydra = pmAccessStackStatus && await hydraMetadata(); @@ -26,7 +26,7 @@ export default async function RootPage() { { !pmAccessStackStatus && ( Block identity @@ -169,7 +168,7 @@ export function IdentityDataTable({ data, page, query, permission }: IdentityDat setCurrentIdentity(identity); setUnblockIdentityVisible(true); }} - disabled={!permission.pmUnblockUser} + disabled={!permission.pmEditUserState} className="flex items-center space-x-2 text-red-500"> Unblock identity diff --git a/dashboard/src/app/(inside)/user/page.tsx b/dashboard/src/app/(inside)/user/page.tsx index b889ecf..3e52891 100644 --- a/dashboard/src/app/(inside)/user/page.tsx +++ b/dashboard/src/app/(inside)/user/page.tsx @@ -19,12 +19,11 @@ export default async function UserPage( await requireRole('admin', identityId); - const pmAccessUser = await checkPermission('admin.user', 'access', identityId); - const pmEditUser = await checkPermission('admin.user', 'edit', identityId); - const pmBlockUser = await checkPermission('admin.user', 'block', identityId); - const pmUnblockUser = await checkPermission('admin.user', 'unblock', identityId); - const pmDeleteUser = await checkPermission('admin.user', 'delete', identityId); - const pmDeleteUserSession = await checkPermission('admin.user.session', 'delete', identityId); + const pmAccessUser = await checkPermission(permission.user.it, relation.access, identityId); + const pmEditUser = await checkPermission(permission.user.it, relation.edit, identityId); + const pmDeleteUser = await checkPermission(permission.user.it, relation.delete, identityId); + const pmEditUserState = await checkPermission(permission.user.state, relation.edit, identityId); + const pmDeleteUserSession = await checkPermission(permission.user.session, relation.delete, identityId); const params = await searchParams; @@ -48,8 +47,8 @@ export default async function UserPage( { !pmAccessUser && ( ) @@ -70,9 +69,8 @@ export default async function UserPage( query={query} permission={{ pmEditUser: pmEditUser, - pmBlockUser: pmBlockUser, - pmUnblockUser: pmUnblockUser, pmDeleteUser: pmDeleteUser, + pmEditUserState: pmEditUserState, pmDeleteUserSession: pmDeleteUserSession, }} /> diff --git a/dashboard/src/lib/permission.ts b/dashboard/src/lib/permission.ts new file mode 100644 index 0000000..7bced61 --- /dev/null +++ b/dashboard/src/lib/permission.ts @@ -0,0 +1,16 @@ +const permission = { + stack: { + status: 'admin.stack.status', + }, + user: { + it: 'admin.user', + session: 'admin.user.session', + state: 'admin.user.state', + }, +}; + +const relation = { + access: 'access', + edit: 'edit', + delete: 'delete', +}; From a72ca49271f0e9a474b87f05455fd2318ef2e67f Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 4 Apr 2025 19:48:39 +0200 Subject: [PATCH 09/21] NORY-59: replace 'force-admin-role' with new permission --- dashboard/src/app/(inside)/page.tsx | 5 +++-- dashboard/src/app/(inside)/user/page.tsx | 5 +++-- dashboard/src/lib/permission.ts | 5 +++-- dashboard/src/middleware.ts | 9 ++++----- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/dashboard/src/app/(inside)/page.tsx b/dashboard/src/app/(inside)/page.tsx index dad9358..607c2c4 100644 --- a/dashboard/src/app/(inside)/page.tsx +++ b/dashboard/src/app/(inside)/page.tsx @@ -1,14 +1,15 @@ import { StatusCard } from '@/components/status-card'; import { hydraMetadata, ketoMetadata, kratosMetadata } from '@/lib/action/metadata'; -import { checkPermission, requireRole, requireSession } from '@/lib/action/authentication'; +import { checkPermission, requirePermission, requireSession } from '@/lib/action/authentication'; import InsufficientPermission from '@/components/insufficient-permission'; +import { permission, relation } from '@/lib/permission'; export default async function RootPage() { const session = await requireSession(); const identityId = session.identity!.id; - await requireRole('admin', identityId); + await requirePermission(permission.stack.dashboard, relation.access, identityId); const pmAccessStackStatus = await checkPermission(permission.stack.status, relation.access, identityId); diff --git a/dashboard/src/app/(inside)/user/page.tsx b/dashboard/src/app/(inside)/user/page.tsx index 3e52891..ebb9079 100644 --- a/dashboard/src/app/(inside)/user/page.tsx +++ b/dashboard/src/app/(inside)/user/page.tsx @@ -3,8 +3,9 @@ import { IdentityDataTable } from '@/app/(inside)/user/data-table'; import { SearchInput } from '@/components/search-input'; import { queryIdentities } from '@/lib/action/identity'; import { IdentityPagination } from '@/components/pagination'; -import { checkPermission, requireRole, requireSession } from '@/lib/action/authentication'; +import { checkPermission, requirePermission, requireSession } from '@/lib/action/authentication'; import InsufficientPermission from '@/components/insufficient-permission'; +import { permission, relation } from '@/lib/permission'; export default async function UserPage( { @@ -17,7 +18,7 @@ export default async function UserPage( const session = await requireSession(); const identityId = session.identity!.id; - await requireRole('admin', identityId); + await requirePermission(permission.stack.dashboard, relation.access, identityId); const pmAccessUser = await checkPermission(permission.user.it, relation.access, identityId); const pmEditUser = await checkPermission(permission.user.it, relation.edit, identityId); diff --git a/dashboard/src/lib/permission.ts b/dashboard/src/lib/permission.ts index 7bced61..bab8592 100644 --- a/dashboard/src/lib/permission.ts +++ b/dashboard/src/lib/permission.ts @@ -1,5 +1,6 @@ -const permission = { +export const permission = { stack: { + dashboard: 'admin.stack.dashboard', status: 'admin.stack.status', }, user: { @@ -9,7 +10,7 @@ const permission = { }, }; -const relation = { +export const relation = { access: 'access', edit: 'edit', delete: 'delete', diff --git a/dashboard/src/middleware.ts b/dashboard/src/middleware.ts index 62d78cc..8fb5913 100644 --- a/dashboard/src/middleware.ts +++ b/dashboard/src/middleware.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -import { checkRole, getSession } from '@/lib/action/authentication'; +import { checkPermission, getSession } from '@/lib/action/authentication'; +import { permission, relation } from '@/lib/permission'; export async function middleware(request: NextRequest) { @@ -19,10 +20,8 @@ export async function middleware(request: NextRequest) { return NextResponse.redirect(url!); } - const allowed = await checkRole( - 'admin', - session!.identity!.id, - ); + const allowed = await checkPermission(permission.stack.dashboard, relation.access, session.identity!.id); + if (allowed) { if (request.nextUrl.pathname === '/unauthorised') { From f794f7d7003d751c4eb2ec75e926db114d0c6e40 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 4 Apr 2025 19:58:57 +0200 Subject: [PATCH 10/21] NORY-59: refactor permission-checks to use constant values --- dashboard/src/lib/action/metadata.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dashboard/src/lib/action/metadata.ts b/dashboard/src/lib/action/metadata.ts index 03d5768..5a64b3b 100644 --- a/dashboard/src/lib/action/metadata.ts +++ b/dashboard/src/lib/action/metadata.ts @@ -3,11 +3,12 @@ import { getHydraMetadataApi, getKetoMetadataApi, getKratosMetadataApi } from '@/ory/sdk/server'; import { MetadataApiReady } from '@/components/status-card'; import { checkPermission, requireSession } from '@/lib/action/authentication'; +import { permission, relation } from '@/lib/permission'; export async function kratosMetadata() { const session = await requireSession(); - const allowed = await checkPermission('admin.stack.status', 'access', session.identity!.id); + const allowed = await checkPermission(permission.stack.status, relation.access, session.identity!.id); if (!allowed) { return; } @@ -33,7 +34,7 @@ export async function kratosMetadata() { export async function hydraMetadata() { const session = await requireSession(); - const allowed = await checkPermission('admin.stack.status', 'access', session.identity!.id); + const allowed = await checkPermission(permission.stack.status, relation.access, session.identity!.id); if (!allowed) { return; } @@ -59,7 +60,7 @@ export async function hydraMetadata() { export async function ketoMetadata() { const session = await requireSession(); - const allowed = await checkPermission('admin.stack.status', 'access', session.identity!.id); + const allowed = await checkPermission(permission.stack.status, relation.access, session.identity!.id); if (!allowed) { return; } From 0da4158d60c92febf25a9fb8912b748ba9ca92cb Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Fri, 4 Apr 2025 20:20:45 +0200 Subject: [PATCH 11/21] NORY-59: add protection to identity actions --- dashboard/src/lib/action/identity.ts | 56 ++++++++++++++++++++++++++++ dashboard/src/lib/permission.ts | 4 ++ 2 files changed, 60 insertions(+) diff --git a/dashboard/src/lib/action/identity.ts b/dashboard/src/lib/action/identity.ts index 4ffed77..b39e721 100644 --- a/dashboard/src/lib/action/identity.ts +++ b/dashboard/src/lib/action/identity.ts @@ -12,6 +12,8 @@ import { import { getDB } from '@/db'; import { identities, identity_recovery_addresses, identity_verifiable_addresses } from '@/db/schema'; import { eq, ilike, or, sql } from 'drizzle-orm'; +import { checkPermission, requireSession } from '@/lib/action/authentication'; +import { permission, relation } from '@/lib/permission'; interface QueryIdentitiesProps { page: number, @@ -21,6 +23,12 @@ interface QueryIdentitiesProps { export async function queryIdentities({ page, pageSize, query }: QueryIdentitiesProps) { + const session = await requireSession(); + const allowed = await checkPermission(permission.user.it, relation.access, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + if (page < 1 || pageSize < 1) { return { data: [], @@ -81,6 +89,12 @@ interface UpdatedIdentityProps { export async function updateIdentity({ id, body }: UpdatedIdentityProps) { + const session = await requireSession(); + const allowed = await checkPermission(permission.user.it, relation.edit, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + const identityApi = await getIdentityApi(); const { data } = await identityApi.updateIdentity({ id: id, @@ -101,6 +115,12 @@ interface DeleteIdentityCredentialProps { export async function deleteIdentityCredential({ id, type }: DeleteIdentityCredentialProps) { + const session = await requireSession(); + const allowed = await checkPermission(permission.user.credential, relation.delete, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + const identityApi = await getIdentityApi(); const { data } = await identityApi.deleteIdentityCredentials({ id, type }); @@ -113,6 +133,12 @@ export async function deleteIdentityCredential({ id, type }: DeleteIdentityCrede export async function deleteIdentitySessions(id: string) { + const session = await requireSession(); + const allowed = await checkPermission(permission.user.session, relation.delete, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + const identityApi = await getIdentityApi(); const { data } = await identityApi.deleteIdentitySessions({ id }); @@ -125,6 +151,12 @@ export async function deleteIdentitySessions(id: string) { export async function createRecoveryCode(id: string) { + const session = await requireSession(); + const allowed = await checkPermission(permission.user.code, relation.create, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + const identityApi = await getIdentityApi(); const { data } = await identityApi.createRecoveryCodeForIdentity({ createRecoveryCodeForIdentityBody: { @@ -139,6 +171,12 @@ export async function createRecoveryCode(id: string) { export async function createRecoveryLink(id: string) { + const session = await requireSession(); + const allowed = await checkPermission(permission.user.link, relation.create, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + const identityApi = await getIdentityApi(); const { data } = await identityApi.createRecoveryLinkForIdentity({ createRecoveryLinkForIdentityBody: { @@ -153,6 +191,12 @@ export async function createRecoveryLink(id: string) { export async function blockIdentity(id: string) { + const session = await requireSession(); + const allowed = await checkPermission(permission.user.state, relation.edit, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + const identityApi = await getIdentityApi(); const { data } = await identityApi.patchIdentity({ id, @@ -172,6 +216,12 @@ export async function blockIdentity(id: string) { export async function unblockIdentity(id: string) { + const session = await requireSession(); + const allowed = await checkPermission(permission.user.state, relation.edit, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + const identityApi = await getIdentityApi(); const { data } = await identityApi.patchIdentity({ id, @@ -191,6 +241,12 @@ export async function unblockIdentity(id: string) { export async function deleteIdentity(id: string) { + const session = await requireSession(); + const allowed = await checkPermission(permission.user.credential, relation.delete, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + const identityApi = await getIdentityApi(); const { data } = await identityApi.deleteIdentity({ id }); diff --git a/dashboard/src/lib/permission.ts b/dashboard/src/lib/permission.ts index bab8592..49958b0 100644 --- a/dashboard/src/lib/permission.ts +++ b/dashboard/src/lib/permission.ts @@ -5,6 +5,9 @@ export const permission = { }, user: { it: 'admin.user', + code: 'admin.user.code', + credential: 'admin.user.credential', + link: 'admin.user.link', session: 'admin.user.session', state: 'admin.user.state', }, @@ -12,6 +15,7 @@ export const permission = { export const relation = { access: 'access', + create: 'create', edit: 'edit', delete: 'delete', }; From b72a45f39d82aabf754db53806dd104c32e6e5ed Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 6 Apr 2025 11:14:22 +0200 Subject: [PATCH 12/21] NORY-59: add permission checks to identity action UI --- dashboard/src/app/(inside)/user/[id]/page.tsx | 41 ++++++++++++++--- .../components/identity/identity-actions.tsx | 44 +++++++++++++++---- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/dashboard/src/app/(inside)/user/[id]/page.tsx b/dashboard/src/app/(inside)/user/[id]/page.tsx index 83cc3b3..98a578a 100644 --- a/dashboard/src/app/(inside)/user/[id]/page.tsx +++ b/dashboard/src/app/(inside)/user/[id]/page.tsx @@ -11,6 +11,9 @@ import { Badge } from '@/components/ui/badge'; import { Check, X } from 'lucide-react'; import { IdentityActions } from '@/components/identity/identity-actions'; import { IdentityCredentials } from '@/components/identity/identity-credentials'; +import { checkPermission, requirePermission, requireSession } from '@/lib/action/authentication'; +import { permission, relation } from '@/lib/permission'; +import { redirect } from 'next/navigation'; interface MergedAddress { recovery_id?: string; @@ -76,19 +79,35 @@ function mergeAddresses( export default async function UserDetailsPage({ params }: { params: Promise<{ id: string }> }) { - const identityId = (await params).id; + const session = await requireSession(); + const identityId = session.identity!.id; + + await requirePermission(permission.stack.dashboard, relation.access, identityId); + + const pmAccessUser = await checkPermission(permission.user.it, relation.access, identityId); + if (!pmAccessUser) { + return redirect('/user'); + } + + const pmEditUser = await checkPermission(permission.user.it, relation.edit, identityId); + const pmDeleteUser = await checkPermission(permission.user.it, relation.delete, identityId); + const pmEditUserState = await checkPermission(permission.user.state, relation.edit, identityId); + const pmDeleteUserSession = await checkPermission(permission.user.session, relation.delete, identityId); + const pmCreateUserCode = await checkPermission(permission.user.code, relation.create, identityId); + const pmCreateUserLink = await checkPermission(permission.user.link, relation.create, identityId); + + const detailIdentityId = (await params).id; const identityApi = await getIdentityApi(); - const identity = await identityApi.getIdentity({ id: identityId }) + const identity = await identityApi.getIdentity({ id: detailIdentityId }) .then((response) => { - console.log('identity', response.data); return response.data; }) .catch(() => { console.log('Identity not found'); }); - const sessions = await identityApi.listIdentitySessions({ id: identityId }) + const sessions = await identityApi.listIdentitySessions({ id: detailIdentityId }) .then((response) => response.data) .catch(() => { console.log('No sessions found'); @@ -97,7 +116,7 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id if (!identity) { return ; + message={`The requested identity with id ${detailIdentityId} does not exist`}/>; } if (!identity.verifiable_addresses || !identity.verifiable_addresses[0]) { @@ -137,7 +156,17 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id Quick actions to manage the identity - + diff --git a/dashboard/src/components/identity/identity-actions.tsx b/dashboard/src/components/identity/identity-actions.tsx index 0ddcf32..b6a48ca 100644 --- a/dashboard/src/components/identity/identity-actions.tsx +++ b/dashboard/src/components/identity/identity-actions.tsx @@ -28,12 +28,22 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; interface IdentityActionProps { - identity: Identity; + identity: Identity, + permissions: { + pmEditUser: boolean; + pmDeleteUser: boolean; + pmEditUserState: boolean; + pmDeleteUserSession: boolean; + pmCreateUserCode: boolean; + pmCreateUserLink: boolean; + } } -export function IdentityActions({ identity }: IdentityActionProps, +export function IdentityActions({ identity, permissions }: IdentityActionProps, ) { + console.log('IdentityActions', 'Permissions', permissions); + const router = useRouter(); const [dialogVisible, setDialogVisible] = useState(false); @@ -122,7 +132,10 @@ export function IdentityActions({ identity }: IdentityActionProps, dialogDescription="Are you sure you want to create a recovery code for this identity?" dialogButtonSubmit="Create code" > - @@ -142,7 +155,10 @@ export function IdentityActions({ identity }: IdentityActionProps, dialogDescription="Are you sure you want to create a recovery link for this identity?" dialogButtonSubmit="Create link" > - @@ -160,7 +176,10 @@ export function IdentityActions({ identity }: IdentityActionProps, dialogDescription="Are you sure you want to deactivate this identity? The user will not be able to sign-in or use any active session until re-activation!" dialogButtonSubmit="Deactivate" > - @@ -176,7 +195,10 @@ export function IdentityActions({ identity }: IdentityActionProps, dialogDescription="Are you sure you want to activate this identity?" dialogButtonSubmit="Activate" > - @@ -194,7 +216,10 @@ export function IdentityActions({ identity }: IdentityActionProps, dialogButtonSubmit="Invalidate sessions" dialogButtonSubmitProps={{ variant: 'destructive' }} > - @@ -214,7 +239,10 @@ export function IdentityActions({ identity }: IdentityActionProps, dialogButtonSubmit="Delete identity" dialogButtonSubmitProps={{ variant: 'destructive' }} > - From b3be0440d120d7c7dd3ef9346be5f54e2ba990a6 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 6 Apr 2025 13:02:56 +0200 Subject: [PATCH 13/21] NORY-59: refactor identity queries to use server actions --- dashboard/src/app/(inside)/user/[id]/page.tsx | 149 +++++++++--------- dashboard/src/lib/action/identity.ts | 46 ++++++ dashboard/src/lib/permission.ts | 1 + 3 files changed, 123 insertions(+), 73 deletions(-) diff --git a/dashboard/src/app/(inside)/user/[id]/page.tsx b/dashboard/src/app/(inside)/user/[id]/page.tsx index 98a578a..e590abc 100644 --- a/dashboard/src/app/(inside)/user/[id]/page.tsx +++ b/dashboard/src/app/(inside)/user/[id]/page.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { getIdentityApi } from '@/ory/sdk/server'; import { ErrorDisplay } from '@/components/error'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { IdentityTraits } from '@/components/identity/identity-traits'; @@ -14,6 +13,8 @@ import { IdentityCredentials } from '@/components/identity/identity-credentials' import { checkPermission, requirePermission, requireSession } from '@/lib/action/authentication'; import { permission, relation } from '@/lib/permission'; import { redirect } from 'next/navigation'; +import InsufficientPermission from '@/components/insufficient-permission'; +import { getIdentity, getIdentitySchema, listIdentitySessions } from '@/lib/action/identity'; interface MergedAddress { recovery_id?: string; @@ -91,65 +92,65 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id const pmEditUser = await checkPermission(permission.user.it, relation.edit, identityId); const pmDeleteUser = await checkPermission(permission.user.it, relation.delete, identityId); + const pmAccessUserTraits = await checkPermission(permission.user.trait, relation.access, identityId); const pmEditUserState = await checkPermission(permission.user.state, relation.edit, identityId); + const pmAccessUserSession = await checkPermission(permission.user.session, relation.access, identityId); const pmDeleteUserSession = await checkPermission(permission.user.session, relation.delete, identityId); const pmCreateUserCode = await checkPermission(permission.user.code, relation.create, identityId); const pmCreateUserLink = await checkPermission(permission.user.link, relation.create, identityId); const detailIdentityId = (await params).id; + const detailIdentity = pmAccessUser && await getIdentity(detailIdentityId); - const identityApi = await getIdentityApi(); - const identity = await identityApi.getIdentity({ id: detailIdentityId }) - .then((response) => { - return response.data; - }) - .catch(() => { - console.log('Identity not found'); - }); - - const sessions = await identityApi.listIdentitySessions({ id: detailIdentityId }) - .then((response) => response.data) - .catch(() => { - console.log('No sessions found'); - }); - - if (!identity) { + if (!detailIdentity) { return ; } - if (!identity.verifiable_addresses || !identity.verifiable_addresses[0]) { + if (!detailIdentity.verifiable_addresses || !detailIdentity.verifiable_addresses[0]) { return ; } - const identitySchema = await identityApi - .getIdentitySchema({ id: identity.schema_id }) - .then((response) => response.data as KratosSchema); + + const detailIdentitySessions = pmAccessUserSession && await listIdentitySessions(detailIdentityId); + + const detailIdentitySchema = await getIdentitySchema(detailIdentity.schema_id) + .then((response) => response as KratosSchema); const addresses = mergeAddresses( - identity.recovery_addresses ?? [], - identity.verifiable_addresses ?? [], + detailIdentity.recovery_addresses ?? [], + detailIdentity.verifiable_addresses ?? [], ); return (

{addresses[0].value}

-

{identity.id}

+

{detailIdentity.id}

- - - Traits - All identity properties specified in the identity schema - - - - - + { + pmAccessUserTraits ? + + + Traits + All identity properties specified in the identity + schema + + + + + + : + + } Actions @@ -157,7 +158,7 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id All authentication mechanisms registered with this identity - + @@ -229,46 +230,48 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id See and manage all sessions of this identity - - - - OS - Browser - Active since - - - - { - sessions ? - sessions.map((session) => { + { + detailIdentitySessions ? +
+ + + OS + Browser + Active since + + + + { + detailIdentitySessions.map((session) => { - const device = session.devices![0]; - const parser = new UAParser(device.user_agent); - const result = parser.getResult(); + const device = session.devices![0]; + const parser = new UAParser(device.user_agent); + const result = parser.getResult(); - return ( - - - {result.os.name} - {result.os.version} - - - {result.browser.name} - {result.browser.version} - - - {new Date(session.authenticated_at!).toLocaleString()} - - - ); - }) - : - - } - -
+ return ( + + + {result.os.name} + {result.os.version} + + + {result.browser.name} + {result.browser.version} + + + {new Date(session.authenticated_at!).toLocaleString()} + + + ); + }) + } + + + : +

This user has no active sessions

+ }
diff --git a/dashboard/src/lib/action/identity.ts b/dashboard/src/lib/action/identity.ts index b39e721..11ef3d9 100644 --- a/dashboard/src/lib/action/identity.ts +++ b/dashboard/src/lib/action/identity.ts @@ -15,6 +15,34 @@ import { eq, ilike, or, sql } from 'drizzle-orm'; import { checkPermission, requireSession } from '@/lib/action/authentication'; import { permission, relation } from '@/lib/permission'; +export async function getIdentity(id: string) { + + const session = await requireSession(); + const allowed = await checkPermission(permission.user.it, relation.access, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + + const identityApi = await getIdentityApi(); + const { data } = await identityApi.getIdentity({ id }); + + console.log('Got identity', data); + + return data; +} + + +export async function getIdentitySchema(id: string) { + + const identityApi = await getIdentityApi(); + const { data } = await identityApi.getIdentitySchema({ id: id }); + + console.log('Got identity schema'); + + return data; +} + + interface QueryIdentitiesProps { page: number, pageSize: number, @@ -131,6 +159,24 @@ export async function deleteIdentityCredential({ id, type }: DeleteIdentityCrede return data; } +export async function listIdentitySessions(id: string) { + + const session = await requireSession(); + const allowed = await checkPermission(permission.user.session, relation.access, session.identity!.id); + if (!allowed) { + throw Error('Unauthorised'); + } + + const identityApi = await getIdentityApi(); + const { data } = await identityApi.listIdentitySessions({ id }); + + console.log('Listed identity\'s sessions', data); + + revalidatePath('/user'); + + return data; +} + export async function deleteIdentitySessions(id: string) { const session = await requireSession(); diff --git a/dashboard/src/lib/permission.ts b/dashboard/src/lib/permission.ts index 49958b0..69a8523 100644 --- a/dashboard/src/lib/permission.ts +++ b/dashboard/src/lib/permission.ts @@ -10,6 +10,7 @@ export const permission = { link: 'admin.user.link', session: 'admin.user.session', state: 'admin.user.state', + trait: 'admin.user.trait', }, }; From 85234b44655be26b4188b1a7a88df014862afe63 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 6 Apr 2025 15:07:16 +0200 Subject: [PATCH 14/21] NORY-59: fix path-revalidation during rendering --- dashboard/src/lib/action/identity.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/dashboard/src/lib/action/identity.ts b/dashboard/src/lib/action/identity.ts index 11ef3d9..902d17d 100644 --- a/dashboard/src/lib/action/identity.ts +++ b/dashboard/src/lib/action/identity.ts @@ -172,8 +172,6 @@ export async function listIdentitySessions(id: string) { console.log('Listed identity\'s sessions', data); - revalidatePath('/user'); - return data; } From 5494233e59f0972af28751792c459c7ab190d20c Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 6 Apr 2025 15:08:05 +0200 Subject: [PATCH 15/21] NORY-59: protect all missing cards in identity details --- dashboard/src/app/(inside)/user/[id]/page.tsx | 207 ++++++++++-------- dashboard/src/lib/permission.ts | 1 + 2 files changed, 118 insertions(+), 90 deletions(-) diff --git a/dashboard/src/app/(inside)/user/[id]/page.tsx b/dashboard/src/app/(inside)/user/[id]/page.tsx index e590abc..2fff21c 100644 --- a/dashboard/src/app/(inside)/user/[id]/page.tsx +++ b/dashboard/src/app/(inside)/user/[id]/page.tsx @@ -92,7 +92,10 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id const pmEditUser = await checkPermission(permission.user.it, relation.edit, identityId); const pmDeleteUser = await checkPermission(permission.user.it, relation.delete, identityId); - const pmAccessUserTraits = await checkPermission(permission.user.trait, relation.access, identityId); + const pmAccessUserTrait = await checkPermission(permission.user.trait, relation.access, identityId); + const pmEditUserTraits = await checkPermission(permission.user.trait, relation.edit, identityId); + const pmAccessUserAddress = await checkPermission(permission.user.address, relation.access, identityId); + const pmAccessUserCredential = await checkPermission(permission.user.credential, relation.access, identityId); const pmEditUserState = await checkPermission(permission.user.state, relation.edit, identityId); const pmAccessUserSession = await checkPermission(permission.user.session, relation.access, identityId); const pmDeleteUserSession = await checkPermission(permission.user.session, relation.delete, identityId); @@ -114,7 +117,6 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id message="The identity you are trying to see exists but has no identifiable address"/>; } - const detailIdentitySessions = pmAccessUserSession && await listIdentitySessions(detailIdentityId); const detailIdentitySchema = await getIdentitySchema(detailIdentity.schema_id) @@ -133,7 +135,7 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id
{ - pmAccessUserTraits ? + pmAccessUserTrait ? Traits @@ -170,98 +172,44 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id /> - - - Addresses - All linked addresses for verification and recovery - - + { + pmAccessUserAddress ? + + + Addresses + All linked addresses for verification and recovery + + - - - - Type - Value - - - - { - addresses.map((address) => { - return ( - - {address.value} - {address.via} - - {address.verifiable_id && - - Verifiable - { - address.verified ? - - : - - } - - } - {address.recovery_id && - Recovery - } - - - ); - }) - } - -
-
-
- - - Credentials - All authentication mechanisms registered with this identity - - - - - - - - Sessions - See and manage all sessions of this identity - - - { - detailIdentitySessions ? - OS - Browser - Active since + Type + Value { - detailIdentitySessions.map((session) => { - - const device = session.devices![0]; - const parser = new UAParser(device.user_agent); - const result = parser.getResult(); - + addresses.map((address) => { return ( - - - {result.os.name} - {result.os.version} - - - {result.browser.name} - {result.browser.version} - + + {address.value} + {address.via} - {new Date(session.authenticated_at!).toLocaleString()} + {address.verifiable_id && + + Verifiable + { + address.verified ? + + : + + } + + } + {address.recovery_id && + Recovery + } ); @@ -269,11 +217,90 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id }
- : -

This user has no active sessions

- } -
-
+
+
+ : + + } + { + pmAccessUserCredential ? + + + Credentials + All authentication mechanisms registered with this + identity + + + + + + : + + } + { + pmAccessUserSession ? + + + Sessions + See and manage all sessions of this identity + + + { + detailIdentitySessions ? + + + + OS + Browser + Active since + + + + { + detailIdentitySessions.map((session) => { + + const device = session.devices![0]; + const parser = new UAParser(device.user_agent); + const result = parser.getResult(); + + return ( + + + {result.os.name} + {result.os.version} + + + {result.browser.name} + {result.browser.version} + + + {new Date(session.authenticated_at!).toLocaleString()} + + + ); + }) + } + +
+ : +

This user has no active sessions

+ } +
+
+ : + + }
); diff --git a/dashboard/src/lib/permission.ts b/dashboard/src/lib/permission.ts index 69a8523..9fc8ba0 100644 --- a/dashboard/src/lib/permission.ts +++ b/dashboard/src/lib/permission.ts @@ -5,6 +5,7 @@ export const permission = { }, user: { it: 'admin.user', + address: 'admin.user.address', code: 'admin.user.code', credential: 'admin.user.credential', link: 'admin.user.link', From 3151103195998de4a8fcb1cbd5fcdbdcf907e478 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 6 Apr 2025 17:59:45 +0200 Subject: [PATCH 16/21] NORY-59: fix permission in delete-identity server action --- dashboard/src/lib/action/identity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/lib/action/identity.ts b/dashboard/src/lib/action/identity.ts index 902d17d..d214e17 100644 --- a/dashboard/src/lib/action/identity.ts +++ b/dashboard/src/lib/action/identity.ts @@ -286,7 +286,7 @@ export async function unblockIdentity(id: string) { export async function deleteIdentity(id: string) { const session = await requireSession(); - const allowed = await checkPermission(permission.user.credential, relation.delete, session.identity!.id); + const allowed = await checkPermission(permission.user.it, relation.delete, session.identity!.id); if (!allowed) { throw Error('Unauthorised'); } From 50bedbb976a9af14d5567ba1a360e3ec76d578f3 Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Sun, 6 Apr 2025 19:16:01 +0200 Subject: [PATCH 17/21] NORY-59: disable identity traits form if edit permission is missing --- dashboard/src/app/(inside)/user/[id]/page.tsx | 6 +++++- dashboard/src/components/dynamic-form.tsx | 8 +++++--- dashboard/src/components/identity/identity-traits.tsx | 10 ++++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/dashboard/src/app/(inside)/user/[id]/page.tsx b/dashboard/src/app/(inside)/user/[id]/page.tsx index 2fff21c..3393c58 100644 --- a/dashboard/src/app/(inside)/user/[id]/page.tsx +++ b/dashboard/src/app/(inside)/user/[id]/page.tsx @@ -143,7 +143,11 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id schema - +
: diff --git a/dashboard/src/components/dynamic-form.tsx b/dashboard/src/components/dynamic-form.tsx index 9020d46..f36cde4 100644 --- a/dashboard/src/components/dynamic-form.tsx +++ b/dashboard/src/components/dynamic-form.tsx @@ -15,6 +15,7 @@ interface DynamicFormProps { onValid: SubmitHandler, onInvalid: SubmitErrorHandler, submitLabel?: string, + disabled?: boolean, } export function DynamicForm( @@ -25,6 +26,7 @@ export function DynamicForm( onValid, onInvalid, submitLabel, + disabled, }: DynamicFormProps, ) { @@ -48,7 +50,7 @@ export function DynamicForm( key={fullFieldName} render={({ field }) => ( - + {key} )} @@ -65,7 +67,7 @@ export function DynamicForm( {value.title} - + {value.description} @@ -87,7 +89,7 @@ export function DynamicForm( diff --git a/dashboard/src/components/identity/identity-traits.tsx b/dashboard/src/components/identity/identity-traits.tsx index 1606db4..3feb314 100644 --- a/dashboard/src/components/identity/identity-traits.tsx +++ b/dashboard/src/components/identity/identity-traits.tsx @@ -16,9 +16,10 @@ import { useState } from 'react'; interface IdentityTraitFormProps { schema: KratosSchema; identity: Identity; + disabled: boolean; } -export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) { +export function IdentityTraits({ schema, identity, disabled }: IdentityTraitFormProps) { const [currentIdentity, setCurrentIdentity] = useState(identity); @@ -74,10 +75,11 @@ export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) { return ( Public Metadata -