Skip to content

Commit

Permalink
feat(backend): delete user via authentik (#2258)
Browse files Browse the repository at this point in the history
* feat(backend): delete user via authentik

Motivation
----------
This will synchronize user deletions in Authentik to our own database.

How to test
-----------
1. `docker compose up` (important!)
2. Log into Authentik as admin
3. Create a new user via Directory>>Users
4. Impersonate as this user and change your password
5. Log out and log in as the new user
6. Visit http://localhost:3000 to create a user in our database via JWT
7. Log out and login in as admin
8. Delete the test user in the Authentik admin dashboard under Directory>>Users
9. Backend will receive the webhook and delete the user

* follow @Mogge's review

See: #2258 (review)

* follow @Mogge's review

see: #2258 (review)
  • Loading branch information
roschaefer authored Oct 8, 2024
1 parent 90d4b14 commit 05a8099
Show file tree
Hide file tree
Showing 29 changed files with 852 additions and 124 deletions.
48 changes: 48 additions & 0 deletions authentik/blueprints/dreammall/dreammallearth-webhooks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
version: 1
metadata:
name: Webhooks
context:
dreammall_backend_url: !Env [DREAMMALL_BACKEND_URL, "http://backend:4000"]
dreammall_webhook_secret: !Env [DREAMMALL_WEBHOOK_SECRET, ""]
entries:
- model: authentik_blueprints.metaapplyblueprint
attrs:
identifiers:
path: system/bootstrap.yaml
required: false
- model: authentik_events.notificationtransport
id: webhook_notification_transport
identifiers:
name: webhook_notification_transport
attrs:
mode: webhook
webhook_url: !Format ['%s/authentik-webhook?authorization=%s', !Context dreammall_backend_url, !Context dreammall_webhook_secret]
- model: authentik_core.group
id: group
state: created
identifiers:
name: authentik Admins

- model: authentik_policies_event_matcher.eventmatcherpolicy
id: delete_user_event_matcher
identifiers:
name: delete_user_event_matcher
attrs:
action: model_deleted
model: authentik_core.user
- model: authentik_events.notificationrule
id: webhook_notification
identifiers:
name: webhook_notification
attrs:
severity: "notice"
group: !KeyOf group
transports:
- !KeyOf webhook_notification_transport
- model: authentik_policies.policybinding
id: delete_user_event_matcher_binding
identifiers:
order: 0
policy: !KeyOf delete_user_event_matcher
target: !KeyOf webhook_notification
2 changes: 2 additions & 0 deletions authentik/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ x-shared-environments: &x-shared-environments
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
DREAMMALL_FRONTEND_URL: http://localhost:3000
DREAMMALL_PRESENTER_URL: http://localhost:3001
DREAMMALL_BACKEND_URL: http://backend:4000 # for webhook testing, backend must be started via docker compose
DREAMMALL_WEBHOOK_SECRET: 1547ccd9fd7b143aa7a76021648581163ffae58c4a558be0da2e9ae0131707fc
AUTHENTIK_BOOTSTRAP_PASSWORD: dreammall
AUTHENTIK_BOOTSTRAP_EMAIL: [email protected]

Expand Down
2 changes: 2 additions & 0 deletions backend/.env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ JWKS_URI="http://localhost:9000/application/o/dreammallearth/jwks/"

# SENTRY_DSN=
# SENTRY_ENVIRONMENT=localhost

# WEBHOOK_SECRET=1547ccd9fd7b143aa7a76021648581163ffae58c4a558be0da2e9ae0131707fc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- DropForeignKey
ALTER TABLE `UsersInMeetings` DROP FOREIGN KEY `UsersInMeetings_meetingId_fkey`;

-- DropForeignKey
ALTER TABLE `UsersInMeetings` DROP FOREIGN KEY `UsersInMeetings_userId_fkey`;

-- AddForeignKey
ALTER TABLE `UsersInMeetings` ADD CONSTRAINT `UsersInMeetings_meetingId_fkey` FOREIGN KEY (`meetingId`) REFERENCES `Meeting`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `UsersInMeetings` ADD CONSTRAINT `UsersInMeetings_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
4 changes: 2 additions & 2 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ model Meeting {
}

model UsersInMeetings {
meeting Meeting @relation(fields: [meetingId], references: [id])
meeting Meeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
meetingId Int
user User @relation(fields: [userId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
assignedAt DateTime @default(now())
role String @db.VarChar(12) @default("MODERATOR")
Expand Down
71 changes: 71 additions & 0 deletions backend/src/api/Authentik/deleteUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { leaveTable } from '#src/use-cases/leave-table'

import type { Dependencies } from '.'
import type { Request } from 'express'

type ModelDeleteUserBodyPayload = {
model: {
pk: number
app: string
name: string
model_name: string
}
http_request: {
args: Request<string, unknown>
path: string
method: string
request_id: string
user_agent: string
}
}

const isEvent = (body: string): ModelDeleteUserBodyPayload | undefined => {
const [eventType, ...rest] = body.split(': ')
if (eventType !== 'model_deleted') return
const payload = JSON.parse(rest.join(': ').replaceAll("'", '"')) as ModelDeleteUserBodyPayload
if (payload.model.model_name !== 'user') return
return payload
}

const handleEvent =
({ prisma, logger }: Dependencies) =>
async (authentikPayload: ModelDeleteUserBodyPayload) => {
logger.debug('payload', authentikPayload)
const {
model: { pk },
} = authentikPayload

const deletedUser = await prisma.user.findFirst({
where: { pk },
include: {
meetings: true,
},
})

if (!deletedUser) {
return
}

const { id: userId, meetingId } = deletedUser

const renameMeetingIdToTableId = (meeting: { userId: number; meetingId: number }) => ({
userId: meeting.userId,
tableId: meeting.meetingId,
})
await Promise.all(
deletedUser.meetings.map(renameMeetingIdToTableId).map(leaveTable({ prisma, logger })),
)

if (meetingId) {
await prisma.meeting.deleteMany({ where: { id: meetingId } })
}

await prisma.userDetail.deleteMany({ where: { userId } })
await prisma.socialMedia.deleteMany({ where: { userId } })
await prisma.user.deleteMany({ where: { pk } })

// TODO: why prisma.$transaction doesn't work here ?
// await prisma.$transaction([prisma.user.deleteMany({ where: { id: userId } })])
}

export const deleteUser = { isEvent, handleEvent }
Loading

0 comments on commit 05a8099

Please sign in to comment.