Skip to content
Snippets Groups Projects
Unverified Commit 31fa9ab6 authored by Ahmad Farhat's avatar Ahmad Farhat Committed by GitHub
Browse files

Limit recording visibility by Role (#5614)

* initial work

* more work

* Final work

* Remove unneeded changes

* Fixes to versions
parent acfd54fb
Branches
No related tags found
No related merge requests found
Showing
with 7352 additions and 10406 deletions
......@@ -351,7 +351,8 @@
"manage_roles": "Allow users with this role to edit other roles",
"shared_list": "Include users with this role in the dropdown for sharing rooms",
"room_limit": "Room Limit",
"email_on_signup": "Receive an email when a new user signs up"
"email_on_signup": "Receive an email when a new user signs up",
"allowed_recording_visibility": "Allowed recording visibilities"
}
}
},
......
......@@ -284,6 +284,21 @@ input.search-bar {
}
}
.custom-select {
.select-brand-control {
border-color: var(--brand-color) !important;
box-shadow: 0 0 0 1px var(--brand-color) !important;
}
.select-brand-option {
background-color: whitesmoke;
color: var(--brand-color) !important;
&:active {
background-color: var(--brand-color-light) !important;
}
}
}
//Brand
:root {
--brand-color: '';
......
......@@ -50,7 +50,7 @@ module Api
private
def role_params
params.require(:role).permit(:role_id, :name, :value)
params.require(:role).permit(:role_id, :name, :value, value: [])
end
def create_default_room
......
......@@ -63,9 +63,13 @@ module Api
def update_visibility
new_visibility = params[:visibility].to_s
new_visibility_params = visibility_params_of(new_visibility)
allowed_visibilities = JSON.parse(RolePermission.joins(:permission)
.find_by(role_id: current_user.role_id, permission: { name: 'AccessToVisibilities' })
.value)
return render_error status: :forbidden unless allowed_visibilities.include?(new_visibility)
return render_error status: :bad_request if new_visibility_params.nil?
new_visibility_params = visibility_params_of(new_visibility)
bbb_api = BigBlueButtonApi.new(provider: current_provider)
......
......@@ -18,6 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button, Stack } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import Select from 'react-select';
import Form from '../../../shared_components/forms/Form';
import FormControl from '../../../shared_components/forms/FormControl';
import useUpdateRole from '../../../../hooks/mutations/admin/roles/useUpdateRole';
......@@ -44,6 +45,14 @@ export default function EditRoleForm({ role }) {
const { methods: methodsName, fields: fieldsName } = useEditRoleNameForm({ defaultValues: { name: role?.name } });
const visibilityOptions = [
{ value: 'Published', label: 'Published' },
{ value: 'Unpublished', label: 'Unpublished' },
{ value: 'Protected', label: 'Protected' },
{ value: 'Public', label: 'Public' },
{ value: 'Public/Protected', label: 'Public/Protected' },
];
const {
methods: methodsLimit,
fields: fieldsLimit,
......@@ -137,6 +146,31 @@ export default function EditRoleForm({ role }) {
defaultValue={rolePermissions?.EmailOnSignup === 'true'}
/>
<Form className="pb-3">
<Stack direction="horizontal">
<div className="text-muted me-auto">
{t('admin.roles.edit.allowed_recording_visibility')}
</div>
<div>
<Select
className="custom-select float-end"
isMulti
isClearable={false}
isSearchable={false}
defaultValue={visibilityOptions?.filter((vis) => JSON.parse(rolePermissions?.AccessToVisibilities)?.includes(vis.value))}
options={visibilityOptions}
onChange={(value) => {
updatePermissionAPI.mutate({ role_id: role?.id, name: 'AccessToVisibilities', value: value.map((v) => v.value) });
}}
classNames={{
control: (state) => (state.isFocused ? 'select-brand-control' : ''),
option: (state) => (state.isFocused ? 'select-brand-option' : ''),
}}
/>
</div>
</Stack>
</Form>
<Form methods={methodsLimit} onBlur={methodsLimit.handleSubmit(updatePermissionAPI.mutate)}>
<Stack direction="horizontal">
<div className="text-muted me-auto">
......
......@@ -47,6 +47,7 @@ export default function RecordingRow({
const currentUser = useAuth();
const redirectRecordingUrl = useRedirectRecordingUrl();
const copyRecordingUrl = useCopyRecordingUrl();
const allowedVisibilities = JSON.parse(currentUser.permissions?.AccessToVisibilities);
const localizedTime = localizeDateTimeString(recording?.recorded_at, currentUser?.language);
const formats = recording.formats.sort(
......@@ -106,6 +107,7 @@ export default function RecordingRow({
defaultValue={recording.visibility}
dropUp={dropUp}
>
{ (allowedVisibilities.includes('Public/Protected') || recording.visibility === 'Public/Protected') && (
<Dropdown.Item
key="Public/Protected"
value="Public/Protected"
......@@ -113,6 +115,9 @@ export default function RecordingRow({
>
{t('recording.public_protected')}
</Dropdown.Item>
)}
{ (allowedVisibilities.includes('Public') || recording.visibility === 'Public') && (
<Dropdown.Item
key="Public"
value="Public"
......@@ -120,6 +125,9 @@ export default function RecordingRow({
>
{t('recording.public')}
</Dropdown.Item>
)}
{ (allowedVisibilities.includes('Protected') || recording.visibility === 'Protected') && (
<Dropdown.Item
key="Protected"
value="Protected"
......@@ -127,6 +135,9 @@ export default function RecordingRow({
>
{t('recording.protected')}
</Dropdown.Item>
)}
{ (allowedVisibilities.includes('Published') || recording.visibility === 'Published') && (
<Dropdown.Item
key="Published"
value="Published"
......@@ -134,6 +145,9 @@ export default function RecordingRow({
>
{t('recording.published')}
</Dropdown.Item>
)}
{ (allowedVisibilities.includes('Unpublished') || recording.visibility === 'Unpublished') && (
<Dropdown.Item
key="Unpublished"
value="Unpublished"
......@@ -141,6 +155,7 @@ export default function RecordingRow({
>
{t('recording.unpublished')}
</Dropdown.Item>
)}
</SimpleSelect>
</td>
<td className="border-0">
......
......@@ -21,7 +21,7 @@ import { ChevronDownIcon } from '@heroicons/react/20/solid';
export default function SimpleSelect({ defaultValue, dropUp, children }) {
// Get the currently selected option and set the dropdown toggle to that value
const defaultString = children?.filter((item) => item.props.value === defaultValue)[0];
const defaultString = children?.filter(Boolean)?.filter((item) => item.props.value === defaultValue)[0];
return (
<Dropdown className="simple-select" drop={dropUp ? 'up' : undefined}>
......
......@@ -83,6 +83,7 @@ class TenantSetup
shared_list = Permission.find_by(name: 'SharedList')
can_record = Permission.find_by(name: 'CanRecord')
room_limit = Permission.find_by(name: 'RoomLimit')
access_to_visbilities = Permission.find_by(name: 'AccessToVisibilities')
RolePermission.create! [
{ role: admin, permission: create_room, value: 'true' },
......@@ -94,6 +95,7 @@ class TenantSetup
{ role: admin, permission: shared_list, value: 'true' },
{ role: admin, permission: can_record, value: 'true' },
{ role: admin, permission: room_limit, value: '100' },
{ role: admin, permission: access_to_visbilities, value: Recording::VISIBILITIES.values },
{ role: user, permission: create_room, value: 'true' },
{ role: user, permission: manage_users, value: 'false' },
......@@ -104,6 +106,7 @@ class TenantSetup
{ role: user, permission: shared_list, value: 'true' },
{ role: user, permission: can_record, value: 'true' },
{ role: user, permission: room_limit, value: '100' },
{ role: user, permission: access_to_visbilities, value: Recording::VISIBILITIES.values },
{ role: guest, permission: create_room, value: 'false' },
{ role: guest, permission: manage_users, value: 'false' },
......@@ -113,7 +116,8 @@ class TenantSetup
{ role: guest, permission: manage_roles, value: 'false' },
{ role: guest, permission: shared_list, value: 'true' },
{ role: guest, permission: can_record, value: 'true' },
{ role: guest, permission: room_limit, value: '100' }
{ role: guest, permission: room_limit, value: '100' },
{ role: guest, permission: access_to_visbilities, value: Recording::VISIBILITIES.values }
]
end
end
# frozen_string_literal: true
class AddVisibilityToRolePermissions < ActiveRecord::Migration[7.1]
def up
visibility_permission = Permission.create!(name: 'AccessToVisibilities')
Role.all.each do |role|
RolePermission.create!(role:, permission: visibility_permission, value: Recording::VISIBILITIES.values)
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
DataMigrate::Data.define(version: 20231117151542)
DataMigrate::Data.define(version: 20231210154647)
This diff is collapsed.
......@@ -229,8 +229,25 @@ RSpec.describe Api::V1::RecordingsController, type: :controller do
expect_to_update_recording_props_to(publish: true, protect: true, list: true, visibility: Recording::VISIBILITIES[:public_protected])
end
context 'Unkown visibility' do
it 'returns :bad_request and does not update the recording' do
context 'AccessToVisibilities permission' do
before do
RolePermission.find_by(role: user.role, permission: Permission.find_by(name: 'AccessToVisibilities')).update(value: ['Published'])
end
it 'returns forbidden if the user is not permitted to use that format' do
expect_any_instance_of(BigBlueButtonApi).not_to receive(:publish_recordings)
expect_any_instance_of(BigBlueButtonApi).not_to receive(:update_recordings)
expect do
post :update_visibility, params: { visibility: 'Unpublished', id: recording.record_id }
end.not_to(change { recording.reload.visibility })
expect(response).to have_http_status(:forbidden)
end
end
context 'Unknown visibility' do
it 'returns :forbidden and does not update the recording' do
expect_any_instance_of(BigBlueButtonApi).not_to receive(:publish_recordings)
expect_any_instance_of(BigBlueButtonApi).not_to receive(:update_recordings)
......@@ -238,7 +255,7 @@ RSpec.describe Api::V1::RecordingsController, type: :controller do
post :update_visibility, params: { visibility: '404', id: recording.record_id }
end.not_to(change { recording.reload.visibility })
expect(response).to have_http_status(:bad_request)
expect(response).to have_http_status(:forbidden)
end
end
......@@ -248,6 +265,9 @@ RSpec.describe Api::V1::RecordingsController, type: :controller do
before do
sign_in_user(signed_in_user)
# IDK where this is created so, small hack to remove it
RolePermission.find_by(permission: Permission.find_by(name: 'AccessToVisibilities'), value: 'false').destroy
end
it 'allows a shared user to update a recording visibility' do
......
......@@ -25,9 +25,11 @@ FactoryBot.define do
perm = Permission.find_or_create_by(name: 'CreateRoom')
perm2 = Permission.find_or_create_by(name: 'RoomLimit')
perm3 = Permission.find_or_create_by(name: 'SharedList')
perm4 = Permission.find_or_create_by(name: 'AccessToVisibilities')
RolePermission.find_or_create_by(permission: perm, role:, value: 'true')
RolePermission.find_or_create_by(permission: perm2, role:, value: '100')
RolePermission.find_or_create_by(permission: perm3, role:, value: 'true')
RolePermission.find_or_create_by(permission: perm4, role:, value: Recording::VISIBILITIES.values)
end
trait :with_super_admin do
......
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment