1
0
Fork 0
mirror of https://codeberg.org/MarkusThielker/next-ory.git synced 2025-04-18 00:21:18 +00:00

NORY-59: Handling of CVE-2025-29927 (#60)

Reviewed-on: #60
This commit is contained in:
Markus Thielker 2025-04-07 09:40:29 +00:00
commit 7961b50adb
22 changed files with 990 additions and 432 deletions

View file

@ -1,334 +1,334 @@
import { relations } from 'drizzle-orm/relations'; import { relations } from 'drizzle-orm/relations';
import { import {
continuityContainers, continuity_containers,
courierMessageDispatches, courier_message_dispatches,
courierMessages, courier_messages,
identities, identities,
identityCredentialIdentifiers, identity_credential_identifiers,
identityCredentials, identity_credential_types,
identityCredentialTypes, identity_credentials,
identityLoginCodes, identity_login_codes,
identityRecoveryAddresses, identity_recovery_addresses,
identityRecoveryCodes, identity_recovery_codes,
identityRecoveryTokens, identity_recovery_tokens,
identityRegistrationCodes, identity_registration_codes,
identityVerifiableAddresses, identity_verifiable_addresses,
identityVerificationCodes, identity_verification_codes,
identityVerificationTokens, identity_verification_tokens,
networks, networks,
selfserviceErrors, selfservice_errors,
selfserviceLoginFlows, selfservice_login_flows,
selfserviceRecoveryFlows, selfservice_recovery_flows,
selfserviceRegistrationFlows, selfservice_registration_flows,
selfserviceSettingsFlows, selfservice_settings_flows,
selfserviceVerificationFlows, selfservice_verification_flows,
sessionDevices, session_devices,
sessions, sessions,
} from './schema'; } from './schema';
export const identityCredentialsRelations = relations(identityCredentials, ({ one, many }) => ({ export const identityCredentialsRelations = relations(identity_credentials, ({ one, many }) => ({
identity: one(identities, { identity: one(identities, {
fields: [identityCredentials.identityId], fields: [identity_credentials.identity_id],
references: [identities.id], references: [identities.id],
}), }),
identityCredentialType: one(identityCredentialTypes, { identityCredentialType: one(identity_credential_types, {
fields: [identityCredentials.identityCredentialTypeId], fields: [identity_credentials.identity_credential_type_id],
references: [identityCredentialTypes.id], references: [identity_credential_types.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [identityCredentials.nid], fields: [identity_credentials.nid],
references: [networks.id], references: [networks.id],
}), }),
identityCredentialIdentifiers: many(identityCredentialIdentifiers), identityCredentialIdentifiers: many(identity_credential_identifiers),
})); }));
export const identitiesRelations = relations(identities, ({ one, many }) => ({ export const identitiesRelations = relations(identities, ({ one, many }) => ({
identityCredentials: many(identityCredentials), identityCredentials: many(identity_credentials),
network: one(networks, { network: one(networks, {
fields: [identities.nid], fields: [identities.nid],
references: [networks.id], references: [networks.id],
}), }),
identityVerifiableAddresses: many(identityVerifiableAddresses), identityVerifiableAddresses: many(identity_verifiable_addresses),
selfserviceSettingsFlows: many(selfserviceSettingsFlows), selfserviceSettingsFlows: many(selfservice_settings_flows),
continuityContainers: many(continuityContainers), continuityContainers: many(continuity_containers),
sessions: many(sessions), sessions: many(sessions),
identityRecoveryAddresses: many(identityRecoveryAddresses), identityRecoveryAddresses: many(identity_recovery_addresses),
selfserviceRecoveryFlows: many(selfserviceRecoveryFlows), selfserviceRecoveryFlows: many(selfservice_recovery_flows),
identityRecoveryTokens: many(identityRecoveryTokens), identityRecoveryTokens: many(identity_recovery_tokens),
identityRecoveryCodes: many(identityRecoveryCodes), identityRecoveryCodes: many(identity_recovery_codes),
identityLoginCodes: many(identityLoginCodes), identityLoginCodes: many(identity_login_codes),
})); }));
export const identityCredentialTypesRelations = relations(identityCredentialTypes, ({ many }) => ({ export const identityCredentialTypesRelations = relations(identity_credential_types, ({ many }) => ({
identityCredentials: many(identityCredentials), identityCredentials: many(identity_credentials),
identityCredentialIdentifiers: many(identityCredentialIdentifiers), identityCredentialIdentifiers: many(identity_credential_identifiers),
})); }));
export const networksRelations = relations(networks, ({ many }) => ({ export const networksRelations = relations(networks, ({ many }) => ({
identityCredentials: many(identityCredentials), identityCredentials: many(identity_credentials),
selfserviceLoginFlows: many(selfserviceLoginFlows), selfserviceLoginFlows: many(selfservice_login_flows),
selfserviceRegistrationFlows: many(selfserviceRegistrationFlows), selfserviceRegistrationFlows: many(selfservice_registration_flows),
identities: many(identities), identities: many(identities),
identityCredentialIdentifiers: many(identityCredentialIdentifiers), identityCredentialIdentifiers: many(identity_credential_identifiers),
identityVerifiableAddresses: many(identityVerifiableAddresses), identityVerifiableAddresses: many(identity_verifiable_addresses),
courierMessages: many(courierMessages), courierMessages: many(courier_messages),
selfserviceErrors: many(selfserviceErrors), selfserviceErrors: many(selfservice_errors),
selfserviceVerificationFlows: many(selfserviceVerificationFlows), selfserviceVerificationFlows: many(selfservice_verification_flows),
selfserviceSettingsFlows: many(selfserviceSettingsFlows), selfserviceSettingsFlows: many(selfservice_settings_flows),
continuityContainers: many(continuityContainers), continuityContainers: many(continuity_containers),
sessions: many(sessions), sessions: many(sessions),
identityRecoveryAddresses: many(identityRecoveryAddresses), identityRecoveryAddresses: many(identity_recovery_addresses),
identityVerificationTokens: many(identityVerificationTokens), identityVerificationTokens: many(identity_verification_tokens),
selfserviceRecoveryFlows: many(selfserviceRecoveryFlows), selfserviceRecoveryFlows: many(selfservice_recovery_flows),
identityRecoveryTokens: many(identityRecoveryTokens), identityRecoveryTokens: many(identity_recovery_tokens),
identityRecoveryCodes: many(identityRecoveryCodes), identityRecoveryCodes: many(identity_recovery_codes),
sessionDevices: many(sessionDevices), sessionDevices: many(session_devices),
identityVerificationCodes: many(identityVerificationCodes), identityVerificationCodes: many(identity_verification_codes),
courierMessageDispatches: many(courierMessageDispatches), courierMessageDispatches: many(courier_message_dispatches),
identityLoginCodes: many(identityLoginCodes), identityLoginCodes: many(identity_login_codes),
identityRegistrationCodes: many(identityRegistrationCodes), identityRegistrationCodes: many(identity_registration_codes),
})); }));
export const selfserviceLoginFlowsRelations = relations(selfserviceLoginFlows, ({ one, many }) => ({ export const selfserviceLoginFlowsRelations = relations(selfservice_login_flows, ({ one, many }) => ({
network: one(networks, { network: one(networks, {
fields: [selfserviceLoginFlows.nid], fields: [selfservice_login_flows.nid],
references: [networks.id], 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, { network: one(networks, {
fields: [selfserviceRegistrationFlows.nid], fields: [selfservice_registration_flows.nid],
references: [networks.id], references: [networks.id],
}), }),
identityRegistrationCodes: many(identityRegistrationCodes), identityRegistrationCodes: many(identity_registration_codes),
})); }));
export const identityCredentialIdentifiersRelations = relations(identityCredentialIdentifiers, ({ one }) => ({ export const identityCredentialIdentifiersRelations = relations(identity_credential_identifiers, ({ one }) => ({
identityCredential: one(identityCredentials, { identityCredential: one(identity_credentials, {
fields: [identityCredentialIdentifiers.identityCredentialId], fields: [identity_credential_identifiers.identity_credential_id],
references: [identityCredentials.id], references: [identity_credentials.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [identityCredentialIdentifiers.nid], fields: [identity_credential_identifiers.nid],
references: [networks.id], references: [networks.id],
}), }),
identityCredentialType: one(identityCredentialTypes, { identityCredentialType: one(identity_credential_types, {
fields: [identityCredentialIdentifiers.identityCredentialTypeId], fields: [identity_credential_identifiers.identity_credential_type_id],
references: [identityCredentialTypes.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, { identity: one(identities, {
fields: [identityVerifiableAddresses.identityId], fields: [identity_verifiable_addresses.identity_id],
references: [identities.id], references: [identities.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [identityVerifiableAddresses.nid], fields: [identity_verifiable_addresses.nid],
references: [networks.id], references: [networks.id],
}), }),
identityVerificationTokens: many(identityVerificationTokens), identityVerificationTokens: many(identity_verification_tokens),
identityVerificationCodes: many(identityVerificationCodes), identityVerificationCodes: many(identity_verification_codes),
})); }));
export const courierMessagesRelations = relations(courierMessages, ({ one, many }) => ({ export const courierMessagesRelations = relations(courier_messages, ({ one, many }) => ({
network: one(networks, { network: one(networks, {
fields: [courierMessages.nid], fields: [courier_messages.nid],
references: [networks.id], 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, { network: one(networks, {
fields: [selfserviceErrors.nid], fields: [selfservice_errors.nid],
references: [networks.id], references: [networks.id],
}), }),
})); }));
export const selfserviceVerificationFlowsRelations = relations(selfserviceVerificationFlows, ({ one, many }) => ({ export const selfserviceVerificationFlowsRelations = relations(selfservice_verification_flows, ({ one, many }) => ({
network: one(networks, { network: one(networks, {
fields: [selfserviceVerificationFlows.nid], fields: [selfservice_verification_flows.nid],
references: [networks.id], references: [networks.id],
}), }),
identityVerificationTokens: many(identityVerificationTokens), identityVerificationTokens: many(identity_verification_tokens),
identityVerificationCodes: many(identityVerificationCodes), identityVerificationCodes: many(identity_verification_codes),
})); }));
export const selfserviceSettingsFlowsRelations = relations(selfserviceSettingsFlows, ({ one }) => ({ export const selfserviceSettingsFlowsRelations = relations(selfservice_settings_flows, ({ one }) => ({
identity: one(identities, { identity: one(identities, {
fields: [selfserviceSettingsFlows.identityId], fields: [selfservice_settings_flows.identity_id],
references: [identities.id], references: [identities.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [selfserviceSettingsFlows.nid], fields: [selfservice_settings_flows.nid],
references: [networks.id], references: [networks.id],
}), }),
})); }));
export const continuityContainersRelations = relations(continuityContainers, ({ one }) => ({ export const continuityContainersRelations = relations(continuity_containers, ({ one }) => ({
identity: one(identities, { identity: one(identities, {
fields: [continuityContainers.identityId], fields: [continuity_containers.identity_id],
references: [identities.id], references: [identities.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [continuityContainers.nid], fields: [continuity_containers.nid],
references: [networks.id], references: [networks.id],
}), }),
})); }));
export const sessionsRelations = relations(sessions, ({ one, many }) => ({ export const sessionsRelations = relations(sessions, ({ one, many }) => ({
identity: one(identities, { identity: one(identities, {
fields: [sessions.identityId], fields: [sessions.identity_id],
references: [identities.id], references: [identities.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [sessions.nid], fields: [sessions.nid],
references: [networks.id], 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, { identity: one(identities, {
fields: [identityRecoveryAddresses.identityId], fields: [identity_recovery_addresses.identity_id],
references: [identities.id], references: [identities.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [identityRecoveryAddresses.nid], fields: [identity_recovery_addresses.nid],
references: [networks.id], references: [networks.id],
}), }),
identityRecoveryTokens: many(identityRecoveryTokens), identityRecoveryTokens: many(identity_recovery_tokens),
identityRecoveryCodes: many(identityRecoveryCodes), identityRecoveryCodes: many(identity_recovery_codes),
})); }));
export const identityVerificationTokensRelations = relations(identityVerificationTokens, ({ one }) => ({ export const identityVerificationTokensRelations = relations(identity_verification_tokens, ({ one }) => ({
identityVerifiableAddress: one(identityVerifiableAddresses, { identityVerifiableAddress: one(identity_verifiable_addresses, {
fields: [identityVerificationTokens.identityVerifiableAddressId], fields: [identity_verification_tokens.identity_verifiable_address_id],
references: [identityVerifiableAddresses.id], references: [identity_verifiable_addresses.id],
}), }),
selfserviceVerificationFlow: one(selfserviceVerificationFlows, { selfserviceVerificationFlow: one(selfservice_verification_flows, {
fields: [identityVerificationTokens.selfserviceVerificationFlowId], fields: [identity_verification_tokens.selfservice_verification_flow_id],
references: [selfserviceVerificationFlows.id], references: [selfservice_verification_flows.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [identityVerificationTokens.nid], fields: [identity_verification_tokens.nid],
references: [networks.id], references: [networks.id],
}), }),
})); }));
export const selfserviceRecoveryFlowsRelations = relations(selfserviceRecoveryFlows, ({ one, many }) => ({ export const selfserviceRecoveryFlowsRelations = relations(selfservice_recovery_flows, ({ one, many }) => ({
identity: one(identities, { identity: one(identities, {
fields: [selfserviceRecoveryFlows.recoveredIdentityId], fields: [selfservice_recovery_flows.recovered_identity_id],
references: [identities.id], references: [identities.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [selfserviceRecoveryFlows.nid], fields: [selfservice_recovery_flows.nid],
references: [networks.id], references: [networks.id],
}), }),
identityRecoveryTokens: many(identityRecoveryTokens), identityRecoveryTokens: many(identity_recovery_tokens),
identityRecoveryCodes: many(identityRecoveryCodes), identityRecoveryCodes: many(identity_recovery_codes),
})); }));
export const identityRecoveryTokensRelations = relations(identityRecoveryTokens, ({ one }) => ({ export const identityRecoveryTokensRelations = relations(identity_recovery_tokens, ({ one }) => ({
selfserviceRecoveryFlow: one(selfserviceRecoveryFlows, { selfserviceRecoveryFlow: one(selfservice_recovery_flows, {
fields: [identityRecoveryTokens.selfserviceRecoveryFlowId], fields: [identity_recovery_tokens.selfservice_recovery_flow_id],
references: [selfserviceRecoveryFlows.id], references: [selfservice_recovery_flows.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [identityRecoveryTokens.nid], fields: [identity_recovery_tokens.nid],
references: [networks.id], references: [networks.id],
}), }),
identityRecoveryAddress: one(identityRecoveryAddresses, { identityRecoveryAddress: one(identity_recovery_addresses, {
fields: [identityRecoveryTokens.identityRecoveryAddressId], fields: [identity_recovery_tokens.identity_recovery_address_id],
references: [identityRecoveryAddresses.id], references: [identity_recovery_addresses.id],
}), }),
identity: one(identities, { identity: one(identities, {
fields: [identityRecoveryTokens.identityId], fields: [identity_recovery_tokens.identity_id],
references: [identities.id], references: [identities.id],
}), }),
})); }));
export const identityRecoveryCodesRelations = relations(identityRecoveryCodes, ({ one }) => ({ export const identityRecoveryCodesRelations = relations(identity_recovery_codes, ({ one }) => ({
identityRecoveryAddress: one(identityRecoveryAddresses, { identityRecoveryAddress: one(identity_recovery_addresses, {
fields: [identityRecoveryCodes.identityRecoveryAddressId], fields: [identity_recovery_codes.identity_recovery_address_id],
references: [identityRecoveryAddresses.id], references: [identity_recovery_addresses.id],
}), }),
selfserviceRecoveryFlow: one(selfserviceRecoveryFlows, { selfserviceRecoveryFlow: one(selfservice_recovery_flows, {
fields: [identityRecoveryCodes.selfserviceRecoveryFlowId], fields: [identity_recovery_codes.selfservice_recovery_flow_id],
references: [selfserviceRecoveryFlows.id], references: [selfservice_recovery_flows.id],
}), }),
identity: one(identities, { identity: one(identities, {
fields: [identityRecoveryCodes.identityId], fields: [identity_recovery_codes.identity_id],
references: [identities.id], references: [identities.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [identityRecoveryCodes.nid], fields: [identity_recovery_codes.nid],
references: [networks.id], references: [networks.id],
}), }),
})); }));
export const sessionDevicesRelations = relations(sessionDevices, ({ one }) => ({ export const sessionDevicesRelations = relations(session_devices, ({ one }) => ({
session: one(sessions, { session: one(sessions, {
fields: [sessionDevices.sessionId], fields: [session_devices.session_id],
references: [sessions.id], references: [sessions.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [sessionDevices.nid], fields: [session_devices.nid],
references: [networks.id], references: [networks.id],
}), }),
})); }));
export const identityVerificationCodesRelations = relations(identityVerificationCodes, ({ one }) => ({ export const identityVerificationCodesRelations = relations(identity_verification_codes, ({ one }) => ({
identityVerifiableAddress: one(identityVerifiableAddresses, { identityVerifiableAddress: one(identity_verifiable_addresses, {
fields: [identityVerificationCodes.identityVerifiableAddressId], fields: [identity_verification_codes.identity_verifiable_address_id],
references: [identityVerifiableAddresses.id], references: [identity_verifiable_addresses.id],
}), }),
selfserviceVerificationFlow: one(selfserviceVerificationFlows, { selfserviceVerificationFlow: one(selfservice_verification_flows, {
fields: [identityVerificationCodes.selfserviceVerificationFlowId], fields: [identity_verification_codes.selfservice_verification_flow_id],
references: [selfserviceVerificationFlows.id], references: [selfservice_verification_flows.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [identityVerificationCodes.nid], fields: [identity_verification_codes.nid],
references: [networks.id], references: [networks.id],
}), }),
})); }));
export const courierMessageDispatchesRelations = relations(courierMessageDispatches, ({ one }) => ({ export const courierMessageDispatchesRelations = relations(courier_message_dispatches, ({ one }) => ({
courierMessage: one(courierMessages, { courierMessage: one(courier_messages, {
fields: [courierMessageDispatches.messageId], fields: [courier_message_dispatches.message_id],
references: [courierMessages.id], references: [courier_messages.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [courierMessageDispatches.nid], fields: [courier_message_dispatches.nid],
references: [networks.id], references: [networks.id],
}), }),
})); }));
export const identityLoginCodesRelations = relations(identityLoginCodes, ({ one }) => ({ export const identityLoginCodesRelations = relations(identity_login_codes, ({ one }) => ({
selfserviceLoginFlow: one(selfserviceLoginFlows, { selfserviceLoginFlow: one(selfservice_login_flows, {
fields: [identityLoginCodes.selfserviceLoginFlowId], fields: [identity_login_codes.selfservice_login_flow_id],
references: [selfserviceLoginFlows.id], references: [selfservice_login_flows.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [identityLoginCodes.nid], fields: [identity_login_codes.nid],
references: [networks.id], references: [networks.id],
}), }),
identity: one(identities, { identity: one(identities, {
fields: [identityLoginCodes.identityId], fields: [identity_login_codes.identity_id],
references: [identities.id], references: [identities.id],
}), }),
})); }));
export const identityRegistrationCodesRelations = relations(identityRegistrationCodes, ({ one }) => ({ export const identityRegistrationCodesRelations = relations(identity_registration_codes, ({ one }) => ({
selfserviceRegistrationFlow: one(selfserviceRegistrationFlows, { selfserviceRegistrationFlow: one(selfservice_registration_flows, {
fields: [identityRegistrationCodes.selfserviceRegistrationFlowId], fields: [identity_registration_codes.selfservice_registration_flow_id],
references: [selfserviceRegistrationFlows.id], references: [selfservice_registration_flows.id],
}), }),
network: one(networks, { network: one(networks, {
fields: [identityRegistrationCodes.nid], fields: [identity_registration_codes.nid],
references: [networks.id], references: [networks.id],
}), }),
})); }));

View file

@ -1,40 +1,21 @@
import { getHydraMetadataApi, getKetoMetadataApi, getKratosMetadataApi } from '@/ory/sdk/server'; import { StatusCard } from '@/components/status-card';
import { MetadataApiReady, 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() { export default async function RootPage() {
const kratosMetadataApi = await getKratosMetadataApi(); const session = await requireSession();
const kratosVersion = await kratosMetadataApi.getVersion() const identityId = session.identity!.id;
.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;
});
await requirePermission(permission.stack.dashboard, relation.access, identityId);
const hydraMetadataApi = await getHydraMetadataApi(); const pmAccessStackStatus = await checkPermission(permission.stack.status, relation.access, identityId);
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 kratos = pmAccessStackStatus && await kratosMetadata();
const hydra = pmAccessStackStatus && await hydraMetadata();
const keto = pmAccessStackStatus && await ketoMetadata();
return ( return (
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
@ -43,25 +24,46 @@ export default async function RootPage() {
<p className="text-lg font-light">See the list of all applications in your stack</p> <p className="text-lg font-light">See the list of all applications in your stack</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatusCard {
title="Ory Kratos" !pmAccessStackStatus && (
version={kratosVersion} <InsufficientPermission
name="Kratos" permission={permission.stack.status}
status={kratosStatus} relation="access"
className="flex-1"/> identityId={identityId}
<StatusCard classNames="col-span-1 md:col-span-4"
title="Ory Hydra" />
version={hydraVersion} )
name="Hydra" }
status={hydraStatus} {
className="flex-1"/> kratos && (
<StatusCard <StatusCard
title="Ory Keto" title="Ory Kratos"
version={ketoVersion} version={kratos.version}
name="Keto" name="Kratos"
status={ketoStatus} status={kratos.status}
className="flex-1"/> className="flex-1"/>
<div className="flex-1"/> )
}
{
hydra && (
<StatusCard
title="Ory Hydra"
version={hydra.version}
name="Hydra"
status={hydra.status}
className="flex-1"/>
)
}
{
keto && (
<StatusCard
title="Ory Keto"
version={keto.version}
name="Keto"
status={keto.status}
className="flex-1"/>
)
}
</div> </div>
</div> </div>
); );

View file

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { getIdentityApi } from '@/ory/sdk/server';
import { ErrorDisplay } from '@/components/error'; import { ErrorDisplay } from '@/components/error';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { IdentityTraits } from '@/components/identity/identity-traits'; import { IdentityTraits } from '@/components/identity/identity-traits';
@ -11,6 +10,11 @@ import { Badge } from '@/components/ui/badge';
import { Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
import { IdentityActions } from '@/components/identity/identity-actions'; import { IdentityActions } from '@/components/identity/identity-actions';
import { IdentityCredentials } from '@/components/identity/identity-credentials'; 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 { interface MergedAddress {
recovery_id?: string; recovery_id?: string;
@ -76,172 +80,229 @@ function mergeAddresses(
export default async function UserDetailsPage({ params }: { params: Promise<{ id: string }> }) { 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(); await requirePermission(permission.stack.dashboard, relation.access, identityId);
const identity = await identityApi.getIdentity({ id: identityId })
.then((response) => {
console.log('identity', response.data);
return response.data;
})
.catch(() => {
console.log('Identity not found');
});
const sessions = await identityApi.listIdentitySessions({ id: identityId }) const pmAccessUser = await checkPermission(permission.user.it, relation.access, identityId);
.then((response) => response.data) if (!pmAccessUser) {
.catch(() => { return redirect('/user');
console.log('No sessions found');
});
if (!identity) {
return <ErrorDisplay
title="Identity not found"
message={`The requested identity with id ${identityId} does not exist`}/>;
} }
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 <ErrorDisplay
title="Identity not found"
message={`The requested identity with id ${detailIdentityId} does not exist`}/>;
}
if (!detailIdentity.verifiable_addresses || !detailIdentity.verifiable_addresses[0]) {
return <ErrorDisplay return <ErrorDisplay
title="No verifiable adress" title="No verifiable adress"
message="The identity you are trying to see exists but has no identifiable address"/>; message="The identity you are trying to see exists but has no identifiable address"/>;
} }
const identitySchema = await identityApi const detailIdentitySessions = pmAccessUserSession && await listIdentitySessions(detailIdentityId);
.getIdentitySchema({ id: identity.schema_id })
.then((response) => response.data as KratosSchema); const detailIdentitySchema = await getIdentitySchema(detailIdentity.schema_id)
.then((response) => response as KratosSchema);
const addresses = mergeAddresses( const addresses = mergeAddresses(
identity.recovery_addresses ?? [], detailIdentity.recovery_addresses ?? [],
identity.verifiable_addresses ?? [], detailIdentity.verifiable_addresses ?? [],
); );
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<p className="text-3xl font-bold leading-tight tracking-tight">{addresses[0].value}</p> <p className="text-3xl font-bold leading-tight tracking-tight">{addresses[0].value}</p>
<p className="text-lg font-light">{identity.id}</p> <p className="text-lg font-light">{detailIdentity.id}</p>
</div> </div>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4"> <div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
<Card className="row-span-3"> {
<CardHeader> pmAccessUserTrait ?
<CardTitle>Traits</CardTitle> <Card className="row-span-3">
<CardDescription>All identity properties specified in the identity schema</CardDescription> <CardHeader>
</CardHeader> <CardTitle>Traits</CardTitle>
<CardContent> <CardDescription>All identity properties specified in the identity
<IdentityTraits schema={identitySchema} identity={identity}/> schema</CardDescription>
</CardContent> </CardHeader>
</Card> <CardContent>
<IdentityTraits
schema={detailIdentitySchema}
identity={detailIdentity}
disabled={!pmEditUserTraits}
/>
</CardContent>
</Card>
:
<InsufficientPermission
permission={permission.user.trait}
relation={relation.access}
identityId={identityId}
classNames="row-span-3"/>
}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Actions</CardTitle> <CardTitle>Actions</CardTitle>
<CardDescription>Quick actions to manage the identity</CardDescription> <CardDescription>Quick actions to manage the identity</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<IdentityActions identity={identity}/> <IdentityActions
identity={detailIdentity}
permissions={{
pmDeleteUser,
pmEditUserState,
pmDeleteUserSession,
pmCreateUserCode,
pmCreateUserLink,
}}
/>
</CardContent> </CardContent>
</Card> </Card>
<Card> {
<CardHeader> pmAccessUserAddress ?
<CardTitle>Addresses</CardTitle> <Card>
<CardDescription>All linked addresses for verification and recovery</CardDescription> <CardHeader>
</CardHeader> <CardTitle>Addresses</CardTitle>
<CardContent> <CardDescription>All linked addresses for verification and recovery</CardDescription>
</CardHeader>
<CardContent>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Type</TableHead> <TableHead>Type</TableHead>
<TableHead>Value</TableHead> <TableHead>Value</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{ {
addresses.map((address) => { addresses.map((address) => {
return ( return (
<TableRow key={address.value}> <TableRow key={address.value}>
<TableCell>{address.value}</TableCell> <TableCell>{address.value}</TableCell>
<TableCell>{address.via}</TableCell> <TableCell>{address.via}</TableCell>
<TableCell> <TableCell>
{address.verifiable_id && {address.verifiable_id &&
<Badge className="m-1 space-x-1"> <Badge className="m-1 space-x-1">
<span>Verifiable</span> <span>Verifiable</span>
{ {
address.verified ? address.verified ?
<Check className="h-3 w-3"/> <Check className="h-3 w-3"/>
: :
<X className="h-3 w-3"/> <X className="h-3 w-3"/>
}
</Badge>
} }
</Badge> {address.recovery_id &&
} <Badge className="m-1">Recovery</Badge>
{address.recovery_id && }
<Badge className="m-1">Recovery</Badge> </TableCell>
} </TableRow>
</TableCell> );
</TableRow> })
); }
}) </TableBody>
} </Table>
</TableBody> </CardContent>
</Table> </Card>
</CardContent> :
</Card> <InsufficientPermission
<Card> permission={permission.user.address}
<CardHeader> relation={relation.access}
<CardTitle>Credentials</CardTitle> identityId={identityId}/>
<CardDescription>All authentication mechanisms registered with this identity</CardDescription> }
</CardHeader> {
<CardContent className="space-y-4"> pmAccessUserCredential ?
<IdentityCredentials identity={identity}/> <Card>
</CardContent> <CardHeader>
</Card> <CardTitle>Credentials</CardTitle>
<Card> <CardDescription>All authentication mechanisms registered with this
<CardHeader> identity</CardDescription>
<CardTitle>Sessions</CardTitle> </CardHeader>
<CardDescription>See and manage all sessions of this identity</CardDescription> <CardContent className="space-y-4">
</CardHeader> <IdentityCredentials identity={detailIdentity}/>
<CardContent> </CardContent>
<Table> </Card>
<TableHeader> :
<TableRow> <InsufficientPermission
<TableHead>OS</TableHead> permission={permission.user.credential}
<TableHead>Browser</TableHead> relation={relation.access}
<TableHead>Active since</TableHead> identityId={identityId}/>
</TableRow> }
</TableHeader> {
<TableBody> pmAccessUserSession ?
<Card>
<CardHeader>
<CardTitle>Sessions</CardTitle>
<CardDescription>See and manage all sessions of this identity</CardDescription>
</CardHeader>
<CardContent>
{ {
sessions ? detailIdentitySessions ?
sessions.map((session) => { <Table>
<TableHeader>
const device = session.devices![0]; <TableRow>
const parser = new UAParser(device.user_agent); <TableHead>OS</TableHead>
const result = parser.getResult(); <TableHead>Browser</TableHead>
<TableHead>Active since</TableHead>
return (
<TableRow key={session.id}>
<TableCell className="space-x-1">
<span>{result.os.name}</span>
<span
className="text-xs text-neutral-500">{result.os.version}</span>
</TableCell>
<TableCell className="space-x-1">
<span>{result.browser.name}</span>
<span
className="text-xs text-neutral-500">{result.browser.version}</span>
</TableCell>
<TableCell>
{new Date(session.authenticated_at!).toLocaleString()}
</TableCell>
</TableRow> </TableRow>
); </TableHeader>
}) <TableBody>
{
detailIdentitySessions.map((session) => {
const device = session.devices![0];
const parser = new UAParser(device.user_agent);
const result = parser.getResult();
return (
<TableRow key={session.id}>
<TableCell className="space-x-1">
<span>{result.os.name}</span>
<span
className="text-xs text-neutral-500">{result.os.version}</span>
</TableCell>
<TableCell className="space-x-1">
<span>{result.browser.name}</span>
<span
className="text-xs text-neutral-500">{result.browser.version}</span>
</TableCell>
<TableCell>
{new Date(session.authenticated_at!).toLocaleString()}
</TableCell>
</TableRow>
);
})
}
</TableBody>
</Table>
: :
<ErrorDisplay title="No sessions" message=""/> <p>This user has no active sessions</p>
} }
</TableBody> </CardContent>
</Table> </Card>
</CardContent> :
</Card> <InsufficientPermission
permission={permission.user.session}
relation={relation.access}
identityId={identityId}/>
}
</div> </div>
</div> </div>
); );

View file

@ -33,9 +33,15 @@ interface IdentityDataTableProps {
data: Identity[]; data: Identity[];
page: number; page: number;
query: string; 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<Identity>[] = [ const columns: ColumnDef<Identity>[] = [
{ {
@ -137,6 +143,7 @@ export function IdentityDataTable({ data, page, query }: IdentityDataTableProps)
setCurrentIdentity(identity); setCurrentIdentity(identity);
setIdentitySessionVisible(true); setIdentitySessionVisible(true);
}} }}
disabled={!permission.pmDeleteUserSession}
className="flex items-center space-x-2 text-red-500"> className="flex items-center space-x-2 text-red-500">
<UserMinus className="h-4 w-4"/> <UserMinus className="h-4 w-4"/>
<span>Delete sessions</span> <span>Delete sessions</span>
@ -148,6 +155,7 @@ export function IdentityDataTable({ data, page, query }: IdentityDataTableProps)
setCurrentIdentity(identity); setCurrentIdentity(identity);
setBlockIdentityVisible(true); setBlockIdentityVisible(true);
}} }}
disabled={!permission.pmEditUserState}
className="flex items-center space-x-2 text-red-500"> className="flex items-center space-x-2 text-red-500">
<UserX className="h-4 w-4"/> <UserX className="h-4 w-4"/>
<span>Block identity</span> <span>Block identity</span>
@ -160,6 +168,7 @@ export function IdentityDataTable({ data, page, query }: IdentityDataTableProps)
setCurrentIdentity(identity); setCurrentIdentity(identity);
setUnblockIdentityVisible(true); setUnblockIdentityVisible(true);
}} }}
disabled={!permission.pmEditUserState}
className="flex items-center space-x-2 text-red-500"> className="flex items-center space-x-2 text-red-500">
<UserCheck className="h-4 w-4"/> <UserCheck className="h-4 w-4"/>
<span>Unblock identity</span> <span>Unblock identity</span>
@ -170,6 +179,7 @@ export function IdentityDataTable({ data, page, query }: IdentityDataTableProps)
setCurrentIdentity(identity); setCurrentIdentity(identity);
setDeleteIdentityVisible(true); setDeleteIdentityVisible(true);
}} }}
disabled={!permission.pmDeleteUser}
className="flex items-center space-x-2 text-red-500"> className="flex items-center space-x-2 text-red-500">
<Trash className="h-4 w-4"/> <Trash className="h-4 w-4"/>
<span>Delete identity</span> <span>Delete identity</span>

View file

@ -3,6 +3,9 @@ import { IdentityDataTable } from '@/app/(inside)/user/data-table';
import { SearchInput } from '@/components/search-input'; import { SearchInput } from '@/components/search-input';
import { queryIdentities } from '@/lib/action/identity'; import { queryIdentities } from '@/lib/action/identity';
import { IdentityPagination } from '@/components/pagination'; 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( 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 params = await searchParams;
const page = params.page ? Number(params.page) : 1; const page = params.page ? Number(params.page) : 1;
@ -20,7 +34,7 @@ export default async function UserPage(
let pageSize = 50; let pageSize = 50;
let paginationRange = 11; let paginationRange = 11;
const { data, itemCount, pageCount } = await queryIdentities({ page, pageSize, query }); const users = pmAccessUser && await queryIdentities(page, pageSize, query);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -31,23 +45,45 @@ export default async function UserPage(
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<SearchInput {
value={query} !pmAccessUser && (
pageParamKey="page" <InsufficientPermission
queryParamKey="query" permission={permission.user.it}
placeholder="Search for addresses and traits"/> relation={relation.access}
<div> identityId={identityId}
<p className="text-xs text-neutral-500">{itemCount} item{itemCount && itemCount > 1 ? 's' : ''} found</p> />
<IdentityDataTable )
data={data} }
page={page} {
query={query}/> pmAccessUser && users && (
</div> <>
<IdentityPagination <SearchInput
page={page} value={query}
pageCount={pageCount} pageParamKey="page"
pageParamKey="page" queryParamKey="query"
paginationRange={paginationRange}/> placeholder="Search for addresses and traits"/>
<div>
<p className="text-xs text-neutral-500">{users.itemCount} item{users.itemCount && users.itemCount > 1 ? 's' : ''} found</p>
<IdentityDataTable
data={users.data}
page={page}
query={query}
permission={{
pmEditUser: pmEditUser,
pmDeleteUser: pmDeleteUser,
pmEditUserState: pmEditUserState,
pmDeleteUserSession: pmDeleteUserSession,
}}
/>
</div>
<IdentityPagination
page={page}
pageCount={users.pageCount}
pageParamKey="page"
paginationRange={paginationRange}/>
</>
)
}
</div> </div>
</div> </div>
); );

View file

@ -15,6 +15,7 @@ interface DynamicFormProps<T extends FieldValues> {
onValid: SubmitHandler<T>, onValid: SubmitHandler<T>,
onInvalid: SubmitErrorHandler<T>, onInvalid: SubmitErrorHandler<T>,
submitLabel?: string, submitLabel?: string,
disabled?: boolean,
} }
export function DynamicForm<T extends FieldValues>( export function DynamicForm<T extends FieldValues>(
@ -25,6 +26,7 @@ export function DynamicForm<T extends FieldValues>(
onValid, onValid,
onInvalid, onInvalid,
submitLabel, submitLabel,
disabled,
}: DynamicFormProps<T>, }: DynamicFormProps<T>,
) { ) {
@ -48,7 +50,7 @@ export function DynamicForm<T extends FieldValues>(
key={fullFieldName} key={fullFieldName}
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2 space-y-0"> <FormItem className="flex items-center space-x-2 space-y-0">
<Checkbox {...field} checked={field.value}/> <Checkbox {...field} disabled={disabled} checked={field.value}/>
<FormLabel>{key}</FormLabel> <FormLabel>{key}</FormLabel>
</FormItem> </FormItem>
)} )}
@ -65,7 +67,7 @@ export function DynamicForm<T extends FieldValues>(
<FormItem> <FormItem>
<FormLabel>{value.title}</FormLabel> <FormLabel>{value.title}</FormLabel>
<FormControl> <FormControl>
<Input placeholder={value.title} {...field} /> <Input placeholder={value.title} {...field} disabled={disabled}/>
</FormControl> </FormControl>
<FormDescription>{value.description}</FormDescription> <FormDescription>{value.description}</FormDescription>
</FormItem> </FormItem>
@ -87,7 +89,7 @@ export function DynamicForm<T extends FieldValues>(
<Button <Button
key="submit" key="submit"
type="submit" type="submit"
disabled={!form.formState.isDirty} disabled={!form.formState.isDirty || disabled}
> >
{submitLabel ?? 'Submit'} {submitLabel ?? 'Submit'}
</Button> </Button>

View file

@ -28,12 +28,21 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
interface IdentityActionProps { 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 router = useRouter();
const [dialogVisible, setDialogVisible] = useState(false); 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?" dialogDescription="Are you sure you want to create a recovery code for this identity?"
dialogButtonSubmit="Create code" dialogButtonSubmit="Create code"
> >
<Button className="mr-2" size="icon"> <Button
disabled={!permissions.pmCreateUserCode}
className="mr-2"
size="icon">
<Key className="h-4"/> <Key className="h-4"/>
</Button> </Button>
</ConfirmationDialogWrapper> </ConfirmationDialogWrapper>
@ -142,7 +154,10 @@ export function IdentityActions({ identity }: IdentityActionProps,
dialogDescription="Are you sure you want to create a recovery link for this identity?" dialogDescription="Are you sure you want to create a recovery link for this identity?"
dialogButtonSubmit="Create link" dialogButtonSubmit="Create link"
> >
<Button className="mr-2" size="icon"> <Button
disabled={!permissions.pmCreateUserLink}
className="mr-2"
size="icon">
<Link className="h-4"/> <Link className="h-4"/>
</Button> </Button>
</ConfirmationDialogWrapper> </ConfirmationDialogWrapper>
@ -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!" 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" dialogButtonSubmit="Deactivate"
> >
<Button className="mr-2" size="icon"> <Button
disabled={!permissions.pmEditUserState}
className="mr-2"
size="icon">
<UserX className="h-4"/> <UserX className="h-4"/>
</Button> </Button>
</ConfirmationDialogWrapper> </ConfirmationDialogWrapper>
@ -176,7 +194,10 @@ export function IdentityActions({ identity }: IdentityActionProps,
dialogDescription="Are you sure you want to activate this identity?" dialogDescription="Are you sure you want to activate this identity?"
dialogButtonSubmit="Activate" dialogButtonSubmit="Activate"
> >
<Button className="mr-2" size="icon"> <Button
disabled={!permissions.pmEditUserState}
className="mr-2"
size="icon">
<UserCheck className="h-4"/> <UserCheck className="h-4"/>
</Button> </Button>
</ConfirmationDialogWrapper> </ConfirmationDialogWrapper>
@ -194,7 +215,10 @@ export function IdentityActions({ identity }: IdentityActionProps,
dialogButtonSubmit="Invalidate sessions" dialogButtonSubmit="Invalidate sessions"
dialogButtonSubmitProps={{ variant: 'destructive' }} dialogButtonSubmitProps={{ variant: 'destructive' }}
> >
<Button className="mr-2" size="icon"> <Button
disabled={!permissions.pmDeleteUserSession}
className="mr-2"
size="icon">
<UserMinus className="h-4"/> <UserMinus className="h-4"/>
</Button> </Button>
</ConfirmationDialogWrapper> </ConfirmationDialogWrapper>
@ -214,7 +238,10 @@ export function IdentityActions({ identity }: IdentityActionProps,
dialogButtonSubmit="Delete identity" dialogButtonSubmit="Delete identity"
dialogButtonSubmitProps={{ variant: 'destructive' }} dialogButtonSubmitProps={{ variant: 'destructive' }}
> >
<Button className="mr-2" size="icon"> <Button
disabled={!permissions.pmDeleteUser}
className="mr-2"
size="icon">
<Trash className="h-4"/> <Trash className="h-4"/>
</Button> </Button>
</ConfirmationDialogWrapper> </ConfirmationDialogWrapper>

View file

@ -36,7 +36,7 @@ export function IdentityCredentials({ identity }: IdentityCredentialsProps) {
( (
<ConfirmationDialogWrapper <ConfirmationDialogWrapper
onSubmit={async () => { onSubmit={async () => {
deleteIdentityCredential({ id: identity.id, type: key as never }) deleteIdentityCredential(identity.id, key as never)
.then(() => toast.success(`Credential ${key} deleted`)) .then(() => toast.success(`Credential ${key} deleted`))
.catch(() => toast.error(`Deleting credential ${key} failed`)); .catch(() => toast.error(`Deleting credential ${key} failed`));
}} }}

View file

@ -16,9 +16,10 @@ import { useState } from 'react';
interface IdentityTraitFormProps { interface IdentityTraitFormProps {
schema: KratosSchema; schema: KratosSchema;
identity: Identity; identity: Identity;
disabled: boolean;
} }
export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) { export function IdentityTraits({ schema, identity, disabled }: IdentityTraitFormProps) {
const [currentIdentity, setCurrentIdentity] = useState(identity); const [currentIdentity, setCurrentIdentity] = useState(identity);
@ -47,16 +48,16 @@ export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) {
delete traits['metadata_public']; delete traits['metadata_public'];
delete traits['metadata_admin']; delete traits['metadata_admin'];
updateIdentity({ updateIdentity(
id: currentIdentity.id, currentIdentity.id,
body: { {
schema_id: currentIdentity.schema_id, schema_id: currentIdentity.schema_id,
state: currentIdentity.state!, state: currentIdentity.state!,
traits: traits, traits: traits,
metadata_public: data.metadata_public, metadata_public: data.metadata_public,
metadata_admin: data.metadata_admin, metadata_admin: data.metadata_admin,
}, },
}) )
.then((identity) => { .then((identity) => {
setCurrentIdentity(identity); setCurrentIdentity(identity);
toast.success('Identity updated'); toast.success('Identity updated');
@ -74,10 +75,11 @@ export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) {
return ( return (
<DynamicForm <DynamicForm
form={form} form={form}
disabled={disabled}
properties={schema.properties.traits.properties} properties={schema.properties.traits.properties}
onValid={onValid} onValid={onValid}
onInvalid={onInvalid} onInvalid={onInvalid}
submitLabel="Update Identity" submitLabel={disabled ? 'Insufficient permissions' : 'Update Identity'}
> >
<FormField <FormField
{...form.register('metadata_public')} {...form.register('metadata_public')}
@ -86,7 +88,7 @@ export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) {
<FormItem> <FormItem>
<FormLabel>Public Metadata</FormLabel> <FormLabel>Public Metadata</FormLabel>
<FormControl> <FormControl>
<Textarea placeholder="Public Metadata" {...field} /> <Textarea placeholder="Public Metadata" {...field} disabled={disabled}/>
</FormControl> </FormControl>
<FormDescription>This has to be valid JSON</FormDescription> <FormDescription>This has to be valid JSON</FormDescription>
</FormItem> </FormItem>
@ -99,7 +101,7 @@ export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) {
<FormItem> <FormItem>
<FormLabel>Admin Metadata</FormLabel> <FormLabel>Admin Metadata</FormLabel>
<FormControl> <FormControl>
<Textarea placeholder="Admin Metadata" {...field} /> <Textarea placeholder="Admin Metadata" {...field} disabled={disabled}/>
</FormControl> </FormControl>
<FormDescription>This has to be valid JSON</FormDescription> <FormDescription>This has to be valid JSON</FormDescription>
</FormItem> </FormItem>

View file

@ -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 (
<Card className={classNames}>
<CardHeader>
<CardTitle>Insufficient Permission</CardTitle>
<CardDescription>
You are missing the permission to see this content.<br/>
If you think this is an error, please contact your system administrator
</CardDescription>
</CardHeader>
<CardFooter>
<CardDescription className="text-xs">
Permission: {permission}<br/>
Relation: {relation}<br/>
Identity: {identityId}
</CardDescription>
</CardFooter>
</Card>
);
}

View file

@ -50,7 +50,7 @@ export function StatusCard({ title, version, name, status, className }: StatusCa
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{ {
status.errors.map((error) => <span>{error}</span>) status.errors.map((error) => <span key={error}>{error}</span>)
} }
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View file

@ -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;
}

View file

@ -12,14 +12,43 @@ import {
import { getDB } from '@/db'; import { getDB } from '@/db';
import { identities, identity_recovery_addresses, identity_verifiable_addresses } from '@/db/schema'; import { identities, identity_recovery_addresses, identity_verifiable_addresses } from '@/db/schema';
import { eq, ilike, or, sql } from 'drizzle-orm'; import { eq, ilike, or, sql } from 'drizzle-orm';
import { checkPermission, requireSession } from '@/lib/action/authentication';
import { permission, relation } from '@/lib/permission';
interface QueryIdentitiesProps { export async function getIdentity(id: string) {
page: number,
pageSize: number, const session = await requireSession();
query?: string, 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) { if (page < 1 || pageSize < 1) {
return { return {
@ -73,13 +102,13 @@ export async function queryIdentities({ page, pageSize, query }: QueryIdentities
}; };
} }
export async function updateIdentity(id: string, body: UpdateIdentityBody) {
interface UpdatedIdentityProps { const session = await requireSession();
id: string; const allowed = await checkPermission(permission.user.it, relation.edit, session.identity!.id);
body: UpdateIdentityBody; if (!allowed) {
} throw Error('Unauthorised');
}
export async function updateIdentity({ id, body }: UpdatedIdentityProps) {
const identityApi = await getIdentityApi(); const identityApi = await getIdentityApi();
const { data } = await identityApi.updateIdentity({ const { data } = await identityApi.updateIdentity({
@ -94,12 +123,13 @@ export async function updateIdentity({ id, body }: UpdatedIdentityProps) {
return data; return data;
} }
interface DeleteIdentityCredentialProps { export async function deleteIdentityCredential(id: string, type: DeleteIdentityCredentialsTypeEnum) {
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 identityApi = await getIdentityApi();
const { data } = await identityApi.deleteIdentityCredentials({ id, type }); const { data } = await identityApi.deleteIdentityCredentials({ id, type });
@ -111,8 +141,30 @@ export async function deleteIdentityCredential({ id, type }: DeleteIdentityCrede
return data; 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) { 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 identityApi = await getIdentityApi();
const { data } = await identityApi.deleteIdentitySessions({ id }); const { data } = await identityApi.deleteIdentitySessions({ id });
@ -125,6 +177,12 @@ export async function deleteIdentitySessions(id: string) {
export async function createRecoveryCode(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 identityApi = await getIdentityApi();
const { data } = await identityApi.createRecoveryCodeForIdentity({ const { data } = await identityApi.createRecoveryCodeForIdentity({
createRecoveryCodeForIdentityBody: { createRecoveryCodeForIdentityBody: {
@ -139,6 +197,12 @@ export async function createRecoveryCode(id: string) {
export async function createRecoveryLink(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 identityApi = await getIdentityApi();
const { data } = await identityApi.createRecoveryLinkForIdentity({ const { data } = await identityApi.createRecoveryLinkForIdentity({
createRecoveryLinkForIdentityBody: { createRecoveryLinkForIdentityBody: {
@ -153,6 +217,12 @@ export async function createRecoveryLink(id: string) {
export async function blockIdentity(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 identityApi = await getIdentityApi();
const { data } = await identityApi.patchIdentity({ const { data } = await identityApi.patchIdentity({
id, id,
@ -172,6 +242,12 @@ export async function blockIdentity(id: string) {
export async function unblockIdentity(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 identityApi = await getIdentityApi();
const { data } = await identityApi.patchIdentity({ const { data } = await identityApi.patchIdentity({
id, id,
@ -191,6 +267,12 @@ export async function unblockIdentity(id: string) {
export async function deleteIdentity(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 identityApi = await getIdentityApi();
const { data } = await identityApi.deleteIdentity({ id }); const { data } = await identityApi.deleteIdentity({ id });

View file

@ -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,
};
}

View file

@ -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',
};

View file

@ -1,47 +1,29 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers'; import { checkPermission, getSession } from '@/lib/action/authentication';
import { getFrontendApi, getPermissionApi } from '@/ory/sdk/server'; import { permission, relation } from '@/lib/permission';
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
const frontendApi = await getFrontendApi(); // middleware can not work with requireSession, requireRole and
const cookie = await cookies(); // requirePermission due to the different redirect mechanisms in use!
const session = await frontendApi
.toSession({ cookie: 'ory_kratos_session=' + cookie.get('ory_kratos_session')?.value })
.then((response) => response.data)
.catch(() => null);
const session = await getSession();
if (!session) { if (!session) {
console.log('NO SESSION'); console.log('middleware', 'MISSING SESSION');
const url = process.env.NEXT_PUBLIC_AUTHENTICATION_NODE_URL + const url = process.env.NEXT_PUBLIC_AUTHENTICATION_NODE_URL +
'/flow/login?return_to=' + '/flow/login?return_to=' +
process.env.NEXT_PUBLIC_DASHBOARD_NODE_URL; process.env.NEXT_PUBLIC_DASHBOARD_NODE_URL;
console.log('REDIRECT TO', url); console.log('middleware', 'REDIRECT TO', url);
return NextResponse.redirect(url!);
return NextResponse.redirect(url);
} }
const permissionApi = await getPermissionApi(); const allowed = await checkPermission(permission.stack.dashboard, relation.access, session.identity!.id);
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;
});
if (isAdmin) {
if (allowed) {
if (request.nextUrl.pathname === '/unauthorised') { if (request.nextUrl.pathname === '/unauthorised') {
return redirect('/', 'HAS PERMISSION BUT ACCESSING /unauthorized'); return redirect('/', 'HAS PERMISSION BUT ACCESSING /unauthorized');
} }
@ -55,9 +37,9 @@ export async function middleware(request: NextRequest) {
} }
function redirect(path: string, reason: string) { function redirect(path: string, reason: string) {
console.log(reason); console.log('middleware', reason);
const url = `${process.env.NEXT_PUBLIC_DASHBOARD_NODE_URL}${path}`; 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!); return NextResponse.redirect(url!);
} }

View file

@ -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 # Ory Hydra CLI and writes the client id and
# client secret to the command line. # client secret to the command line.

View file

@ -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 # Ory Hydra CLI and uses the client to start
# the Ory Hydra test application. # the Ory Hydra test application.

View file

@ -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 <object> <relation> <role> <role_relation>"
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)"

View file

@ -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 <object> <relation> <identity_id>"
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"

View file

@ -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

View file

@ -22,6 +22,8 @@ dsn: postgres://postgres:postgres@ory-postgres:5432/keto?sslmode=disable&max_con
namespaces: namespaces:
- id: 0 - id: 0
name: roles name: roles
- id: 1
name: permissions
serve: serve:
read: read: