Possibility to configure an OpenID Connect provider on the instance level WIP (#128)

Pruning external users periodically.
This commit is contained in:
John Livingston 2024-04-18 20:16:44 +02:00
parent a9a0925ac0
commit ce2d8ed123
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
4 changed files with 113 additions and 3 deletions

View File

@ -7,6 +7,8 @@ module:depends"http";
local module_host = module:get_host(); -- this module is not global local module_host = module:get_host(); -- this module is not global
local bare_sessions = prosody.bare_sessions;
local vcards = module:open_store("vcard"); local vcards = module:open_store("vcard");
function check_auth(routes) function check_auth(routes)
@ -113,10 +115,35 @@ local function ensure_user(event)
}); });
end 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", { module:provides("http", {
route = check_auth { route = check_auth {
["POST /" .. module_host .. "/ensure-user"] = ensure_user; ["POST /" .. module_host .. "/ensure-user"] = ensure_user;
["POST /" .. module_host .. "/prune-users"] = prune_users;
}; };
}); });

View File

@ -19,6 +19,7 @@ interface DebugContent {
alwaysPublishXMPPRoom?: boolean alwaysPublishXMPPRoom?: boolean
enablePodcastChatTagForNonLive?: boolean enablePodcastChatTagForNonLive?: boolean
mucAdmins?: string[] mucAdmins?: string[]
externalAccountPruneInterval?: number
} }
type DebugNumericValue = 'renewCertCheckInterval' type DebugNumericValue = 'renewCertCheckInterval'
@ -26,6 +27,7 @@ type DebugNumericValue = 'renewCertCheckInterval'
| 'logRotateEvery' | 'logRotateEvery'
| 'logRotateCheckInterval' | 'logRotateCheckInterval'
| 'remoteServerInfosMaxAge' | 'remoteServerInfosMaxAge'
| 'externalAccountPruneInterval'
type DebugBooleanValue = 'alwaysPublishXMPPRoom' | 'enablePodcastChatTagForNonLive' | 'useOpenSSL' type DebugBooleanValue = 'alwaysPublishXMPPRoom' | 'enablePodcastChatTagForNonLive' | 'useOpenSSL'
@ -65,6 +67,7 @@ function _readDebugFile (options: RegisterServerOptions): DebugContent | false {
debugContent.alwaysPublishXMPPRoom = json.always_publish_xmpp_room === true debugContent.alwaysPublishXMPPRoom = json.always_publish_xmpp_room === true
debugContent.enablePodcastChatTagForNonLive = json.enable_podcast_chat_tag_for_nonlive === true debugContent.enablePodcastChatTagForNonLive = json.enable_podcast_chat_tag_for_nonlive === true
debugContent.mucAdmins = _getJIDs(options, json, 'muc_admins') debugContent.mucAdmins = _getJIDs(options, json, 'muc_admins')
debugContent.externalAccountPruneInterval = _getNumericOptions(options, json, 'external_account_prune_interval')
} catch (err) { } catch (err) {
logger.error('Failed to read the debug_mode file content:', err) logger.error('Failed to read the debug_mode file content:', err)
} }

View File

@ -5,7 +5,9 @@ import { ExternalAuthenticationError } from './error'
import { getBaseRouterRoute } from '../helpers' import { getBaseRouterRoute } from '../helpers'
import { canonicalizePluginUri } from '../uri/canonicalize' import { canonicalizePluginUri } from '../uri/canonicalize'
import { getProsodyDomain } from '../prosody/config/domain' import { getProsodyDomain } from '../prosody/config/domain'
import { pruneUsers } from '../prosody/api/manage-users'
import { getProsodyFilePaths } from '../prosody/config' import { getProsodyFilePaths } from '../prosody/config'
import { debugNumericParameter } from '../debug'
import { createCipheriv, createDecipheriv, randomBytes, Encoding } from 'node:crypto' import { createCipheriv, createDecipheriv, randomBytes, Encoding } from 'node:crypto'
import { Issuer, BaseClient, generators, UnknownObject } from 'openid-client' import { Issuer, BaseClient, generators, UnknownObject } from 'openid-client'
import { JID } from '@xmpp/jid' import { JID } from '@xmpp/jid'
@ -80,6 +82,7 @@ class ExternalAuthOIDC {
private readonly externalVirtualhost: string private readonly externalVirtualhost: string
private readonly avatarsDir: string private readonly avatarsDir: string
private readonly avatarsFiles: string[] private readonly avatarsFiles: string[]
private pruneTimer?: NodeJS.Timer
private readonly encryptionOptions = { private readonly encryptionOptions = {
algorithm: 'aes256' as string, 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 * frees the singleton
*/ */
public static async destroySingleton (): Promise<void> { public static async destroySingleton (): Promise<void> {
if (!singleton) { return } if (!singleton) { return }
singleton.stopPruneTimer()
singleton = undefined singleton = undefined
} }
@ -663,6 +700,8 @@ class ExternalAuthOIDC {
avatarsFiles: prosodyFilePaths.avatarsFiles avatarsFiles: prosodyFilePaths.avatarsFiles
}) })
singleton.startPruneTimer(options)
return singleton return singleton
} }

View File

@ -62,6 +62,47 @@ async function ensureUser (options: RegisterServerOptions, infos: ExternalAccoun
return true 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<void> {
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
} }