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 {
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],
}),
}));

View file

@ -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 (
<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>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{
!pmAccessStackStatus && (
<InsufficientPermission
permission={permission.stack.status}
relation="access"
identityId={identityId}
classNames="col-span-1 md:col-span-4"
/>
)
}
{
kratos && (
<StatusCard
title="Ory Kratos"
version={kratosVersion}
version={kratos.version}
name="Kratos"
status={kratosStatus}
status={kratos.status}
className="flex-1"/>
)
}
{
hydra && (
<StatusCard
title="Ory Hydra"
version={hydraVersion}
version={hydra.version}
name="Hydra"
status={hydraStatus}
status={hydra.status}
className="flex-1"/>
)
}
{
keto && (
<StatusCard
title="Ory Keto"
version={ketoVersion}
version={keto.version}
name="Keto"
status={ketoStatus}
status={keto.status}
className="flex-1"/>
<div className="flex-1"/>
)
}
</div>
</div>
);

View file

@ -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,70 +80,102 @@ 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 <ErrorDisplay
title="Identity not found"
message={`The requested identity with id ${identityId} does not exist`}/>;
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 <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
title="No verifiable adress"
message="The identity you are trying to see exists but has no identifiable address"/>;
}
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 (
<div className="space-y-4">
<div>
<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 className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{
pmAccessUserTrait ?
<Card className="row-span-3">
<CardHeader>
<CardTitle>Traits</CardTitle>
<CardDescription>All identity properties specified in the identity schema</CardDescription>
<CardDescription>All identity properties specified in the identity
schema</CardDescription>
</CardHeader>
<CardContent>
<IdentityTraits schema={identitySchema} identity={identity}/>
<IdentityTraits
schema={detailIdentitySchema}
identity={detailIdentity}
disabled={!pmEditUserTraits}
/>
</CardContent>
</Card>
:
<InsufficientPermission
permission={permission.user.trait}
relation={relation.access}
identityId={identityId}
classNames="row-span-3"/>
}
<Card>
<CardHeader>
<CardTitle>Actions</CardTitle>
<CardDescription>Quick actions to manage the identity</CardDescription>
</CardHeader>
<CardContent>
<IdentityActions identity={identity}/>
<IdentityActions
identity={detailIdentity}
permissions={{
pmDeleteUser,
pmEditUserState,
pmDeleteUserSession,
pmCreateUserCode,
pmCreateUserLink,
}}
/>
</CardContent>
</Card>
{
pmAccessUserAddress ?
<Card>
<CardHeader>
<CardTitle>Addresses</CardTitle>
@ -185,21 +221,40 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id
</Table>
</CardContent>
</Card>
:
<InsufficientPermission
permission={permission.user.address}
relation={relation.access}
identityId={identityId}/>
}
{
pmAccessUserCredential ?
<Card>
<CardHeader>
<CardTitle>Credentials</CardTitle>
<CardDescription>All authentication mechanisms registered with this identity</CardDescription>
<CardDescription>All authentication mechanisms registered with this
identity</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<IdentityCredentials identity={identity}/>
<IdentityCredentials identity={detailIdentity}/>
</CardContent>
</Card>
:
<InsufficientPermission
permission={permission.user.credential}
relation={relation.access}
identityId={identityId}/>
}
{
pmAccessUserSession ?
<Card>
<CardHeader>
<CardTitle>Sessions</CardTitle>
<CardDescription>See and manage all sessions of this identity</CardDescription>
</CardHeader>
<CardContent>
{
detailIdentitySessions ?
<Table>
<TableHeader>
<TableRow>
@ -210,8 +265,7 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id
</TableHeader>
<TableBody>
{
sessions ?
sessions.map((session) => {
detailIdentitySessions.map((session) => {
const device = session.devices![0];
const parser = new UAParser(device.user_agent);
@ -235,13 +289,20 @@ export default async function UserDetailsPage({ params }: { params: Promise<{ id
</TableRow>
);
})
:
<ErrorDisplay title="No sessions" message=""/>
}
</TableBody>
</Table>
:
<p>This user has no active sessions</p>
}
</CardContent>
</Card>
:
<InsufficientPermission
permission={permission.user.session}
relation={relation.access}
identityId={identityId}/>
}
</div>
</div>
);

View file

@ -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<Identity>[] = [
{
@ -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">
<UserMinus className="h-4 w-4"/>
<span>Delete sessions</span>
@ -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">
<UserX className="h-4 w-4"/>
<span>Block identity</span>
@ -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">
<UserCheck className="h-4 w-4"/>
<span>Unblock identity</span>
@ -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">
<Trash className="h-4 w-4"/>
<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 { 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 (
<div className="space-y-4">
@ -31,23 +45,45 @@ export default async function UserPage(
</p>
</div>
<div className="space-y-2">
{
!pmAccessUser && (
<InsufficientPermission
permission={permission.user.it}
relation={relation.access}
identityId={identityId}
/>
)
}
{
pmAccessUser && users && (
<>
<SearchInput
value={query}
pageParamKey="page"
queryParamKey="query"
placeholder="Search for addresses and traits"/>
<div>
<p className="text-xs text-neutral-500">{itemCount} item{itemCount && itemCount > 1 ? 's' : ''} found</p>
<p className="text-xs text-neutral-500">{users.itemCount} item{users.itemCount && users.itemCount > 1 ? 's' : ''} found</p>
<IdentityDataTable
data={data}
data={users.data}
page={page}
query={query}/>
query={query}
permission={{
pmEditUser: pmEditUser,
pmDeleteUser: pmDeleteUser,
pmEditUserState: pmEditUserState,
pmDeleteUserSession: pmDeleteUserSession,
}}
/>
</div>
<IdentityPagination
page={page}
pageCount={pageCount}
pageCount={users.pageCount}
pageParamKey="page"
paginationRange={paginationRange}/>
</>
)
}
</div>
</div>
);

View file

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

View file

@ -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"
>
<Button className="mr-2" size="icon">
<Button
disabled={!permissions.pmCreateUserCode}
className="mr-2"
size="icon">
<Key className="h-4"/>
</Button>
</ConfirmationDialogWrapper>
@ -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"
>
<Button className="mr-2" size="icon">
<Button
disabled={!permissions.pmCreateUserLink}
className="mr-2"
size="icon">
<Link className="h-4"/>
</Button>
</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!"
dialogButtonSubmit="Deactivate"
>
<Button className="mr-2" size="icon">
<Button
disabled={!permissions.pmEditUserState}
className="mr-2"
size="icon">
<UserX className="h-4"/>
</Button>
</ConfirmationDialogWrapper>
@ -176,7 +194,10 @@ export function IdentityActions({ identity }: IdentityActionProps,
dialogDescription="Are you sure you want to activate this identity?"
dialogButtonSubmit="Activate"
>
<Button className="mr-2" size="icon">
<Button
disabled={!permissions.pmEditUserState}
className="mr-2"
size="icon">
<UserCheck className="h-4"/>
</Button>
</ConfirmationDialogWrapper>
@ -194,7 +215,10 @@ export function IdentityActions({ identity }: IdentityActionProps,
dialogButtonSubmit="Invalidate sessions"
dialogButtonSubmitProps={{ variant: 'destructive' }}
>
<Button className="mr-2" size="icon">
<Button
disabled={!permissions.pmDeleteUserSession}
className="mr-2"
size="icon">
<UserMinus className="h-4"/>
</Button>
</ConfirmationDialogWrapper>
@ -214,7 +238,10 @@ export function IdentityActions({ identity }: IdentityActionProps,
dialogButtonSubmit="Delete identity"
dialogButtonSubmitProps={{ variant: 'destructive' }}
>
<Button className="mr-2" size="icon">
<Button
disabled={!permissions.pmDeleteUser}
className="mr-2"
size="icon">
<Trash className="h-4"/>
</Button>
</ConfirmationDialogWrapper>

View file

@ -36,7 +36,7 @@ export function IdentityCredentials({ identity }: IdentityCredentialsProps) {
(
<ConfirmationDialogWrapper
onSubmit={async () => {
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`));
}}

View file

@ -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 (
<DynamicForm
form={form}
disabled={disabled}
properties={schema.properties.traits.properties}
onValid={onValid}
onInvalid={onInvalid}
submitLabel="Update Identity"
submitLabel={disabled ? 'Insufficient permissions' : 'Update Identity'}
>
<FormField
{...form.register('metadata_public')}
@ -86,7 +88,7 @@ export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) {
<FormItem>
<FormLabel>Public Metadata</FormLabel>
<FormControl>
<Textarea placeholder="Public Metadata" {...field} />
<Textarea placeholder="Public Metadata" {...field} disabled={disabled}/>
</FormControl>
<FormDescription>This has to be valid JSON</FormDescription>
</FormItem>
@ -99,7 +101,7 @@ export function IdentityTraits({ schema, identity }: IdentityTraitFormProps) {
<FormItem>
<FormLabel>Admin Metadata</FormLabel>
<FormControl>
<Textarea placeholder="Admin Metadata" {...field} />
<Textarea placeholder="Admin Metadata" {...field} disabled={disabled}/>
</FormControl>
<FormDescription>This has to be valid JSON</FormDescription>
</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>
<TooltipContent>
{
status.errors.map((error) => <span>{error}</span>)
status.errors.map((error) => <span key={error}>{error}</span>)
}
</TooltipContent>
</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 { 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');
}
export async function queryIdentities({ page, pageSize, query }: QueryIdentitiesProps) {
const identityApi = await getIdentityApi();
const { data } = await identityApi.getIdentity({ id });
console.log('Got identity', data);
return data;
}
export async function getIdentitySchema(id: string) {
const identityApi = await getIdentityApi();
const { data } = await identityApi.getIdentitySchema({ id: id });
console.log('Got identity schema');
return data;
}
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,14 +102,14 @@ export async function queryIdentities({ page, pageSize, query }: QueryIdentities
};
}
export async function updateIdentity(id: string, body: UpdateIdentityBody) {
interface UpdatedIdentityProps {
id: string;
body: UpdateIdentityBody;
const session = await requireSession();
const allowed = await checkPermission(permission.user.it, relation.edit, session.identity!.id);
if (!allowed) {
throw Error('Unauthorised');
}
export async function updateIdentity({ id, body }: UpdatedIdentityProps) {
const identityApi = await getIdentityApi();
const { data } = await identityApi.updateIdentity({
id: id,
@ -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 });

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 { 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!);
}

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
# 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
# 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:
- id: 0
name: roles
- id: 1
name: permissions
serve:
read: