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
diff --git a/dashboard/src/app/(inside)/page.tsx b/dashboard/src/app/(inside)/page.tsx
index 604a7e2..607c2c4 100644
--- a/dashboard/src/app/(inside)/page.tsx
+++ b/dashboard/src/app/(inside)/page.tsx
@@ -1,40 +1,21 @@
-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, requirePermission, requireSession } from '@/lib/action/authentication';
+import InsufficientPermission from '@/components/insufficient-permission';
+import { permission, relation } from '@/lib/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 requirePermission(permission.stack.dashboard, relation.access, 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(permission.stack.status, relation.access, identityId);
+ const kratos = pmAccessStackStatus && await kratosMetadata();
+ const hydra = pmAccessStackStatus && await hydraMetadata();
+ const keto = pmAccessStackStatus && await ketoMetadata();
return (
@@ -43,25 +24,46 @@ export default async function RootPage() {
See the list of all applications in your stack
-
-
-
-
+ {
+ !pmAccessStackStatus && (
+
+ )
+ }
+ {
+ kratos && (
+
+ )
+ }
+ {
+ hydra && (
+
+ )
+ }
+ {
+ keto && (
+
+ )
+ }
);
diff --git a/dashboard/src/app/(inside)/user/[id]/page.tsx b/dashboard/src/app/(inside)/user/[id]/page.tsx
index 83cc3b3..e132f18 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';
@@ -11,6 +10,11 @@ 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';
+import InsufficientPermission from '@/components/insufficient-permission';
+import { getIdentity, getIdentitySchema, listIdentitySessions } from '@/lib/action/identity';
interface MergedAddress {
recovery_id?: string;
@@ -76,172 +80,229 @@ 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;
- const identityApi = await getIdentityApi();
- const identity = await identityApi.getIdentity({ id: identityId })
- .then((response) => {
- console.log('identity', response.data);
- return response.data;
- })
- .catch(() => {
- console.log('Identity not found');
- });
+ await requirePermission(permission.stack.dashboard, relation.access, identityId);
- const sessions = await identityApi.listIdentitySessions({ id: identityId })
- .then((response) => response.data)
- .catch(() => {
- console.log('No sessions found');
- });
-
- if (!identity) {
- return ;
+ const pmAccessUser = await checkPermission(permission.user.it, relation.access, identityId);
+ if (!pmAccessUser) {
+ return redirect('/user');
}
- if (!identity.verifiable_addresses || !identity.verifiable_addresses[0]) {
+ const pmDeleteUser = await checkPermission(permission.user.it, relation.delete, 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);
+ 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);
+
+ if (!detailIdentity) {
+ return ;
+ }
+
+ 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
-
-
-
-
-
+ {
+ pmAccessUserTrait ?
+
+
+ Traits
+ All identity properties specified in the identity
+ schema
+
+
+
+
+
+ :
+
+ }
Actions
Quick actions to manage the identity
-
+
-
-
- 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 ?
-
- :
-
+
+
+
+ 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
-
-
-
-
-
- OS
- Browser
- Active since
-
-
-
+ {address.recovery_id &&
+ Recovery
+ }
+
+
+ );
+ })
+ }
+
+
+
+
+ :
+
+ }
+ {
+ pmAccessUserCredential ?
+
+
+ Credentials
+ All authentication mechanisms registered with this
+ identity
+
+
+
+
+
+ :
+
+ }
+ {
+ pmAccessUserSession ?
+
+
+ Sessions
+ See and manage all sessions of this identity
+
+
{
- sessions ?
- sessions.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()}
-
+ 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/app/(inside)/user/data-table.tsx b/dashboard/src/app/(inside)/user/data-table.tsx
index 510ee6c..69a1c4b 100644
--- a/dashboard/src/app/(inside)/user/data-table.tsx
+++ b/dashboard/src/app/(inside)/user/data-table.tsx
@@ -33,9 +33,15 @@ interface IdentityDataTableProps {
data: Identity[];
page: number;
query: string;
+ permission: {
+ pmEditUser: boolean;
+ pmDeleteUser: boolean;
+ pmEditUserState: boolean;
+ pmDeleteUserSession: boolean;
+ };
}
-export function IdentityDataTable({ data, page, query }: IdentityDataTableProps) {
+export function IdentityDataTable({ data, page, query, permission }: IdentityDataTableProps) {
const columns: ColumnDef[] = [
{
@@ -137,6 +143,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 +155,7 @@ export function IdentityDataTable({ data, page, query }: IdentityDataTableProps)
setCurrentIdentity(identity);
setBlockIdentityVisible(true);
}}
+ disabled={!permission.pmEditUserState}
className="flex items-center space-x-2 text-red-500">
Block identity
@@ -160,6 +168,7 @@ export function IdentityDataTable({ data, page, query }: IdentityDataTableProps)
setCurrentIdentity(identity);
setUnblockIdentityVisible(true);
}}
+ disabled={!permission.pmEditUserState}
className="flex items-center space-x-2 text-red-500">
Unblock identity
@@ -170,6 +179,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..8218dc3 100644
--- a/dashboard/src/app/(inside)/user/page.tsx
+++ b/dashboard/src/app/(inside)/user/page.tsx
@@ -3,6 +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, requirePermission, requireSession } from '@/lib/action/authentication';
+import InsufficientPermission from '@/components/insufficient-permission';
+import { permission, relation } from '@/lib/permission';
export default async function UserPage(
{
@@ -12,6 +15,17 @@ export default async function UserPage(
},
) {
+ 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);
+ 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;
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,45 @@ 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
+
+
+
+ >
+ )
+ }
);
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(
{submitLabel ?? 'Submit'}
diff --git a/dashboard/src/components/identity/identity-actions.tsx b/dashboard/src/components/identity/identity-actions.tsx
index 0ddcf32..7606af3 100644
--- a/dashboard/src/components/identity/identity-actions.tsx
+++ b/dashboard/src/components/identity/identity-actions.tsx
@@ -28,12 +28,21 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
interface IdentityActionProps {
- identity: Identity;
+ identity: Identity,
+ permissions: {
+ 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 +131,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 +154,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 +175,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 +194,10 @@ export function IdentityActions({ identity }: IdentityActionProps,
dialogDescription="Are you sure you want to activate this identity?"
dialogButtonSubmit="Activate"
>
-
+
@@ -194,7 +215,10 @@ export function IdentityActions({ identity }: IdentityActionProps,
dialogButtonSubmit="Invalidate sessions"
dialogButtonSubmitProps={{ variant: 'destructive' }}
>
-
+
@@ -214,7 +238,10 @@ export function IdentityActions({ identity }: IdentityActionProps,
dialogButtonSubmit="Delete identity"
dialogButtonSubmitProps={{ variant: 'destructive' }}
>
-
+
diff --git a/dashboard/src/components/identity/identity-credentials.tsx b/dashboard/src/components/identity/identity-credentials.tsx
index f362971..78e1fe6 100644
--- a/dashboard/src/components/identity/identity-credentials.tsx
+++ b/dashboard/src/components/identity/identity-credentials.tsx
@@ -36,7 +36,7 @@ export function IdentityCredentials({ identity }: IdentityCredentialsProps) {
(
{
- deleteIdentityCredential({ id: identity.id, type: key as never })
+ deleteIdentityCredential(identity.id, key as never)
.then(() => toast.success(`Credential ${key} deleted`))
.catch(() => toast.error(`Deleting credential ${key} failed`));
}}
diff --git a/dashboard/src/components/identity/identity-traits.tsx b/dashboard/src/components/identity/identity-traits.tsx
index 1606db4..1a9205c 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);
@@ -47,16 +48,16 @@ export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) {
delete traits['metadata_public'];
delete traits['metadata_admin'];
- updateIdentity({
- id: currentIdentity.id,
- body: {
+ updateIdentity(
+ currentIdentity.id,
+ {
schema_id: currentIdentity.schema_id,
state: currentIdentity.state!,
traits: traits,
metadata_public: data.metadata_public,
metadata_admin: data.metadata_admin,
},
- })
+ )
.then((identity) => {
setCurrentIdentity(identity);
toast.success('Identity updated');
@@ -74,10 +75,11 @@ export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) {
return (
Public Metadata
-
+
This has to be valid JSON
@@ -99,7 +101,7 @@ export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) {
Admin Metadata
-
+
This has to be valid JSON
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}
+
+
+
+ );
+}
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/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;
+}
diff --git a/dashboard/src/lib/action/identity.ts b/dashboard/src/lib/action/identity.ts
index 4ffed77..236e4b0 100644
--- a/dashboard/src/lib/action/identity.ts
+++ b/dashboard/src/lib/action/identity.ts
@@ -12,14 +12,43 @@ 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,
- pageSize: number,
- query?: string,
+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 queryIdentities({ page, pageSize, query }: QueryIdentitiesProps) {
+
+export async function getIdentitySchema(id: string) {
+
+ const identityApi = await getIdentityApi();
+ const { data } = await identityApi.getIdentitySchema({ id: id });
+
+ console.log('Got identity schema');
+
+ return data;
+}
+
+export async function queryIdentities(page: number, pageSize: number, query?: string) {
+
+ 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 {
@@ -73,13 +102,13 @@ export async function queryIdentities({ page, pageSize, query }: QueryIdentities
};
}
+export async function updateIdentity(id: string, body: UpdateIdentityBody) {
-interface UpdatedIdentityProps {
- id: string;
- body: UpdateIdentityBody;
-}
-
-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({
@@ -94,12 +123,13 @@ export async function updateIdentity({ id, body }: UpdatedIdentityProps) {
return data;
}
-interface DeleteIdentityCredentialProps {
- id: string;
- type: DeleteIdentityCredentialsTypeEnum;
-}
+export async function deleteIdentityCredential(id: string, type: DeleteIdentityCredentialsTypeEnum) {
-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 });
@@ -111,8 +141,30 @@ 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);
+
+ return data;
+}
+
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 +177,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 +197,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 +217,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 +242,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 +267,12 @@ export async function unblockIdentity(id: string) {
export async function deleteIdentity(id: string) {
+ const session = await requireSession();
+ const allowed = await checkPermission(permission.user.it, 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/action/metadata.ts b/dashboard/src/lib/action/metadata.ts
new file mode 100644
index 0000000..5a64b3b
--- /dev/null
+++ b/dashboard/src/lib/action/metadata.ts
@@ -0,0 +1,84 @@
+'use server';
+
+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(permission.stack.status, relation.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(permission.stack.status, relation.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(permission.stack.status, relation.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,
+ };
+}
diff --git a/dashboard/src/lib/permission.ts b/dashboard/src/lib/permission.ts
new file mode 100644
index 0000000..9fc8ba0
--- /dev/null
+++ b/dashboard/src/lib/permission.ts
@@ -0,0 +1,23 @@
+export const permission = {
+ stack: {
+ dashboard: 'admin.stack.dashboard',
+ status: 'admin.stack.status',
+ },
+ user: {
+ it: 'admin.user',
+ address: 'admin.user.address',
+ code: 'admin.user.code',
+ credential: 'admin.user.credential',
+ link: 'admin.user.link',
+ session: 'admin.user.session',
+ state: 'admin.user.state',
+ trait: 'admin.user.trait',
+ },
+};
+
+export const relation = {
+ access: 'access',
+ create: 'create',
+ edit: 'edit',
+ delete: 'delete',
+};
diff --git a/dashboard/src/middleware.ts b/dashboard/src/middleware.ts
index 52d7714..8fb5913 100644
--- a/dashboard/src/middleware.ts
+++ b/dashboard/src/middleware.ts
@@ -1,47 +1,29 @@
import { NextRequest, NextResponse } from 'next/server';
-import { cookies } from 'next/headers';
-import { getFrontendApi, getPermissionApi } from '@/ory/sdk/server';
+import { checkPermission, getSession } from '@/lib/action/authentication';
+import { permission, relation } from '@/lib/permission';
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 checkPermission(permission.stack.dashboard, relation.access, session.identity!.id);
- if (isAdmin) {
+
+ if (allowed) {
if (request.nextUrl.pathname === '/unauthorised') {
return redirect('/', 'HAS PERMISSION BUT ACCESSING /unauthorized');
}
@@ -55,9 +37,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!);
}
diff --git a/docker/ory-dev/hydra-create-client.sh b/docker/ory-dev/hydra-create-client.sh
index 181920e..7d47b3a 100644
--- a/docker/ory-dev/hydra-create-client.sh
+++ b/docker/ory-dev/hydra-create-client.sh
@@ -1,4 +1,4 @@
-# this script adds a new oath client using the
+# this script adds a new OAuth client using the
# Ory Hydra CLI and writes the client id and
# client secret to the command line.
diff --git a/docker/ory-dev/hydra-test-consent.sh b/docker/ory-dev/hydra-test-consent.sh
index 8067e7d..1f018ca 100644
--- a/docker/ory-dev/hydra-test-consent.sh
+++ b/docker/ory-dev/hydra-test-consent.sh
@@ -1,4 +1,4 @@
-# this script adds a new oath client using the
+# this script adds a new OAuth client using the
# Ory Hydra CLI and uses the client to start
# the Ory Hydra test application.
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..65807a2
--- /dev/null
+++ b/docker/ory-dev/keto-add-permission-to-role.sh
@@ -0,0 +1,30 @@
+# this script creates a reference from the role to the permission you provide
+
+# 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/keto-init-admin-role.sh b/docker/ory-dev/keto-init-admin-role.sh
new file mode 100644
index 0000000..171ee20
--- /dev/null
+++ b/docker/ory-dev/keto-init-admin-role.sh
@@ -0,0 +1,47 @@
+# this script adds all permissions required for full control over the dashboard to
+# all everybody, who is a member of the admin role
+
+# Define an array with tuples as strings
+permissions=(
+ "admin.stack.dashboard#access"
+ "admin.stack.status#access"
+ "admin.user#access"
+ "admin.user#create"
+ "admin.user#edit"
+ "admin.user#delete"
+ "admin.user.session#access"
+ "admin.user.session#delete"
+ "admin.user.state#edit"
+ "admin.user.code#create"
+ "admin.user.link#create"
+ "admin.user.trait#access"
+ "admin.user.trait#edit"
+ "admin.user.address#access"
+ "admin.user.credential#access"
+ "admin.user.credential#delete"
+)
+
+# Iterate over the array
+for permission in "${permissions[@]}"; do
+
+ # split strings
+ IFS='#' read -r OBJECT RELATION <<< "$permission"
+
+ # execute curl to Ory Keto write endpoint
+ curl --silent --request PUT \
+ --url http://localhost:4467/admin/relation-tuples \
+ --data '{
+ "namespace": "permissions",
+ "object": "'"$OBJECT"'",
+ "relation": "'"$RELATION"'",
+ "subject_set": {
+ "namespace": "roles",
+ "object": "admin",
+ "relation": "member"
+ }
+ }' > /dev/null
+
+ # write success response to terminal
+ echo "Added relation Permissions:$OBJECT#$RELATION@(Roles:admin#member)"
+
+done
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: