diff --git a/apps/meteor/app/cas/server/cas_rocketchat.js b/apps/meteor/app/cas/server/cas_rocketchat.js deleted file mode 100644 index f0b62b6ccb8..00000000000 --- a/apps/meteor/app/cas/server/cas_rocketchat.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Logger } from '@rocket.chat/logger'; -import { ServiceConfiguration } from 'meteor/service-configuration'; - -import { settings } from '../../settings/server'; - -export const logger = new Logger('CAS'); - -let timer; - -async function updateServices(/* record*/) { - if (typeof timer !== 'undefined') { - clearTimeout(timer); - } - - timer = setTimeout(async () => { - const data = { - // These will pe passed to 'node-cas' as options - enabled: settings.get('CAS_enabled'), - base_url: settings.get('CAS_base_url'), - login_url: settings.get('CAS_login_url'), - // Rocketchat Visuals - buttonLabelText: settings.get('CAS_button_label_text'), - buttonLabelColor: settings.get('CAS_button_label_color'), - buttonColor: settings.get('CAS_button_color'), - width: settings.get('CAS_popup_width'), - height: settings.get('CAS_popup_height'), - autoclose: settings.get('CAS_autoclose'), - }; - - // Either register or deregister the CAS login service based upon its configuration - if (data.enabled) { - logger.info('Enabling CAS login service'); - await ServiceConfiguration.configurations.upsertAsync({ service: 'cas' }, { $set: data }); - } else { - logger.info('Disabling CAS login service'); - await ServiceConfiguration.configurations.removeAsync({ service: 'cas' }); - } - }, 2000); -} - -settings.watchByRegex(/^CAS_.+/, async (key, value) => { - await updateServices(value); -}); diff --git a/apps/meteor/app/cas/server/cas_server.js b/apps/meteor/app/cas/server/cas_server.js deleted file mode 100644 index 60880c77d4f..00000000000 --- a/apps/meteor/app/cas/server/cas_server.js +++ /dev/null @@ -1,272 +0,0 @@ -import url from 'url'; - -import { validate } from '@rocket.chat/cas-validate'; -import { CredentialTokens, Rooms, Users } from '@rocket.chat/models'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; -import { RoutePolicy } from 'meteor/routepolicy'; -import { WebApp } from 'meteor/webapp'; -import _ from 'underscore'; - -import { createRoom } from '../../lib/server/functions/createRoom'; -import { _setRealName } from '../../lib/server/functions/setRealName'; -import { settings } from '../../settings/server'; -import { logger } from './cas_rocketchat'; - -RoutePolicy.declare('/_cas/', 'network'); - -const closePopup = function (res) { - res.writeHead(200, { 'Content-Type': 'text/html' }); - const content = ''; - res.end(content, 'utf-8'); -}; - -const casTicket = function (req, token, callback) { - // get configuration - if (!settings.get('CAS_enabled')) { - logger.error('Got ticket validation request, but CAS is not enabled'); - callback(); - } - - // get ticket and validate. - const parsedUrl = url.parse(req.url, true); - const ticketId = parsedUrl.query.ticket; - const baseUrl = settings.get('CAS_base_url'); - const cas_version = parseFloat(settings.get('CAS_version')); - const appUrl = Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; - logger.debug(`Using CAS_base_url: ${baseUrl}`); - - validate( - { - base_url: baseUrl, - version: cas_version, - service: `${appUrl}/_cas/${token}`, - }, - ticketId, - async (err, status, username, details) => { - if (err) { - logger.error(`error when trying to validate: ${err.message}`); - } else if (status) { - logger.info(`Validated user: ${username}`); - const user_info = { username }; - - // CAS 2.0 attributes handling - if (details && details.attributes) { - _.extend(user_info, { attributes: details.attributes }); - } - await CredentialTokens.create(token, user_info); - } else { - logger.error(`Unable to validate ticket: ${ticketId}`); - } - // logger.debug("Received response: " + JSON.stringify(details, null , 4)); - - callback(); - }, - ); -}; - -const middleware = function (req, res, next) { - // Make sure to catch any exceptions because otherwise we'd crash - // the runner - try { - const barePath = req.url.substring(0, req.url.indexOf('?')); - const splitPath = barePath.split('/'); - - // Any non-cas request will continue down the default - // middlewares. - if (splitPath[1] !== '_cas') { - next(); - return; - } - - // get auth token - const credentialToken = splitPath[2]; - if (!credentialToken) { - closePopup(res); - return; - } - - // validate ticket - casTicket(req, credentialToken, () => { - closePopup(res); - }); - } catch (err) { - logger.error({ msg: 'Unexpected error', err }); - closePopup(res); - } -}; - -// Listen to incoming OAuth http requests -WebApp.connectHandlers.use((req, res, next) => { - middleware(req, res, next); -}); - -/* - * Register a server-side login handle. - * It is call after Accounts.callLoginMethod() is call from client. - * - */ -Accounts.registerLoginHandler('cas', async (options) => { - if (!options.cas) { - return undefined; - } - - // TODO: Sync wrapper due to the chain conversion to async models - const credentials = await CredentialTokens.findOneNotExpiredById(options.cas.credentialToken); - if (credentials === undefined) { - throw new Meteor.Error(Accounts.LoginCancelledError.numericError, 'no matching login attempt found'); - } - - const result = credentials.userInfo; - const syncUserDataFieldMap = settings.get('CAS_Sync_User_Data_FieldMap').trim(); - const cas_version = parseFloat(settings.get('CAS_version')); - const sync_enabled = settings.get('CAS_Sync_User_Data_Enabled'); - const trustUsername = settings.get('CAS_trust_username'); - const verified = settings.get('Accounts_Verify_Email_For_External_Accounts'); - const userCreationEnabled = settings.get('CAS_Creation_User_Enabled'); - - // We have these - const ext_attrs = { - username: result.username, - }; - - // We need these - const int_attrs = { - email: undefined, - name: undefined, - username: undefined, - rooms: undefined, - }; - - // Import response attributes - if (cas_version >= 2.0) { - // Clean & import external attributes - _.each(result.attributes, (value, ext_name) => { - if (value) { - ext_attrs[ext_name] = value[0]; - } - }); - } - - // Source internal attributes - if (syncUserDataFieldMap) { - // Our mapping table: key(int_attr) -> value(ext_attr) - // Spoken: Source this internal attribute from these external attributes - const attr_map = JSON.parse(syncUserDataFieldMap); - - _.each(attr_map, (source, int_name) => { - // Source is our String to interpolate - if (source && typeof source.valueOf() === 'string') { - let replacedValue = source; - _.each(ext_attrs, (value, ext_name) => { - replacedValue = replacedValue.replace(`%${ext_name}%`, ext_attrs[ext_name]); - }); - - if (source !== replacedValue) { - int_attrs[int_name] = replacedValue; - logger.debug(`Sourced internal attribute: ${int_name} = ${replacedValue}`); - } else { - logger.debug(`Sourced internal attribute: ${int_name} skipped.`); - } - } - }); - } - - // Search existing user by its external service id - logger.debug(`Looking up user by id: ${result.username}`); - // First, look for a user that has logged in from CAS with this username before - let user = await Users.findOne({ 'services.cas.external_id': result.username }); - if (!user) { - // If that user was not found, check if there's any Rocket.Chat user with that username - // With this, CAS login will continue to work if the user is renamed on both sides and also if the user is renamed only on Rocket.Chat. - // It'll also allow non-CAS users to switch to CAS based login - if (trustUsername) { - const username = new RegExp(`^${result.username}$`, 'i'); - user = await Users.findOne({ username }); - if (user) { - // Update the user's external_id to reflect this new username. - await Users.updateOne({ _id: user._id }, { $set: { 'services.cas.external_id': result.username } }); - } - } - } - - if (user) { - logger.debug(`Using existing user for '${result.username}' with id: ${user._id}`); - if (sync_enabled) { - logger.debug('Syncing user attributes'); - // Update name - if (int_attrs.name) { - await _setRealName(user._id, int_attrs.name); - } - - // Update email - if (int_attrs.email) { - await Users.updateOne({ _id: user._id }, { $set: { emails: [{ address: int_attrs.email, verified }] } }); - } - } - } else if (userCreationEnabled) { - // Define new user - const newUser = { - username: result.username, - active: true, - globalRoles: ['user'], - emails: [], - services: { - cas: { - external_id: result.username, - version: cas_version, - attrs: int_attrs, - }, - }, - }; - - // Add username - if (int_attrs.username) { - _.extend(newUser, { - username: int_attrs.username, - }); - } - - // Add User.name - if (int_attrs.name) { - _.extend(newUser, { - name: int_attrs.name, - }); - } - - // Add email - if (int_attrs.email) { - _.extend(newUser, { - emails: [{ address: int_attrs.email, verified }], - }); - } - - // Create the user - logger.debug(`User "${result.username}" does not exist yet, creating it`); - const userId = Accounts.insertUserDoc({}, newUser); - - // Fetch and use it - user = await Users.findOneById(userId); - logger.debug(`Created new user for '${result.username}' with id: ${user._id}`); - // logger.debug(JSON.stringify(user, undefined, 4)); - - logger.debug(`Joining user to attribute channels: ${int_attrs.rooms}`); - if (int_attrs.rooms) { - const roomNames = int_attrs.rooms.split(','); - for await (const roomName of roomNames) { - if (roomName) { - let room = await Rooms.findOneByNameAndType(roomName, 'c'); - if (!room) { - room = await createRoom('c', roomName, user); - } - } - } - } - } else { - // Should fail as no user exist and can't be created - logger.debug(`User "${result.username}" does not exist yet, will fail as no user creation is enabled`); - throw new Meteor.Error(Accounts.LoginCancelledError.numericError, 'no matching user account found'); - } - - return { userId: user._id }; -}); diff --git a/apps/meteor/app/cas/server/index.ts b/apps/meteor/app/cas/server/index.ts deleted file mode 100644 index 0ad22d77b19..00000000000 --- a/apps/meteor/app/cas/server/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './cas_rocketchat'; -import './cas_server'; diff --git a/apps/meteor/server/configuration/cas.ts b/apps/meteor/server/configuration/cas.ts new file mode 100644 index 00000000000..7a82f141dfe --- /dev/null +++ b/apps/meteor/server/configuration/cas.ts @@ -0,0 +1,35 @@ +import type { Awaited } from '@rocket.chat/core-typings'; +import debounce from 'lodash.debounce'; +import { RoutePolicy } from 'meteor/routepolicy'; +import { WebApp } from 'meteor/webapp'; + +import { settings } from '../../app/settings/server/cached'; +import { loginHandlerCAS } from '../lib/cas/loginHandler'; +import { middlewareCAS } from '../lib/cas/middleware'; +import { updateCasServices } from '../lib/cas/updateCasService'; + +const _updateCasServices = debounce(updateCasServices, 2000); + +settings.watchByRegex(/^CAS_.+/, async () => { + await _updateCasServices(); +}); + +RoutePolicy.declare('/_cas/', 'network'); + +// Listen to incoming OAuth http requests +WebApp.connectHandlers.use((req, res, next) => { + middlewareCAS(req, res, next); +}); + +/* + * Register a server-side login handler. + * It is called after Accounts.callLoginMethod() is called from client. + * + */ +Accounts.registerLoginHandler('cas', (options) => { + const promise = loginHandlerCAS(options); + + // Pretend the promise has been awaited so the types will match - + // #TODO: Fix registerLoginHandler's type definitions (it accepts promises) + return promise as unknown as Awaited; +}); diff --git a/apps/meteor/server/importPackages.ts b/apps/meteor/server/importPackages.ts index d92e02f3503..2b4e3106ed4 100644 --- a/apps/meteor/server/importPackages.ts +++ b/apps/meteor/server/importPackages.ts @@ -6,7 +6,6 @@ import '../app/assets/server'; import '../app/authorization/server'; import '../app/autotranslate/server'; import '../app/bot-helpers/server'; -import '../app/cas/server'; import '../app/channel-settings/server'; import '../app/cloud/server'; import '../app/crowd/server'; diff --git a/apps/meteor/server/lib/cas/createNewUser.ts b/apps/meteor/server/lib/cas/createNewUser.ts new file mode 100644 index 00000000000..d04fa2d2249 --- /dev/null +++ b/apps/meteor/server/lib/cas/createNewUser.ts @@ -0,0 +1,63 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Rooms, Users } from '@rocket.chat/models'; +import { pick } from '@rocket.chat/tools'; +import { Accounts } from 'meteor/accounts-base'; + +import { createRoom } from '../../../app/lib/server/functions/createRoom'; +import { logger } from './logger'; + +type CASUserOptions = { + attributes: Record; + casVersion: number; + flagEmailAsVerified: boolean; +}; + +export const createNewUser = async (username: string, { attributes, casVersion, flagEmailAsVerified }: CASUserOptions): Promise => { + // Define new user + const newUser = { + username: attributes.username || username, + active: true, + globalRoles: ['user'], + emails: [attributes.email] + .filter((e) => e) + .map((address) => ({ + address, + verified: flagEmailAsVerified, + })), + services: { + cas: { + external_id: username, + version: casVersion, + attrs: attributes, + }, + }, + ...pick(attributes, 'name'), + }; + + // Create the user + logger.debug(`User "${username}" does not exist yet, creating it`); + const userId = Accounts.insertUserDoc({}, newUser); + + // Fetch and use it + const user = await Users.findOneById(userId); + if (!user) { + throw new Error('Unexpected error: Unable to find user after its creation.'); + } + + logger.debug(`Created new user for '${username}' with id: ${user._id}`); + + logger.debug(`Joining user to attribute channels: ${attributes.rooms}`); + if (attributes.rooms) { + const roomNames = attributes.rooms.split(','); + for await (const roomName of roomNames) { + if (roomName) { + let room = await Rooms.findOneByNameAndType(roomName, 'c'); + if (!room) { + room = await createRoom('c', roomName, user); + } + } + } + } + + return user; +}; diff --git a/apps/meteor/server/lib/cas/findExistingCASUser.ts b/apps/meteor/server/lib/cas/findExistingCASUser.ts new file mode 100644 index 00000000000..60b52965ee6 --- /dev/null +++ b/apps/meteor/server/lib/cas/findExistingCASUser.ts @@ -0,0 +1,27 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import { settings } from '../../../app/settings/server'; + +export const findExistingCASUser = async (username: string): Promise => { + const casUser = await Users.findOne({ 'services.cas.external_id': username }); + if (casUser) { + return casUser; + } + + if (!settings.get('CAS_trust_username')) { + return; + } + + // If that user was not found, check if there's any Rocket.Chat user with that username + // With this, CAS login will continue to work if the user is renamed on both sides and also if the user is renamed only on Rocket.Chat. + // It'll also allow non-CAS users to switch to CAS based login + // #TODO: Remove regex based search + const regex = new RegExp(`^${username}$`, 'i'); + const user = await Users.findOne({ regex }); + if (user) { + // Update the user's external_id to reflect this new username. + await Users.updateOne({ _id: user._id }, { $set: { 'services.cas.external_id': username } }); + return user; + } +}; diff --git a/apps/meteor/server/lib/cas/logger.ts b/apps/meteor/server/lib/cas/logger.ts new file mode 100644 index 00000000000..c2b4abe7a80 --- /dev/null +++ b/apps/meteor/server/lib/cas/logger.ts @@ -0,0 +1,3 @@ +import { Logger } from '@rocket.chat/logger'; + +export const logger = new Logger('CAS'); diff --git a/apps/meteor/server/lib/cas/loginHandler.ts b/apps/meteor/server/lib/cas/loginHandler.ts new file mode 100644 index 00000000000..80ce91350de --- /dev/null +++ b/apps/meteor/server/lib/cas/loginHandler.ts @@ -0,0 +1,121 @@ +import { CredentialTokens, Users } from '@rocket.chat/models'; +import { getObjectKeys, wrapExceptions } from '@rocket.chat/tools'; +import { Accounts } from 'meteor/accounts-base'; + +import { _setRealName } from '../../../app/lib/server/functions/setRealName'; +import { settings } from '../../../app/settings/server'; +import { createNewUser } from './createNewUser'; +import { findExistingCASUser } from './findExistingCASUser'; +import { logger } from './logger'; + +export const loginHandlerCAS = async (options: any): Promise => { + if (!options.cas) { + return undefined; + } + + // TODO: Sync wrapper due to the chain conversion to async models + const credentials = await CredentialTokens.findOneNotExpiredById(options.cas.credentialToken); + if (credentials === undefined || credentials === null) { + throw new Meteor.Error(Accounts.LoginCancelledError.numericError, 'no matching login attempt found'); + } + + const result = credentials.userInfo; + const syncUserDataFieldMap = settings.get('CAS_Sync_User_Data_FieldMap').trim(); + const casVersion = parseFloat(settings.get('CAS_version') ?? '1.0'); + const syncEnabled = settings.get('CAS_Sync_User_Data_Enabled'); + const flagEmailAsVerified = settings.get('Accounts_Verify_Email_For_External_Accounts'); + const userCreationEnabled = settings.get('CAS_Creation_User_Enabled'); + + const { username, attributes: credentialsAttributes } = result as { username: string; attributes: Record }; + + // We have these + const externalAttributes: Record = { + username, + }; + + // We need these + const internalAttributes: Record = { + email: undefined, + name: undefined, + username: undefined, + rooms: undefined, + }; + + // Import response attributes + if (casVersion >= 2.0) { + // Clean & import external attributes + for await (const [externalName, value] of Object.entries(credentialsAttributes)) { + if (value) { + externalAttributes[externalName] = value[0]; + } + } + } + + // Source internal attributes + if (syncUserDataFieldMap) { + // Our mapping table: key(int_attr) -> value(ext_attr) + // Spoken: Source this internal attribute from these external attributes + const attributeMap = wrapExceptions(() => JSON.parse(syncUserDataFieldMap) as Record).catch((err) => { + logger.error({ msg: 'Invalid JSON for attribute mapping', err }); + throw err; + }); + + for await (const [internalName, source] of Object.entries(attributeMap)) { + if (!source || typeof source.valueOf() !== 'string') { + continue; + } + + let replacedValue = source as string; + for await (const externalName of getObjectKeys(externalAttributes)) { + replacedValue = replacedValue.replace(`%${externalName}%`, externalAttributes[externalName]); + } + + if (source !== replacedValue) { + internalAttributes[internalName] = replacedValue; + logger.debug(`Sourced internal attribute: ${internalName} = ${replacedValue}`); + } else { + logger.debug(`Sourced internal attribute: ${internalName} skipped.`); + } + } + } + + // Search existing user by its external service id + logger.debug(`Looking up user by id: ${username}`); + // First, look for a user that has logged in from CAS with this username before + const user = await findExistingCASUser(username); + + if (user) { + logger.debug(`Using existing user for '${username}' with id: ${user._id}`); + if (syncEnabled) { + logger.debug('Syncing user attributes'); + // Update name + if (internalAttributes.name) { + await _setRealName(user._id, internalAttributes.name); + } + + // Update email + if (internalAttributes.email) { + await Users.updateOne( + { _id: user._id }, + { $set: { emails: [{ address: internalAttributes.email, verified: flagEmailAsVerified }] } }, + ); + } + } + + return { userId: user._id }; + } + + if (!userCreationEnabled) { + // Should fail as no user exist and can't be created + logger.debug(`User "${username}" does not exist yet, will fail as no user creation is enabled`); + throw new Meteor.Error(Accounts.LoginCancelledError.numericError, 'no matching user account found'); + } + + const newUser = await createNewUser(username, { + attributes: internalAttributes, + casVersion, + flagEmailAsVerified, + }); + + return { userId: newUser._id }; +}; diff --git a/apps/meteor/server/lib/cas/middleware.ts b/apps/meteor/server/lib/cas/middleware.ts new file mode 100644 index 00000000000..074177838f9 --- /dev/null +++ b/apps/meteor/server/lib/cas/middleware.ts @@ -0,0 +1,97 @@ +import type { IncomingMessage, ServerResponse } from 'http'; +import url from 'url'; + +import { validate } from '@rocket.chat/cas-validate'; +import type { ICredentialToken } from '@rocket.chat/core-typings'; +import { CredentialTokens } from '@rocket.chat/models'; +import _ from 'underscore'; + +import { settings } from '../../../app/settings/server'; +import { logger } from './logger'; + +const closePopup = function (res: ServerResponse): void { + res.writeHead(200, { 'Content-Type': 'text/html' }); + const content = ''; + res.end(content, 'utf-8'); +}; + +type IncomingMessageWithUrl = IncomingMessage & Required>; + +const casTicket = function (req: IncomingMessageWithUrl, token: string, callback: () => void): void { + // get configuration + if (!settings.get('CAS_enabled')) { + logger.error('Got ticket validation request, but CAS is not enabled'); + callback(); + } + + // get ticket and validate. + const parsedUrl = url.parse(req.url, true); + const ticketId = parsedUrl.query.ticket as string; + const baseUrl = settings.get('CAS_base_url'); + const version = parseFloat(settings.get('CAS_version') ?? '1.0') as 1.0 | 2.0; + const appUrl = Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; + logger.debug(`Using CAS_base_url: ${baseUrl}`); + + validate( + { + base_url: baseUrl, + version, + service: `${appUrl}/_cas/${token}`, + }, + ticketId, + async (err, status, username, details) => { + if (err) { + logger.error(`error when trying to validate: ${err.message}`); + } else if (status) { + logger.info(`Validated user: ${username}`); + const userInfo: Partial = { username: username as string }; + + // CAS 2.0 attributes handling + if (details?.attributes) { + _.extend(userInfo, { attributes: details.attributes }); + } + await CredentialTokens.create(token, userInfo); + } else { + logger.error(`Unable to validate ticket: ${ticketId}`); + } + // logger.debug("Received response: " + JSON.stringify(details, null , 4)); + + callback(); + }, + ); +}; + +export const middlewareCAS = function (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) { + // Make sure to catch any exceptions because otherwise we'd crash + // the runner + try { + if (!req.url) { + throw new Error('Invalid request url'); + } + + const barePath = req.url.substring(0, req.url.indexOf('?')); + const splitPath = barePath.split('/'); + + // Any non-cas request will continue down the default + // middlewares. + if (splitPath[1] !== '_cas') { + next(); + return; + } + + // get auth token + const credentialToken = splitPath[2]; + if (!credentialToken) { + closePopup(res); + return; + } + + // validate ticket + casTicket(req as IncomingMessageWithUrl, credentialToken, () => { + closePopup(res); + }); + } catch (err) { + logger.error({ msg: 'Unexpected error', err }); + closePopup(res); + } +}; diff --git a/apps/meteor/server/lib/cas/updateCasService.ts b/apps/meteor/server/lib/cas/updateCasService.ts new file mode 100644 index 00000000000..5583eda22f8 --- /dev/null +++ b/apps/meteor/server/lib/cas/updateCasService.ts @@ -0,0 +1,30 @@ +import type { LoginServiceConfiguration } from '@rocket.chat/core-typings'; +import { ServiceConfiguration } from 'meteor/service-configuration'; + +import { settings } from '../../../app/settings/server/cached'; +import { logger } from './logger'; + +export async function updateCasServices(): Promise { + const data: Partial = { + // These will pe passed to 'node-cas' as options + enabled: settings.get('CAS_enabled'), + base_url: settings.get('CAS_base_url'), + login_url: settings.get('CAS_login_url'), + // Rocketchat Visuals + buttonLabelText: settings.get('CAS_button_label_text'), + buttonLabelColor: settings.get('CAS_button_label_color'), + buttonColor: settings.get('CAS_button_color'), + width: settings.get('CAS_popup_width'), + height: settings.get('CAS_popup_height'), + autoclose: settings.get('CAS_autoclose'), + }; + + // Either register or deregister the CAS login service based upon its configuration + if (data.enabled) { + logger.info('Enabling CAS login service'); + await ServiceConfiguration.configurations.upsertAsync({ service: 'cas' }, { $set: data }); + } else { + logger.info('Disabling CAS login service'); + await ServiceConfiguration.configurations.removeAsync({ service: 'cas' }); + } +} diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index 91edaf1acc3..b26a48ee315 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -24,6 +24,7 @@ await import('../lib/oauthRedirectUriServer'); await import('./lib/pushConfig'); await import('./configuration/accounts_meld'); +await import('./configuration/cas'); await import('./configuration/ldap'); await import('./stream/stdout'); diff --git a/packages/cas-validate/src/validate.ts b/packages/cas-validate/src/validate.ts index bfb2e73af0b..cef47a50a23 100644 --- a/packages/cas-validate/src/validate.ts +++ b/packages/cas-validate/src/validate.ts @@ -13,15 +13,15 @@ export type CasOptions = { }; export type CasCallbackExtendedData = { - username?: unknown; - attributes?: unknown; + username?: string; + attributes?: Record; // eslint-disable-next-line @typescript-eslint/naming-convention - PGTIOU?: unknown; - ticket?: unknown; - proxies?: unknown; + PGTIOU?: string; + ticket?: string; + proxies?: string[]; }; -export type CasCallback = (err: any, status?: unknown, username?: unknown, extended?: CasCallbackExtendedData) => void; +export type CasCallback = (err: any, status?: unknown, username?: string, extended?: CasCallbackExtendedData) => void; function parseJasigAttributes(elemAttribute: Cheerio, cheerio: CheerioAPI): Record { // "Jasig Style" Attributes: diff --git a/packages/cas-validate/tsconfig.json b/packages/cas-validate/tsconfig.json index 26aeeb5e5cf..49c73da90c8 100644 --- a/packages/cas-validate/tsconfig.json +++ b/packages/cas-validate/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.base.server.json", "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], + "declaration": true, "rootDir": "./src", "outDir": "./dist" }, diff --git a/packages/tools/src/getObjectKeys.ts b/packages/tools/src/getObjectKeys.ts new file mode 100644 index 00000000000..00b0f4d1e10 --- /dev/null +++ b/packages/tools/src/getObjectKeys.ts @@ -0,0 +1 @@ +export const getObjectKeys = (object: T) => Object.keys(object) as (keyof T)[]; diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 6ec3e38d358..b1b53ab71a9 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -1,3 +1,4 @@ +export * from './getObjectKeys'; export * from './normalizeLanguage'; export * from './pick'; export * from './stream';