feat(recording): adds custom userdata recording permission (#21497)
* feat(recording): adds custom "bbb_record_permission" userdata Adds two new userdata parameters: - bbb_record_permission: if true, bypass the moderator/viewer role permission and allows the user to start/stop the recording - bbb_record_permission_tooltip: a string to show in the tooltip when the user don't have permission, useful to explain why the user can't start recording This enables integrations to have custom rules for specific users to have or not permission to record a meeting * chore(docs): adds new recording userdata to docs * refactor: clean unused code * Update docs/docs/administration/customize.md Co-authored-by: Anton Georgiev <antobinary@users.noreply.github.com> --------- Co-authored-by: Anton Georgiev <antobinary@users.noreply.github.com>
This commit is contained in:
parent
d876c45784
commit
bc324f480e
@ -9,6 +9,7 @@ import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
|||||||
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
import org.bigbluebutton.core2.message.senders.MsgBuilder
|
||||||
import org.bigbluebutton.core.apps.voice.VoiceApp
|
import org.bigbluebutton.core.apps.voice.VoiceApp
|
||||||
import org.bigbluebutton.core.db.{ MeetingRecordingDAO, NotificationDAO }
|
import org.bigbluebutton.core.db.{ MeetingRecordingDAO, NotificationDAO }
|
||||||
|
import org.bigbluebutton.core.models.{ Users2x }
|
||||||
|
|
||||||
trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait {
|
trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait {
|
||||||
this: UsersApp =>
|
this: UsersApp =>
|
||||||
@ -29,9 +30,19 @@ trait SetRecordingStatusCmdMsgHdlr extends RightsManagementTrait {
|
|||||||
BbbCommonEnvCoreMsg(envelope, event)
|
BbbCommonEnvCoreMsg(envelope, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
// Retrieve custom record permission from metadata
|
||||||
|
val customRecordPermission: Option[Boolean] = Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId).flatMap { user =>
|
||||||
|
user.userMetadata.get("bbb_record_permission").map(_.toBoolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine final permission using metadata or fallback
|
||||||
|
val hasPermission: Boolean = customRecordPermission.getOrElse {
|
||||||
|
!permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
val meetingId = liveMeeting.props.meetingProp.intId
|
val meetingId = liveMeeting.props.meetingProp.intId
|
||||||
val reason = "No permission to clear chat in meeting."
|
val reason = "No permission to set recording status in meeting."
|
||||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
||||||
state
|
state
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { RedisMessage } from '../types';
|
import { RedisMessage } from '../types';
|
||||||
import {throwErrorIfInvalidInput, throwErrorIfNotModerator} from "../imports/validation";
|
import { throwErrorIfInvalidInput } from "../imports/validation";
|
||||||
|
|
||||||
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
export default function buildRedisMessage(sessionVariables: Record<string, unknown>, input: Record<string, unknown>): RedisMessage {
|
||||||
throwErrorIfNotModerator(sessionVariables);
|
|
||||||
throwErrorIfInvalidInput(input,
|
throwErrorIfInvalidInput(input,
|
||||||
[
|
[
|
||||||
{name: 'recording', type: 'boolean', required: true},
|
{ name: 'recording', type: 'boolean', required: true },
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const eventName = 'SetRecordingStatusCmdMsg';
|
const eventName = 'SetRecordingStatusCmdMsg';
|
||||||
|
@ -6,6 +6,7 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||||
import deviceInfo, { isMobile } from '/imports/utils/deviceInfo';
|
import deviceInfo, { isMobile } from '/imports/utils/deviceInfo';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import {
|
import {
|
||||||
GET_MEETING_RECORDING_DATA,
|
GET_MEETING_RECORDING_DATA,
|
||||||
GET_MEETING_RECORDING_POLICIES,
|
GET_MEETING_RECORDING_POLICIES,
|
||||||
@ -20,7 +21,6 @@ import humanizeSeconds from '/imports/utils/humanizeSeconds';
|
|||||||
import { notify } from '/imports/ui/services/notification';
|
import { notify } from '/imports/ui/services/notification';
|
||||||
import Styled from './styles';
|
import Styled from './styles';
|
||||||
import { User } from '/imports/ui/Types/user';
|
import { User } from '/imports/ui/Types/user';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
|
||||||
import useTimeSync from '/imports/ui/core/local-states/useTimeSync';
|
import useTimeSync from '/imports/ui/core/local-states/useTimeSync';
|
||||||
import RecordingNotify from './notify/component';
|
import RecordingNotify from './notify/component';
|
||||||
import RecordingContainer from '/imports/ui/components/recording/container';
|
import RecordingContainer from '/imports/ui/components/recording/container';
|
||||||
@ -28,6 +28,7 @@ import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedS
|
|||||||
import { getSettingsSingletonInstance } from '/imports/ui/services/settings';
|
import { getSettingsSingletonInstance } from '/imports/ui/services/settings';
|
||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
import SvgIcon from '/imports/ui/components/common/icon-svg/component';
|
import SvgIcon from '/imports/ui/components/common/icon-svg/component';
|
||||||
|
import Service from './service';
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
notificationRecordingStart: {
|
notificationRecordingStart: {
|
||||||
@ -233,7 +234,9 @@ const RecordingIndicator: React.FC<RecordingIndicatorProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const recordingButton = recording ? recordMeetingButtonWithTooltip : recordMeetingButton;
|
const recordingButton = recording ? recordMeetingButtonWithTooltip : recordMeetingButton;
|
||||||
const showButton = isModerator && allowStartStopRecording;
|
const showButton = Service.mayIRecord(isModerator, allowStartStopRecording);
|
||||||
|
const defaultRecordTooltip = intl.formatMessage(intlMessages.notificationRecordingStop);
|
||||||
|
const customRecordTooltip = Service.getCustomRecordTooltip(defaultRecordTooltip);
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -249,18 +252,14 @@ const RecordingIndicator: React.FC<RecordingIndicatorProps> = ({
|
|||||||
{showButton ? recordingButton : null}
|
{showButton ? recordingButton : null}
|
||||||
{showButton ? null : (
|
{showButton ? null : (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={`${intl.formatMessage(
|
title={recording
|
||||||
recording
|
? `${intl.formatMessage(intlMessages.notificationRecordingStart)}`
|
||||||
? intlMessages.notificationRecordingStart
|
: customRecordTooltip}
|
||||||
: intlMessages.notificationRecordingStop,
|
|
||||||
)}`}
|
|
||||||
>
|
>
|
||||||
<Styled.RecordingStatusViewOnly
|
<Styled.RecordingStatusViewOnly
|
||||||
aria-label={`${intl.formatMessage(
|
aria-label={recording
|
||||||
recording
|
? `${intl.formatMessage(intlMessages.notificationRecordingStart)}`
|
||||||
? intlMessages.notificationRecordingStart
|
: customRecordTooltip}
|
||||||
: intlMessages.notificationRecordingStop,
|
|
||||||
)}`}
|
|
||||||
recording={recording}
|
recording={recording}
|
||||||
>
|
>
|
||||||
{recordingIndicatorIcon}
|
{recordingIndicatorIcon}
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||||
|
|
||||||
|
const mayIRecord = (amIModerator: boolean, allowStartStopRecording: boolean) => {
|
||||||
|
const customRecord = getFromUserSettings('bbb_record_permission', undefined);
|
||||||
|
if (!allowStartStopRecording) return false;
|
||||||
|
if (customRecord !== undefined) {
|
||||||
|
return customRecord;
|
||||||
|
}
|
||||||
|
return amIModerator;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mayIRecord,
|
||||||
|
getCustomRecordTooltip: (value: string) => getFromUserSettings('bbb_record_permission_tooltip', value),
|
||||||
|
};
|
@ -46,7 +46,6 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface RecordingComponentProps {
|
interface RecordingComponentProps {
|
||||||
amIModerator: boolean;
|
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
recordingStatus: boolean,
|
recordingStatus: boolean,
|
||||||
@ -59,7 +58,6 @@ interface RecordingComponentProps {
|
|||||||
|
|
||||||
const RecordingComponent: React.FC<RecordingComponentProps> = (props) => {
|
const RecordingComponent: React.FC<RecordingComponentProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
amIModerator,
|
|
||||||
connected,
|
connected,
|
||||||
isOpen,
|
isOpen,
|
||||||
recordingStatus,
|
recordingStatus,
|
||||||
@ -76,8 +74,6 @@ const RecordingComponent: React.FC<RecordingComponentProps> = (props) => {
|
|||||||
let description;
|
let description;
|
||||||
let cancelButtonLabel;
|
let cancelButtonLabel;
|
||||||
|
|
||||||
if (!amIModerator) return null;
|
|
||||||
|
|
||||||
if (recordingStatus) {
|
if (recordingStatus) {
|
||||||
description = intl.formatMessage(intlMessages.stopDescription);
|
description = intl.formatMessage(intlMessages.stopDescription);
|
||||||
title = intl.formatMessage(intlMessages.stopTitle);
|
title = intl.formatMessage(intlMessages.stopTitle);
|
||||||
|
@ -2,10 +2,11 @@ import React from 'react';
|
|||||||
import { useMutation, useReactiveVar } from '@apollo/client';
|
import { useMutation, useReactiveVar } from '@apollo/client';
|
||||||
import RecordingComponent from './component';
|
import RecordingComponent from './component';
|
||||||
import { SET_RECORDING_STATUS } from './mutations';
|
import { SET_RECORDING_STATUS } from './mutations';
|
||||||
import { GetRecordingResponse } from './queries';
|
import { GetRecordingResponse, GetRecordingPoliciesResponse } from './queries';
|
||||||
import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedSubscription';
|
import useDeduplicatedSubscription from '/imports/ui/core/hooks/useDeduplicatedSubscription';
|
||||||
import ConnectionStatus from '/imports/ui/core/graphql/singletons/connectionStatus';
|
import ConnectionStatus from '/imports/ui/core/graphql/singletons/connectionStatus';
|
||||||
import { GET_MEETING_RECORDING_DATA } from '/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/queries';
|
import { GET_MEETING_RECORDING_DATA, GET_MEETING_RECORDING_POLICIES } from '/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/queries';
|
||||||
|
import Service from '/imports/ui/components/nav-bar/nav-bar-graphql/recording-indicator/service';
|
||||||
|
|
||||||
interface RecordingContainerProps {
|
interface RecordingContainerProps {
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
@ -24,9 +25,13 @@ const RecordingContainer: React.FC<RecordingContainerProps> = (props) => {
|
|||||||
const {
|
const {
|
||||||
data: recordingData,
|
data: recordingData,
|
||||||
} = useDeduplicatedSubscription<GetRecordingResponse>(GET_MEETING_RECORDING_DATA);
|
} = useDeduplicatedSubscription<GetRecordingResponse>(GET_MEETING_RECORDING_DATA);
|
||||||
|
const {
|
||||||
|
data: recordingPoliciesData,
|
||||||
|
} = useDeduplicatedSubscription<GetRecordingPoliciesResponse>(GET_MEETING_RECORDING_POLICIES);
|
||||||
|
|
||||||
const recording = recordingData?.meeting_recording[0]?.isRecording ?? false;
|
const recording = recordingData?.meeting_recording[0]?.isRecording ?? false;
|
||||||
const time = recordingData?.meeting_recording[0]?.previousRecordedTimeInSeconds ?? 0;
|
const time = recordingData?.meeting_recording[0]?.previousRecordedTimeInSeconds ?? 0;
|
||||||
|
const allowStartStopRecording = recordingPoliciesData?.meeting_recordingPolicies[0]?.allowStartStopRecording ?? false;
|
||||||
|
|
||||||
const toggleRecording = () => {
|
const toggleRecording = () => {
|
||||||
setRecordingStatus({
|
setRecordingStatus({
|
||||||
@ -37,10 +42,13 @@ const RecordingContainer: React.FC<RecordingContainerProps> = (props) => {
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mayIRecord = Service.mayIRecord(amIModerator, allowStartStopRecording);
|
||||||
|
|
||||||
|
if (!mayIRecord) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordingComponent
|
<RecordingComponent
|
||||||
{...{
|
{...{
|
||||||
amIModerator,
|
|
||||||
connected,
|
connected,
|
||||||
isOpen,
|
isOpen,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
|
@ -4,3 +4,9 @@ export interface GetRecordingResponse {
|
|||||||
previousRecordedTimeInSeconds: number;
|
previousRecordedTimeInSeconds: number;
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetRecordingPoliciesResponse {
|
||||||
|
meeting_recordingPolicies: {
|
||||||
|
allowStartStopRecording: boolean;
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
@ -1400,6 +1400,8 @@ Useful tools for development:
|
|||||||
| `userdata-bbb_hide_presentation_on_join` | (Introduced in BigBlueButton 2.6) If set to `true` it will make the user enter the meeting with presentation minimized (Only for non-presenters), not permanent.
|
| `userdata-bbb_hide_presentation_on_join` | (Introduced in BigBlueButton 2.6) If set to `true` it will make the user enter the meeting with presentation minimized (Only for non-presenters), not permanent.
|
||||||
| `userdata-bbb_direct_leave_button` | (Introduced in BigBlueButton 2.7) If set to `true` it will make a button to leave the meeting appear to the left of the Options menu. | `false` |
|
| `userdata-bbb_direct_leave_button` | (Introduced in BigBlueButton 2.7) If set to `true` it will make a button to leave the meeting appear to the left of the Options menu. | `false` |
|
||||||
| `userdata-bbb_parent_room_moderator=` | (Introduced in BigBlueButton 3.0) Only used in breakouts: if set to `true`, user will have permission to kick other users inside the breakout | `false`
|
| `userdata-bbb_parent_room_moderator=` | (Introduced in BigBlueButton 3.0) Only used in breakouts: if set to `true`, user will have permission to kick other users inside the breakout | `false`
|
||||||
|
| `userdata-bbb_record_permission=` | (Introduced in BigBlueButton 3.0) If set to `true`, the user will be able to control the recording start/stop. If set to `false`, the user will not be allowed to control the recording start/stop even if their role is moderator. Otherwise only moderators will have the control (default). | `null` |
|
||||||
|
| `userdata-bbb_record_permission_tooltip=` | (Introduced in BigBlueButton 3.0) If set, the tooltip of the recording indicator shown when the user don't have permission to record will be replaced by it's content. | `null` |
|
||||||
|
|
||||||
#### Branding parameters
|
#### Branding parameters
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user