From ce2d8ed1239d6d832742e1ea741d39444db28aae Mon Sep 17 00:00:00 2001 From: John Livingston Date: Thu, 18 Apr 2024 20:16:44 +0200 Subject: [PATCH] Possibility to configure an OpenID Connect provider on the instance level WIP (#128) Pruning external users periodically. --- ...mod_http_peertubelivechat_manage_users.lua | 29 +++++++++++- server/lib/debug.ts | 3 ++ server/lib/external-auth/oidc.ts | 39 ++++++++++++++++ server/lib/prosody/api/manage-users.ts | 45 ++++++++++++++++++- 4 files changed, 113 insertions(+), 3 deletions(-) diff --git a/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua b/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua index 32ee4446..b323fb08 100644 --- a/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua +++ b/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua @@ -7,6 +7,8 @@ module:depends"http"; local module_host = module:get_host(); -- this module is not global +local bare_sessions = prosody.bare_sessions; + local vcards = module:open_store("vcard"); function check_auth(routes) @@ -113,10 +115,35 @@ local function ensure_user(event) }); end --- TODO: add a function to prune user that have not logged in since X days. + +local function prune_users(event) -- delete all users that are not connected! + local request, response = event.request, event.response; + event.response.headers["Content-Type"] = "application/json"; + + module:log("info", "Calling prune_users for host %s", module_host); + + for user in usermanager.users(module_host) do + -- has the user still open sessions? + if (bare_sessions[user..'@'..module_host] ~= nil) then + module:log("debug", "User %s on host %s has still active sessions, ignoring.", user, module_host); + else + -- FIXME: there is a little chance that we delete a user that is currently connecting... + -- As this prune should not be called too often, we can consider it is not an issue for now. + + module:log("debug", "Deleting user %s on host %s", user, module_host); + update_vcard(user, nil); + usermanager.delete_user(user, module_host); + end + end + + return json.encode({ + result = "ok"; + }); +end module:provides("http", { route = check_auth { ["POST /" .. module_host .. "/ensure-user"] = ensure_user; + ["POST /" .. module_host .. "/prune-users"] = prune_users; }; }); diff --git a/server/lib/debug.ts b/server/lib/debug.ts index bb4063ed..95a2cff9 100644 --- a/server/lib/debug.ts +++ b/server/lib/debug.ts @@ -19,6 +19,7 @@ interface DebugContent { alwaysPublishXMPPRoom?: boolean enablePodcastChatTagForNonLive?: boolean mucAdmins?: string[] + externalAccountPruneInterval?: number } type DebugNumericValue = 'renewCertCheckInterval' @@ -26,6 +27,7 @@ type DebugNumericValue = 'renewCertCheckInterval' | 'logRotateEvery' | 'logRotateCheckInterval' | 'remoteServerInfosMaxAge' +| 'externalAccountPruneInterval' type DebugBooleanValue = 'alwaysPublishXMPPRoom' | 'enablePodcastChatTagForNonLive' | 'useOpenSSL' @@ -65,6 +67,7 @@ function _readDebugFile (options: RegisterServerOptions): DebugContent | false { debugContent.alwaysPublishXMPPRoom = json.always_publish_xmpp_room === true debugContent.enablePodcastChatTagForNonLive = json.enable_podcast_chat_tag_for_nonlive === true debugContent.mucAdmins = _getJIDs(options, json, 'muc_admins') + debugContent.externalAccountPruneInterval = _getNumericOptions(options, json, 'external_account_prune_interval') } catch (err) { logger.error('Failed to read the debug_mode file content:', err) } diff --git a/server/lib/external-auth/oidc.ts b/server/lib/external-auth/oidc.ts index 39084fec..d5433ae3 100644 --- a/server/lib/external-auth/oidc.ts +++ b/server/lib/external-auth/oidc.ts @@ -5,7 +5,9 @@ import { ExternalAuthenticationError } from './error' import { getBaseRouterRoute } from '../helpers' import { canonicalizePluginUri } from '../uri/canonicalize' import { getProsodyDomain } from '../prosody/config/domain' +import { pruneUsers } from '../prosody/api/manage-users' import { getProsodyFilePaths } from '../prosody/config' +import { debugNumericParameter } from '../debug' import { createCipheriv, createDecipheriv, randomBytes, Encoding } from 'node:crypto' import { Issuer, BaseClient, generators, UnknownObject } from 'openid-client' import { JID } from '@xmpp/jid' @@ -80,6 +82,7 @@ class ExternalAuthOIDC { private readonly externalVirtualhost: string private readonly avatarsDir: string private readonly avatarsFiles: string[] + private pruneTimer?: NodeJS.Timer private readonly encryptionOptions = { algorithm: 'aes256' as string, @@ -619,11 +622,45 @@ class ExternalAuthOIDC { } } + /** + * Starts an interval timer to prune external users from Prosody. + * @param options Peertube server options. + */ + public startPruneTimer (options: RegisterServerOptions): void { + this.stopPruneTimer() // just in case... + + // every 4 hour (every minutes in debug mode) + const pruneInterval = debugNumericParameter(options, 'externalAccountPruneInterval', 60 * 1000, 4 * 60 * 60 * 1000) + this.logger.info(`Creating a timer for external account pruning, every ${Math.round(pruneInterval / 1000)}s.`) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.pruneTimer = setInterval(async () => { + try { + if (!await this.isOk()) { return } + + this.logger.info('Pruning external users...') + await pruneUsers(options) + } catch (err) { + this.logger.error('Error while pruning external users: ' + (err as string)) + } + }, pruneInterval) + } + + /** + * Stops the prune timer. + */ + public stopPruneTimer (): void { + if (!this.pruneTimer) { return } + clearInterval(this.pruneTimer) + this.pruneTimer = undefined + } + /** * frees the singleton */ public static async destroySingleton (): Promise { if (!singleton) { return } + singleton.stopPruneTimer() singleton = undefined } @@ -663,6 +700,8 @@ class ExternalAuthOIDC { avatarsFiles: prosodyFilePaths.avatarsFiles }) + singleton.startPruneTimer(options) + return singleton } diff --git a/server/lib/prosody/api/manage-users.ts b/server/lib/prosody/api/manage-users.ts index bb60181f..92176001 100644 --- a/server/lib/prosody/api/manage-users.ts +++ b/server/lib/prosody/api/manage-users.ts @@ -62,6 +62,47 @@ async function ensureUser (options: RegisterServerOptions, infos: ExternalAccoun return true } -export { - ensureUser +/** + * Calls an API provided by mod_http_peertubelivechat_manage_users, to prune unused users. + * @param options Peertube server options + * @throws Error + */ +async function pruneUsers (options: RegisterServerOptions): Promise { + const logger = options.peertubeHelpers.logger + + const currentProsody = getCurrentProsody() + if (!currentProsody) { + throw new Error('It seems that prosody is not binded... Cant call API.') + } + + const prosodyDomain = await getProsodyDomain(options) + + logger.info('Calling pruneUsers') + + // Requesting on localhost, because currentProsody.host does not always resolves correctly (docker use case, ...) + const apiUrl = `http://localhost:${currentProsody.port}/` + + 'peertubelivechat_manage_users/' + + `external.${prosodyDomain}/` + // the virtual host name + 'prune-users' + + try { + logger.debug('Calling prune-users API on url: ' + apiUrl) + await got(apiUrl, { + method: 'POST', + headers: { + authorization: 'Bearer ' + await getAPIKey(options), + host: currentProsody.host + }, + json: {}, + responseType: 'json', + resolveBodyOnly: true + }) + } catch (err) { + logger.error(`prune-users failed: ' ${err as string}`) + } +} + +export { + ensureUser, + pruneUsers }