From 92df4bfbbbddeaf3a2cfcfd7afae10b7137ee80d Mon Sep 17 00:00:00 2001 From: damencho Date: Wed, 26 Mar 2025 13:18:30 -0500 Subject: [PATCH] feat: Backend reports default permissions. When any of the backend is used 'anonymous', 'jitsi-anonymous', 'internal_hashed', 'internal_plain', 'cyrus' and a participant becomes a moderator, because of external module or because set from jicofo we send to client with the self-presence about becoming moderator a default set of permissions which can be controlled via prosody config. If using 'token' authentication the above applies only if there is a token and the token does not contain context.features. --- conference.js | 10 + react/features/base/jwt/functions.ts | 16 +- react/features/chat/functions.ts | 3 +- react/features/chat/middleware.ts | 1 + react/features/invite/functions.ts | 11 +- react/features/polls/functions.ts | 3 +- react/features/recording/actions.any.ts | 10 +- .../LiveStream/AbstractLiveStreamButton.ts | 4 +- .../AbstractStartRecordingDialogContent.tsx | 5 +- react/features/recording/functions.ts | 14 +- react/features/recording/hooks.web.ts | 5 +- react/features/subtitles/middleware.ts | 5 +- .../toolbox/components/web/Toolbox.tsx | 2 +- react/features/toolbox/functions.any.ts | 4 - react/features/toolbox/functions.web.ts | 2 +- react/features/transcribing/functions.ts | 5 +- .../prosody-plugins/mod_filter_iq_rayo.lua | 40 ---- .../prosody-plugins/mod_jitsi_permissions.lua | 179 ++++++++++++++++++ .../prosody-plugins/mod_muc_meeting_id.lua | 2 + 19 files changed, 219 insertions(+), 102 deletions(-) create mode 100644 resources/prosody-plugins/mod_jitsi_permissions.lua diff --git a/conference.js b/conference.js index 951ce73b61..6918299bf5 100644 --- a/conference.js +++ b/conference.js @@ -1888,6 +1888,16 @@ export default { }, timeout); } ); + + room.on(JitsiConferenceEvents.PERMISSIONS_RECEIVED, p => { + const localParticipant = getLocalParticipant(APP.store.getState()); + + APP.store.dispatch(participantUpdated({ + id: localParticipant.id, + local: true, + features: p + })); + }); }, /** diff --git a/react/features/base/jwt/functions.ts b/react/features/base/jwt/functions.ts index 22c9b97797..fe0948edcd 100644 --- a/react/features/base/jwt/functions.ts +++ b/react/features/base/jwt/functions.ts @@ -45,14 +45,12 @@ export function getJwtName(state: IReduxState) { * * @param {IReduxState} state - The app state. * @param {string} feature - The feature we want to check. - * @param {boolean} ifNoToken - Default value if there is no token. * @param {boolean} ifNotInFeatures - Default value if features prop exists but does not have the {@code feature}. * @returns {boolean} */ export function isJwtFeatureEnabled( state: IReduxState, feature: ParticipantFeaturesKey, - ifNoToken: boolean, ifNotInFeatures: boolean ) { const { jwt } = state['features/base/jwt']; @@ -67,14 +65,12 @@ export function isJwtFeatureEnabled( jwt, localParticipantFeatures: features, feature, - ifNoToken, ifNotInFeatures }); } interface IIsJwtFeatureEnabledStatelessParams { feature: ParticipantFeaturesKey; - ifNoToken: boolean; ifNotInFeatures: boolean; jwt?: string; localParticipantFeatures?: IParticipantFeatures; @@ -86,7 +82,6 @@ interface IIsJwtFeatureEnabledStatelessParams { * @param {string | undefined} jwt - The jwt token. * @param {ILocalParticipant} localParticipantFeatures - The features of the local participant. * @param {string} feature - The feature we want to check. - * @param {boolean} ifNoToken - Default value if there is no token. * @param {boolean} ifNotInFeatures - Default value if features is missing * or prop exists but does not have the {@code feature}. * @returns {boolean} @@ -95,18 +90,9 @@ export function isJwtFeatureEnabledStateless({ jwt, localParticipantFeatures: features, feature, - ifNoToken, ifNotInFeatures }: IIsJwtFeatureEnabledStatelessParams) { - if (!jwt) { - return ifNoToken; - } - - if (typeof features === 'undefined') { - return ifNoToken; - } - - if (typeof features[feature] === 'undefined') { + if (!jwt || typeof features?.[feature] === 'undefined') { return ifNotInFeatures; } diff --git a/react/features/chat/functions.ts b/react/features/chat/functions.ts index 07b9fd78e5..e98099d7d6 100644 --- a/react/features/chat/functions.ts +++ b/react/features/chat/functions.ts @@ -7,6 +7,7 @@ import emojiAsciiAliases from 'react-emoji-render/data/asciiAliases'; import { IReduxState } from '../app/types'; import { getLocalizedDateFormatter } from '../base/i18n/dateUtil'; import i18next from '../base/i18n/i18next'; +import { MEET_FEATURES } from '../base/jwt/constants'; import { isJwtFeatureEnabled } from '../base/jwt/functions'; import { getParticipantById } from '../base/participants/functions'; import { escapeRegexp } from '../base/util/helpers'; @@ -206,5 +207,5 @@ export function isSendGroupChatDisabled(state: IReduxState) { return false; } - return !isJwtFeatureEnabled(state, 'send-groupchat', false, false); + return !isJwtFeatureEnabled(state, MEET_FEATURES.SEND_GROUPCHAT, false); } diff --git a/react/features/chat/middleware.ts b/react/features/chat/middleware.ts index 697d2755c9..5a4ee17a37 100644 --- a/react/features/chat/middleware.ts +++ b/react/features/chat/middleware.ts @@ -256,6 +256,7 @@ MiddlewareRegistry.register(store => next => action => { lobbyChat: false }, false, true); } + break; } } diff --git a/react/features/invite/functions.ts b/react/features/invite/functions.ts index 36612c98f6..b677547cac 100644 --- a/react/features/invite/functions.ts +++ b/react/features/invite/functions.ts @@ -7,9 +7,10 @@ import { getRoomName } from '../base/conference/functions'; import { getInviteURL } from '../base/connection/functions'; import { isIosMobileBrowser } from '../base/environment/utils'; import i18next from '../base/i18n/i18next'; +import { MEET_FEATURES } from '../base/jwt/constants'; import { isJwtFeatureEnabled } from '../base/jwt/functions'; import { JitsiRecordingConstants } from '../base/lib-jitsi-meet'; -import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants/functions'; +import { getLocalParticipant } from '../base/participants/functions'; import { toState } from '../base/redux/functions'; import { doGetJSON } from '../base/util/httpUtils'; import { parseURLParams } from '../base/util/parseURLParams'; @@ -491,10 +492,8 @@ export function isAddPeopleEnabled(state: IReduxState): boolean { */ export function isDialOutEnabled(state: IReduxState): boolean { const { conference } = state['features/base/conference']; - const isModerator = isLocalParticipantModerator(state); - return isJwtFeatureEnabled(state, 'outbound-call', isModerator, false) - && conference?.isSIPCallingSupported(); + return isJwtFeatureEnabled(state, MEET_FEATURES.OUTBOUND_CALL, false) && conference?.isSIPCallingSupported(); } /** @@ -505,10 +504,8 @@ export function isDialOutEnabled(state: IReduxState): boolean { */ export function isSipInviteEnabled(state: IReduxState): boolean { const { sipInviteUrl } = state['features/base/config']; - const isModerator = isLocalParticipantModerator(state); - return isJwtFeatureEnabled(state, 'sip-outbound-call', isModerator, false) - && Boolean(sipInviteUrl); + return isJwtFeatureEnabled(state, MEET_FEATURES.SIP_OUTBOUND_CALL, false) && Boolean(sipInviteUrl); } /** diff --git a/react/features/polls/functions.ts b/react/features/polls/functions.ts index 24025c1ddf..327d236545 100644 --- a/react/features/polls/functions.ts +++ b/react/features/polls/functions.ts @@ -1,4 +1,5 @@ import { IReduxState } from '../app/types'; +import { MEET_FEATURES } from '../base/jwt/constants'; import { isJwtFeatureEnabled } from '../base/jwt/functions'; import { IAnswerData } from './types'; @@ -77,5 +78,5 @@ export function isCreatePollDisabled(state: IReduxState) { return false; } - return !isJwtFeatureEnabled(state, 'create-polls', false, false); + return !isJwtFeatureEnabled(state, MEET_FEATURES.CREATE_POLLS, false); } diff --git a/react/features/recording/actions.any.ts b/react/features/recording/actions.any.ts index 61c4dfdcbc..99e2fa0626 100644 --- a/react/features/recording/actions.any.ts +++ b/react/features/recording/actions.any.ts @@ -1,12 +1,9 @@ import { IStore } from '../app/types'; import { getMeetingRegion, getRecordingSharingUrl } from '../base/config/functions'; +import { MEET_FEATURES } from '../base/jwt/constants'; import { isJwtFeatureEnabled } from '../base/jwt/functions'; import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet'; -import { - getLocalParticipant, - getParticipantDisplayName, - isLocalParticipantModerator -} from '../base/participants/functions'; +import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions'; import { BUTTON_TYPES } from '../base/ui/constants.any'; import { copyText } from '../base/util/copyText'; import { getVpaasTenant, isVpaasMeeting } from '../jaas/functions'; @@ -435,10 +432,9 @@ export function showStartRecordingNotificationWithCallback(openRecordingDialog: customActionNameKey: [ 'notify.suggestRecordingAction' ], customActionHandler: [ () => { state = getState(); - const isModerator = isLocalParticipantModerator(state); const { recordingService } = state['features/base/config']; const canBypassDialog = recordingService?.enabled - && isJwtFeatureEnabled(state, 'recording', isModerator, false); + && isJwtFeatureEnabled(state, MEET_FEATURES.RECORDING, false); if (canBypassDialog) { const options = { diff --git a/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts b/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts index 855be7c599..a71d996a73 100644 --- a/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts +++ b/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts @@ -2,7 +2,6 @@ import { IReduxState } from '../../../app/types'; import { IconSites } from '../../../base/icons/svg'; import { MEET_FEATURES } from '../../../base/jwt/constants'; import { isJwtFeatureEnabled } from '../../../base/jwt/functions'; -import { isLocalParticipantModerator } from '../../../base/participants/functions'; import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton'; import { isInBreakoutRoom } from '../../../breakout-rooms/functions'; import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions'; @@ -129,11 +128,10 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) { // If the containing component provides the visible prop, that is one // above all, but if not, the button should be autonomous and decide on // its own to be visible or not. - const isModerator = isLocalParticipantModerator(state); const liveStreaming = getLiveStreaming(state); visible = isLiveStreamingButtonVisible({ - liveStreamingAllowed: isJwtFeatureEnabled(state, 'livestreaming', isModerator, false), + liveStreamingAllowed: isJwtFeatureEnabled(state, MEET_FEATURES.LIVESTREAMING, false), liveStreamingEnabled: liveStreaming?.enabled, isInBreakoutRoom: isInBreakoutRoom(state) }); diff --git a/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx b/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx index be415f06be..926d636afb 100644 --- a/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx +++ b/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx @@ -6,8 +6,8 @@ import { sendAnalytics } from '../../../analytics/functions'; import { IReduxState, IStore } from '../../../app/types'; import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry'; import { _abstractMapStateToProps } from '../../../base/dialog/functions'; +import { MEET_FEATURES } from '../../../base/jwt/constants'; import { isJwtFeatureEnabled } from '../../../base/jwt/functions'; -import { isLocalParticipantModerator } from '../../../base/participants/functions'; import { authorizeDropbox, updateDropboxToken } from '../../../dropbox/actions'; import { isVpaasMeeting } from '../../../jaas/functions'; import { canAddTranscriber } from '../../../transcribing/functions'; @@ -414,14 +414,13 @@ class AbstractStartRecordingDialogContent extends Component { export function mapStateToProps(state: IReduxState) { const { localRecording, recordingService } = state['features/base/config']; const _localRecordingAvailable = !localRecording?.disable && supportsLocalRecording(); - const isModerator = isLocalParticipantModerator(state); return { ..._abstractMapStateToProps(state), isVpaas: isVpaasMeeting(state), _canStartTranscribing: canAddTranscriber(state), _hideStorageWarning: Boolean(recordingService?.hideStorageWarning), - _renderRecording: isJwtFeatureEnabled(state, 'recording', isModerator, false), + _renderRecording: isJwtFeatureEnabled(state, MEET_FEATURES.RECORDING, false), _localRecordingAvailable, _localRecordingEnabled: !localRecording?.disable, _localRecordingSelfEnabled: !localRecording?.disableSelfRecording, diff --git a/react/features/recording/functions.ts b/react/features/recording/functions.ts index ca6895a2a9..f3c3dcd995 100644 --- a/react/features/recording/functions.ts +++ b/react/features/recording/functions.ts @@ -2,14 +2,11 @@ import i18next from 'i18next'; import { IReduxState, IStore } from '../app/types'; import { isMobileBrowser } from '../base/environment/utils'; +import { MEET_FEATURES } from '../base/jwt/constants'; import { isJwtFeatureEnabled } from '../base/jwt/functions'; import { JitsiRecordingConstants, browser } from '../base/lib-jitsi-meet'; import { getSoundFileSrc } from '../base/media/functions'; -import { - getLocalParticipant, - getRemoteParticipants, - isLocalParticipantModerator -} from '../base/participants/functions'; +import { getLocalParticipant, getRemoteParticipants } from '../base/participants/functions'; import { registerSound, unregisterSound } from '../base/sounds/actions'; import { isInBreakoutRoom as isInBreakoutRoomF } from '../breakout-rooms/functions'; import { isEnabled as isDropboxEnabled } from '../dropbox/functions'; @@ -203,9 +200,7 @@ export function canStopRecording(state: IReduxState) { } if (isCloudRecordingRunning(state) || isRecorderTranscriptionsRunning(state)) { - const isModerator = isLocalParticipantModerator(state); - - return isJwtFeatureEnabled(state, 'recording', isModerator, false); + return isJwtFeatureEnabled(state, MEET_FEATURES.RECORDING, false); } return false; @@ -257,7 +252,6 @@ export function getRecordButtonProps(state: IReduxState) { // If the containing component provides the visible prop, that is one // above all, but if not, the button should be autonomus and decide on // its own to be visible or not. - const isModerator = isLocalParticipantModerator(state); const { recordingService, localRecording @@ -269,7 +263,7 @@ export function getRecordButtonProps(state: IReduxState) { if (localRecordingEnabled) { visible = true; - } else if (isJwtFeatureEnabled(state, 'recording', isModerator, false)) { + } else if (isJwtFeatureEnabled(state, MEET_FEATURES.RECORDING, false)) { visible = recordingEnabled; } diff --git a/react/features/recording/hooks.web.ts b/react/features/recording/hooks.web.ts index adbd527809..05de0c8959 100644 --- a/react/features/recording/hooks.web.ts +++ b/react/features/recording/hooks.web.ts @@ -1,8 +1,8 @@ import { useSelector } from 'react-redux'; import { IReduxState } from '../app/types'; +import { MEET_FEATURES } from '../base/jwt/constants'; import { isJwtFeatureEnabled } from '../base/jwt/functions'; -import { isLocalParticipantModerator } from '../base/participants/functions'; import { isInBreakoutRoom } from '../breakout-rooms/functions'; import { getLiveStreaming } from './components/LiveStream/functions'; @@ -45,10 +45,9 @@ export function useRecordingButton() { */ export function useLiveStreamingButton() { const toolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons); - const localParticipantIsModerator = useSelector(isLocalParticipantModerator); const liveStreaming = useSelector(getLiveStreaming); const liveStreamingAllowed = useSelector((state: IReduxState) => - isJwtFeatureEnabled(state, 'livestreaming', localParticipantIsModerator, false)); + isJwtFeatureEnabled(state, MEET_FEATURES.LIVESTREAMING, false)); const _isInBreakoutRoom = useSelector(isInBreakoutRoom); if (toolbarButtons?.includes('recording') diff --git a/react/features/subtitles/middleware.ts b/react/features/subtitles/middleware.ts index e717d12e4f..fb5d73d42b 100644 --- a/react/features/subtitles/middleware.ts +++ b/react/features/subtitles/middleware.ts @@ -2,9 +2,9 @@ import { AnyAction } from 'redux'; import { IStore } from '../app/types'; import { ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes'; +import { MEET_FEATURES } from '../base/jwt/constants'; import { isJwtFeatureEnabled } from '../base/jwt/functions'; import JitsiMeetJS from '../base/lib-jitsi-meet'; -import { isLocalParticipantModerator } from '../base/participants/functions'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import { TRANSCRIBER_JOINED } from '../transcribing/actionTypes'; @@ -293,8 +293,7 @@ function _requestingSubtitlesChange( enabled); if (enabled && conference?.getTranscriptionStatus() === JitsiMeetJS.constants.transcriptionStatus.OFF) { - const isModerator = isLocalParticipantModerator(state); - const featureAllowed = isJwtFeatureEnabled(getState(), 'transcription', isModerator, false); + const featureAllowed = isJwtFeatureEnabled(getState(), MEET_FEATURES.TRANSCRIPTION, false); if (featureAllowed) { conference?.dial(TRANSCRIBER_DIAL_NUMBER) diff --git a/react/features/toolbox/components/web/Toolbox.tsx b/react/features/toolbox/components/web/Toolbox.tsx index d33237f390..60c07ca2ec 100644 --- a/react/features/toolbox/components/web/Toolbox.tsx +++ b/react/features/toolbox/components/web/Toolbox.tsx @@ -94,7 +94,7 @@ export default function Toolbox({ const transcribing = useSelector(isTranscribing); // Do not convert to selector, it returns new array and will cause re-rendering of toolbox on every action. - const jwtDisabledButtons = getJwtDisabledButtons(transcribing, isModerator, jwt, localParticipant?.features); + const jwtDisabledButtons = getJwtDisabledButtons(transcribing, jwt, localParticipant?.features); const reactionsButtonEnabled = useSelector(isReactionsButtonEnabled); const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons); diff --git a/react/features/toolbox/functions.any.ts b/react/features/toolbox/functions.any.ts index 957bc1b8b2..ff0a7e48a6 100644 --- a/react/features/toolbox/functions.any.ts +++ b/react/features/toolbox/functions.any.ts @@ -27,14 +27,12 @@ export function isAudioMuteButtonDisabled(state: IReduxState) { * This function is stateless as it returns a new array and may cause re-rendering. * * @param {boolean} isTranscribing - Whether there is currently a transcriber in the meeting. - * @param {boolean} isModerator - Whether local participant is moderator. * @param {string | undefined} jwt - The jwt token. * @param {ILocalParticipant} localParticipantFeatures - The features of the local participant. * @returns {string[]} - The disabled by jwt buttons array. */ export function getJwtDisabledButtons( isTranscribing: boolean, - isModerator: boolean, jwt: string | undefined, localParticipantFeatures?: IParticipantFeatures) { const acc = []; @@ -43,7 +41,6 @@ export function getJwtDisabledButtons( jwt, localParticipantFeatures, feature: 'livestreaming', - ifNoToken: isModerator, ifNotInFeatures: false })) { acc.push('livestreaming'); @@ -53,7 +50,6 @@ export function getJwtDisabledButtons( jwt, localParticipantFeatures, feature: 'transcription', - ifNoToken: isModerator, ifNotInFeatures: false })) { acc.push('closedcaptions'); diff --git a/react/features/toolbox/functions.web.ts b/react/features/toolbox/functions.web.ts index 57457c83d7..0934429691 100644 --- a/react/features/toolbox/functions.web.ts +++ b/react/features/toolbox/functions.web.ts @@ -73,7 +73,7 @@ export function isAudioSettingsButtonDisabled(state: IReduxState) { export function isDesktopShareButtonDisabled(state: IReduxState) { const { muted, unmuteBlocked } = state['features/base/media'].video; const videoOrShareInProgress = !muted || isScreenMediaShared(state); - const enabledInJwt = isJwtFeatureEnabled(state, MEET_FEATURES.SCREEN_SHARING, true, true); + const enabledInJwt = isJwtFeatureEnabled(state, MEET_FEATURES.SCREEN_SHARING, true); return !enabledInJwt || (unmuteBlocked && !videoOrShareInProgress); } diff --git a/react/features/transcribing/functions.ts b/react/features/transcribing/functions.ts index c5bdab5fb6..a254f320dd 100644 --- a/react/features/transcribing/functions.ts +++ b/react/features/transcribing/functions.ts @@ -2,8 +2,8 @@ import i18next from 'i18next'; import { IReduxState } from '../app/types'; import { IConfig } from '../base/config/configType'; +import { MEET_FEATURES } from '../base/jwt/constants'; import { isJwtFeatureEnabled } from '../base/jwt/functions'; -import { isLocalParticipantModerator } from '../base/participants/functions'; import JITSI_TO_BCP47_MAP from './jitsi-bcp47-map.json'; import logger from './logger'; @@ -77,8 +77,7 @@ export function isRecorderTranscriptionsRunning(state: IReduxState) { */ export function canAddTranscriber(state: IReduxState) { const { transcription } = state['features/base/config']; - const isModerator = isLocalParticipantModerator(state); - const isTranscribingAllowed = isJwtFeatureEnabled(state, 'transcription', isModerator, false); + const isTranscribingAllowed = isJwtFeatureEnabled(state, MEET_FEATURES.TRANSCRIPTION, false); return Boolean(transcription?.enabled) && isTranscribingAllowed; } diff --git a/resources/prosody-plugins/mod_filter_iq_rayo.lua b/resources/prosody-plugins/mod_filter_iq_rayo.lua index ea6d523f8b..5c8b0f7236 100644 --- a/resources/prosody-plugins/mod_filter_iq_rayo.lua +++ b/resources/prosody-plugins/mod_filter_iq_rayo.lua @@ -10,7 +10,6 @@ local room_jid_match_rewrite = util.room_jid_match_rewrite; local is_feature_allowed = util.is_feature_allowed; local is_sip_jigasi = util.is_sip_jigasi; local get_room_from_jid = util.get_room_from_jid; -local is_healthcheck_room = util.is_healthcheck_room; local process_host_module = util.process_host_module; local jid_bare = require "util.jid".bare; @@ -218,49 +217,10 @@ end module:hook_global('config-reloaded', load_config); -function process_set_affiliation(event) - local actor, affiliation, jid, previous_affiliation, room - = event.actor, event.affiliation, event.jid, event.previous_affiliation, event.room; - local actor_session = sessions[actor]; - - if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not previous_affiliation - or not actor_session or not actor_session.jitsi_meet_context_features then - return; - end - - local occupant; - for _, o in room:each_occupant() do - if o.bare_jid == jid then - occupant = o; - end - end - - if not occupant then - return; - end - - local occupant_session = sessions[occupant.jid]; - if not occupant_session then - return; - end - - if previous_affiliation == 'none' and affiliation == 'owner' then - occupant_session.granted_jitsi_meet_context_features = actor_session.jitsi_meet_context_features; - occupant_session.granted_jitsi_meet_context_user_id = actor_session.jitsi_meet_context_user["id"]; - occupant_session.granted_jitsi_meet_context_group_id = actor_session.jitsi_meet_context_group; - elseif previous_affiliation == 'owner' and ( affiliation == 'member' or affiliation == 'none' ) then - occupant_session.granted_jitsi_meet_context_features = nil; - occupant_session.granted_jitsi_meet_context_user_id = nil; - occupant_session.granted_jitsi_meet_context_group_id = nil; - end -end - function process_main_muc_loaded(main_muc, host_module) module:log('debug', 'Main muc loaded'); main_muc_service = main_muc; - module:log("info", "Hook to muc events on %s", main_muc_component_host); - host_module:hook("muc-pre-set-affiliation", process_set_affiliation); end process_host_module(main_muc_component_host, function(host_module, host) diff --git a/resources/prosody-plugins/mod_jitsi_permissions.lua b/resources/prosody-plugins/mod_jitsi_permissions.lua new file mode 100644 index 0000000000..a595880a3a --- /dev/null +++ b/resources/prosody-plugins/mod_jitsi_permissions.lua @@ -0,0 +1,179 @@ +-- this is auto loaded by meeting_id +local filters = require 'util.filters'; +local jid = require 'util.jid'; +local modulemanager = require "prosody.core.modulemanager"; + +local util = module:require 'util'; +local is_admin = util.is_admin; +local get_room_from_jid = util.get_room_from_jid; +local is_healthcheck_room = util.is_healthcheck_room; +local room_jid_match_rewrite = util.room_jid_match_rewrite; +local ends_with = util.ends_with; +local presence_check_status = util.presence_check_status; + +local MUC_NS = 'http://jabber.org/protocol/muc'; + +local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference'); + +local muc_domain_base = module:get_option_string('muc_mapper_domain_base'); +if not muc_domain_base then + module:log('warn', 'No "muc_domain_base" option set, disabling kick check endpoint.'); + return ; +end + +-- only the visitor prosody has main_domain setting +local is_visitor_prosody = module:get_option_string('main_domain') ~= nil; + +-- load it only on the main muc component as it is loaded by muc_meeting_id which is loaded and for the breakout room muc +if muc_domain_prefix..'.'..muc_domain_base ~= module.host or is_visitor_prosody then + return; +end + +local sessions = prosody.full_sessions; +local default_permissions; + +local function load_config() + default_permissions = module:get_option('jitsi_default_permissions', { + livestreaming = true; + recording = true; + transcription = true; + ['outbound-call'] = true; + ['create-polls'] = true; + ['send-groupchat'] = true; + flip = true; + }); +end +load_config(); + +function process_set_affiliation(event) + local actor, affiliation, jid, previous_affiliation, room + = event.actor, event.affiliation, event.jid, event.previous_affiliation, event.room; + local actor_session = sessions[actor]; + + if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not previous_affiliation + or not actor_session or not actor_session.jitsi_meet_context_features then + return; + end + + local occupant; + for _, o in room:each_occupant() do + if o.bare_jid == jid then + occupant = o; + end + end + + if not occupant then + return; + end + + local occupant_session = sessions[occupant.jid]; + if not occupant_session then + return; + end + + if previous_affiliation == 'none' and affiliation == 'owner' then + occupant_session.granted_jitsi_meet_context_features = actor_session.jitsi_meet_context_features; + occupant_session.granted_jitsi_meet_context_user_id = actor_session.jitsi_meet_context_user['id']; + occupant_session.granted_jitsi_meet_context_group_id = actor_session.jitsi_meet_context_group; + elseif previous_affiliation == 'owner' and ( affiliation == 'member' or affiliation == 'none' ) then + occupant_session.granted_jitsi_meet_context_features = nil; + occupant_session.granted_jitsi_meet_context_user_id = nil; + occupant_session.granted_jitsi_meet_context_group_id = nil; + + -- on revoke + if not session.auth_token then + occupant_session.jitsi_meet_context_features = nil; + end + end +end + +-- Detects when sending self-presence because of role change +-- we can end up here because of the following cases: +-- 1. user joins the room and is granted moderator by another moderator or jicofo +-- 2. Some module changes the role of the user by using set_affiliation method +-- In cases where authentication is 'anonymous', 'jitsi-anonymous', 'internal_hashed', 'internal_plain', 'cyrus' we +-- want to send default permissions all to indicate UI that everything is allowed (to not relay on the UI to check +-- is participant moderator or not), to allow finer control over the permissions. +-- In case the authentication is 'token' based we want to send permissions only if the token of the user does not include +-- features in the user.context. +-- In case of allowners we want to send the permissions, no matter of the authentication method. +-- In case permissions were granted we want to send the granted permissions in all cases except when the user is +-- using token that has features pre-defined (authentication is 'token'). +function filter_stanza(stanza, session) + local bare_to = jid.bare(stanza.attr.to); + if not stanza.attr or not stanza.attr.to or stanza.name ~= 'presence' + or stanza.attr.type == 'unavailable' + or ends_with(stanza.attr.from, '/focus') or is_admin(bare_to) then + return stanza; + end + + local muc_x = stanza:get_child('x', MUC_NS..'#user'); + if not muc_x then + return stanza; + end + + local room = get_room_from_jid(room_jid_match_rewrite(jid.bare(stanza.attr.from))); + + if not room or not room.send_default_permissions_to or is_healthcheck_room(room.jid) then + return stanza; + end + + if session.auth_token and session.jitsi_meet_context_features then -- token and features are set so skip + room.send_default_permissions_to[bare_to] = nil; + return stanza; + end + + -- we are sending permissions only when becoming a member + local is_moderator = false; + for item in muc_x:childtags('item') do + if item.attr.role == 'moderator' then + is_moderator = true; + break; + end + end + + if not is_moderator or not room.send_default_permissions_to[bare_to] + or not presence_check_status(muc_x, '110') then + return stanza; + end + + local permissions_to_send = session.granted_jitsi_meet_context_features or default_permissions; + + room.send_default_permissions_to[bare_to] = nil; + + if not session.granted_jitsi_meet_context_features and not session.jitsi_meet_context_features then + session.jitsi_meet_context_features = {}; + end + + stanza:tag('permissions', { xmlns='http://jitsi.org/jitmeet' }); + for k, v in pairs(permissions_to_send) do + local val = tostring(v); + stanza:tag('p', { name = k, val = val }):up(); + if session.jitsi_meet_context_features then + session.jitsi_meet_context_features[k] = val; + end + end + stanza:up(); + + return stanza; +end + +-- we need to indicate that we will send permissions if we need to +module:hook('muc-pre-set-affiliation', function(event) + local jid, room = event.jid, event.room; + + if not room.send_default_permissions_to then + room.send_default_permissions_to = {}; + end + room.send_default_permissions_to[jid] = true; +end); +module:hook('muc-set-affiliation', process_set_affiliation); + +function filter_session(session) + -- domain mapper is filtering on default priority 0 + -- allowners is -1 and we need it after that + filters.add_filter(session, 'stanzas/out', filter_stanza, -2); +end + +-- enable filtering presences +filters.add_filter_hook(filter_session); diff --git a/resources/prosody-plugins/mod_muc_meeting_id.lua b/resources/prosody-plugins/mod_muc_meeting_id.lua index 32fa2e41e4..211f75082f 100644 --- a/resources/prosody-plugins/mod_muc_meeting_id.lua +++ b/resources/prosody-plugins/mod_muc_meeting_id.lua @@ -12,6 +12,8 @@ local presence_check_status = main_util.presence_check_status; local QUEUE_MAX_SIZE = 500; +module:depends("jitsi_permissions"); + -- Common module for all logic that can be loaded under the conference muc component. -- -- This module: