Unverified Commit e9811658 authored by Gung Wah's avatar Gung Wah Committed by GitHub
Browse files

[CHORE] Add permissions to Redux (#2914)



* [FIX] Add permissions to Redux store

* add only permissions being used in the app

* add clear permissions reducer

* call RocketChat.hasPermission from reducer

* add server version comparison on getPermissions

* refactor hasPermission function

* refactor hasPermission function

* remove uncomment code

* use Q.experimentalSortBy()

* add coerce function

* Change Rocketchat.hasPermission

* Apply on isReadOnly

* Apply to RoomInfoEditView

* Apply to RoomInfoView and RoomInfoEditView

* canAutoTranslate

* Unnecessary clear permissions

* Revert getUpdatedSince

* Naming fix
Co-authored-by: default avatarDiego Mello <diegolmello@gmail.com>
parent 6e32a15c
......@@ -70,3 +70,5 @@ export const SETTINGS = createRequestTypes('SETTINGS', ['CLEAR', 'ADD']);
export const APP_STATE = createRequestTypes('APP_STATE', ['FOREGROUND', 'BACKGROUND']);
export const ENTERPRISE_MODULES = createRequestTypes('ENTERPRISE_MODULES', ['CLEAR', 'SET']);
export const ENCRYPTION = createRequestTypes('ENCRYPTION', ['INIT', 'STOP', 'DECODE_KEY', 'SET', 'SET_BANNER']);
export const PERMISSIONS = createRequestTypes('PERMISSIONS', ['SET']);
import * as types from './actionsTypes';
export function setPermissions(permissions) {
return {
type: types.PERMISSIONS.SET,
permissions
};
}
......@@ -34,20 +34,24 @@ const MessageActions = React.memo(forwardRef(({
Message_AllowPinning,
Message_AllowStarring,
Message_Read_Receipt_Store_Users,
isMasterDetail
isMasterDetail,
editMessagePermission,
deleteMessagePermission,
forceDeleteMessagePermission,
pinMessagePermission
}, ref) => {
let permissions = {};
const { showActionSheet, hideActionSheet } = useActionSheet();
const getPermissions = async() => {
try {
const permission = ['edit-message', 'delete-message', 'force-delete-message', 'pin-message'];
const permission = [editMessagePermission, deleteMessagePermission, forceDeleteMessagePermission, pinMessagePermission];
const result = await RocketChat.hasPermission(permission, room.rid);
permissions = {
hasEditPermission: result[permission[0]],
hasDeletePermission: result[permission[1]],
hasForceDeletePermission: result[permission[2]],
hasPinPermission: result[permission[3]]
hasEditPermission: result[0],
hasDeletePermission: result[1],
hasForceDeletePermission: result[2],
hasPinPermission: result[3]
};
} catch {
// Do nothing
......@@ -440,7 +444,11 @@ MessageActions.propTypes = {
Message_AllowPinning: PropTypes.bool,
Message_AllowStarring: PropTypes.bool,
Message_Read_Receipt_Store_Users: PropTypes.bool,
server: PropTypes.string
server: PropTypes.string,
editMessagePermission: PropTypes.array,
deleteMessagePermission: PropTypes.array,
forceDeleteMessagePermission: PropTypes.array,
pinMessagePermission: PropTypes.array
};
const mapStateToProps = state => ({
......@@ -452,7 +460,11 @@ const mapStateToProps = state => ({
Message_AllowPinning: state.settings.Message_AllowPinning,
Message_AllowStarring: state.settings.Message_AllowStarring,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users,
isMasterDetail: state.app.isMasterDetail
isMasterDetail: state.app.isMasterDetail,
editMessagePermission: state.permissions['edit-message'],
deleteMessagePermission: state.permissions['delete-message'],
forceDeleteMessagePermission: state.permissions['force-delete-message'],
pinMessagePermission: state.permissions['pin-message']
});
export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions);
import lt from 'semver/functions/lt';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { Q } from '@nozbe/watermelondb';
import coerce from 'semver/functions/coerce';
import orderBy from 'lodash/orderBy';
import database from '../database';
import log from '../../utils/log';
import reduxStore from '../createStore';
import protectedFunction from './helpers/protectedFunction';
import { setPermissions as setPermissionsAction } from '../../actions/permissions';
const PERMISSIONS = [
'add-user-to-any-c-room',
'add-user-to-any-p-room',
'add-user-to-joined-room',
'archive-room',
'auto-translate',
'create-invite-links',
'delete-c',
'delete-message',
'delete-p',
'edit-message',
'edit-room',
'force-delete-message',
'mute-user',
'pin-message',
'post-readonly',
'remove-user',
'set-leader',
'set-moderator',
'set-owner',
'set-react-when-readonly',
'set-readonly',
'toggle-room-e2e-encryption',
'transfer-livechat-guest',
'unarchive-room',
'view-broadcast-member-list',
'view-privileged-setting',
'view-room-administration',
'view-statistics',
'view-user-administration'
];
export async function setPermissions() {
const db = database.active;
const permissionsCollection = db.collections.get('permissions');
const allPermissions = await permissionsCollection.query(Q.where('id', Q.oneOf(PERMISSIONS))).fetch();
const parsed = allPermissions.reduce((acc, item) => ({ ...acc, [item.id]: item.roles }), {});
reduxStore.dispatch(setPermissionsAction(parsed));
}
const getUpdatedSince = (allRecords) => {
try {
......@@ -64,12 +108,13 @@ const updatePermissions = async({ update = [], remove = [], allRecords }) => {
await db.action(async() => {
await db.batch(...batch);
});
return true;
} catch (e) {
log(e);
}
};
export default function() {
export function getPermissions() {
return new Promise(async(resolve) => {
try {
const serverVersion = reduxStore.getState().server.version;
......@@ -78,17 +123,20 @@ export default function() {
const allRecords = await permissionsCollection.query().fetch();
// if server version is lower than 0.73.0, fetches from old api
if (serverVersion && lt(serverVersion, '0.73.0')) {
if (serverVersion && lt(coerce(serverVersion), '0.73.0')) {
// RC 0.66.0
const result = await this.sdk.get('permissions.list');
if (!result.success) {
return resolve();
}
await updatePermissions({ update: result.permissions, allRecords });
const changePermissions = await updatePermissions({ update: result.permissions, allRecords });
if (changePermissions) {
setPermissions();
}
return resolve();
} else {
const params = {};
const updatedSince = await getUpdatedSince(allRecords);
const updatedSince = getUpdatedSince(allRecords);
if (updatedSince) {
params.updatedSince = updatedSince;
}
......@@ -99,7 +147,10 @@ export default function() {
return resolve();
}
await updatePermissions({ update: result.update, remove: result.delete, allRecords });
const changePermissions = await updatePermissions({ update: result.update, remove: result.delete, allRecords });
if (changePermissions) {
setPermissions();
}
return resolve();
}
} catch (e) {
......
......@@ -32,7 +32,7 @@ import readMessages from './methods/readMessages';
import getSettings, { getLoginSettings, setSettings } from './methods/getSettings';
import getRooms from './methods/getRooms';
import getPermissions from './methods/getPermissions';
import { setPermissions, getPermissions } from './methods/getPermissions';
import { getCustomEmojis, setCustomEmojis } from './methods/getCustomEmojis';
import {
getEnterpriseModules, setEnterpriseModules, hasLicense, isOmnichannelModuleAvailable
......@@ -70,7 +70,6 @@ const CERTIFICATE_KEY = 'RC_CERTIFICATE_KEY';
export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY';
export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY';
export const ANALYTICS_EVENTS_KEY = 'RC_ANALYTICS_EVENTS_KEY';
const returnAnArray = obj => obj || [];
const MIN_ROCKETCHAT_VERSION = '0.70.0';
const STATUSES = ['offline', 'online', 'away', 'busy'];
......@@ -740,6 +739,7 @@ const RocketChat = {
getLoginSettings,
setSettings,
getPermissions,
setPermissions,
getCustomEmojis,
setCustomEmojis,
getEnterpriseModules,
......@@ -1172,10 +1172,13 @@ const RocketChat = {
// RC 0.65.0
return this.sdk.get(`${ this.roomTypeToApiType(type) }.roles`, { roomId });
},
/**
* Permissions: array of permissions' roles from redux. Example: [['owner', 'admin'], ['leader']]
* Returns an array of boolean for each permission from permissions arg
*/
async hasPermission(permissions, rid) {
const db = database.active;
const subsCollection = db.collections.get('subscriptions');
const permissionsCollection = db.collections.get('permissions');
let roomRoles = [];
try {
// get the room from database
......@@ -1184,31 +1187,16 @@ const RocketChat = {
roomRoles = room.roles || [];
} catch (error) {
console.log('hasPermission -> Room not found');
return permissions.reduce((result, permission) => {
result[permission] = false;
return result;
}, {});
return permissions.map(() => false);
}
// get permissions from database
try {
const permissionsFiltered = await permissionsCollection.query(Q.where('id', Q.oneOf(permissions))).fetch();
const shareUser = reduxStore.getState().share.user;
const loginUser = reduxStore.getState().login.user;
// get user roles on the server from redux
const userRoles = (shareUser?.roles || loginUser?.roles) || [];
// merge both roles
const mergedRoles = [...new Set([...roomRoles, ...userRoles])];
// return permissions in object format
// e.g. { 'edit-room': true, 'set-readonly': false }
return permissions.reduce((result, permission) => {
result[permission] = false;
const permissionFound = permissionsFiltered.find(p => p.id === permission);
if (permissionFound) {
result[permission] = returnAnArray(permissionFound.roles).some(r => mergedRoles.includes(r));
}
return result;
}, {});
return permissions.map(permission => permission.some(r => mergedRoles.includes(r) ?? false));
} catch (e) {
log(e);
}
......@@ -1438,17 +1426,15 @@ const RocketChat = {
query, count, offset, sort
});
},
async canAutoTranslate() {
const db = database.active;
canAutoTranslate() {
try {
const AutoTranslate_Enabled = reduxStore.getState().settings && reduxStore.getState().settings.AutoTranslate_Enabled;
const { AutoTranslate_Enabled } = reduxStore.getState().settings;
if (!AutoTranslate_Enabled) {
return false;
}
const permissionsCollection = db.collections.get('permissions');
const autoTranslatePermission = await permissionsCollection.find('auto-translate');
const userRoles = (reduxStore.getState().login.user && reduxStore.getState().login.user.roles) || [];
return autoTranslatePermission.roles.some(role => userRoles.includes(role));
const autoTranslatePermission = reduxStore.getState().permissions['auto-translate'];
const userRoles = (reduxStore.getState().login?.user?.roles) ?? [];
return autoTranslatePermission?.some(role => userRoles.includes(role));
} catch (e) {
log(e);
return false;
......
......@@ -18,6 +18,7 @@ import inviteLinks from './inviteLinks';
import createDiscussion from './createDiscussion';
import enterpriseModules from './enterpriseModules';
import encryption from './encryption';
import permissions from './permissions';
import inquiry from '../ee/omnichannel/reducers/inquiry';
......@@ -41,5 +42,6 @@ export default combineReducers({
createDiscussion,
inquiry,
enterpriseModules,
encryption
encryption,
permissions
});
import { PERMISSIONS } from '../actions/actionsTypes';
const initialState = {
permissions: {}
};
export default function permissions(state = initialState, action) {
switch (action.type) {
case PERMISSIONS.SET:
return action.permissions;
default:
return state;
}
}
......@@ -124,6 +124,7 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
// and block the selectServerSuccess raising multiples errors
RocketChat.setSettings();
RocketChat.setCustomEmojis();
RocketChat.setPermissions();
RocketChat.setEnterpriseModules();
let serverInfo;
......
import RocketChat from '../lib/rocketchat';
import reduxStore from '../lib/createStore';
const canPost = async({ rid }) => {
try {
const permission = await RocketChat.hasPermission(['post-readonly'], rid);
return permission && permission['post-readonly'];
} catch {
// do nothing
}
return false;
const canPostReadOnly = async({ rid }) => {
// TODO: this is not reactive. If this permission changes, the component won't be updated
const postReadOnlyPermission = reduxStore.getState().permissions['post-readonly'];
const permission = await RocketChat.hasPermission([postReadOnlyPermission], rid);
return permission[0];
};
const isMuted = (room, user) => room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username);
......@@ -20,7 +18,7 @@ export const isReadOnly = async(room, user) => {
return true;
}
if (room?.ro) {
const allowPost = await canPost(room);
const allowPost = await canPostReadOnly(room);
if (allowPost) {
return false;
}
......
......@@ -53,7 +53,15 @@ class RoomActionsView extends React.Component {
closeRoom: PropTypes.func,
theme: PropTypes.string,
fontScale: PropTypes.number,
serverVersion: PropTypes.string
serverVersion: PropTypes.string,
addUserToJoinedRoomPermission: PropTypes.array,
addUserToAnyCRoomPermission: PropTypes.array,
addUserToAnyPRoomPermission: PropTypes.array,
createInviteLinksPermission: PropTypes.array,
editRoomPermission: PropTypes.array,
toggleRoomE2EEncryptionPermission: PropTypes.array,
viewBroadcastMemberListPermission: PropTypes.array,
transferLivechatGuestPermission: PropTypes.array
}
constructor(props) {
......@@ -118,7 +126,7 @@ class RoomActionsView extends React.Component {
this.updateRoomMember();
}
const canAutoTranslate = await RocketChat.canAutoTranslate();
const canAutoTranslate = RocketChat.canAutoTranslate();
this.setState({ canAutoTranslate });
this.canAddUser();
......@@ -159,60 +167,62 @@ class RoomActionsView extends React.Component {
canAddUser = async() => {
const { room, joined } = this.state;
const { addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission } = this.props;
const { rid, t } = room;
let canAdd = false;
let canAddUser = false;
const userInRoom = joined;
const permissions = await RocketChat.hasPermission(['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], rid);
const permissions = await RocketChat.hasPermission([addUserToJoinedRoomPermission, addUserToAnyCRoomPermission, addUserToAnyPRoomPermission], rid);
if (permissions) {
if (userInRoom && permissions['add-user-to-joined-room']) {
canAdd = true;
}
if (t === 'c' && permissions['add-user-to-any-c-room']) {
canAdd = true;
}
if (t === 'p' && permissions['add-user-to-any-p-room']) {
canAdd = true;
}
if (userInRoom && permissions[0]) {
canAddUser = true;
}
if (t === 'c' && permissions[1]) {
canAddUser = true;
}
this.setState({ canAddUser: canAdd });
if (t === 'p' && permissions[2]) {
canAddUser = true;
}
this.setState({ canAddUser });
}
canInviteUser = async() => {
const { room } = this.state;
const { createInviteLinksPermission } = this.props;
const { rid } = room;
const permissions = await RocketChat.hasPermission(['create-invite-links'], rid);
const permissions = await RocketChat.hasPermission([createInviteLinksPermission], rid);
const canInviteUser = permissions && permissions['create-invite-links'];
const canInviteUser = permissions[0];
this.setState({ canInviteUser });
}
canEdit = async() => {
const { room } = this.state;
const { editRoomPermission } = this.props;
const { rid } = room;
const permissions = await RocketChat.hasPermission(['edit-room'], rid);
const permissions = await RocketChat.hasPermission([editRoomPermission], rid);
const canEdit = permissions && permissions['edit-room'];
const canEdit = permissions[0];
this.setState({ canEdit });
}
canToggleEncryption = async() => {
const { room } = this.state;
const { toggleRoomE2EEncryptionPermission } = this.props;
const { rid } = room;
const permissions = await RocketChat.hasPermission(['toggle-room-e2e-encryption'], rid);
const permissions = await RocketChat.hasPermission([toggleRoomE2EEncryptionPermission], rid);
const canToggleEncryption = permissions && permissions['toggle-room-e2e-encryption'];
const canToggleEncryption = permissions[0];
this.setState({ canToggleEncryption });
}
canViewMembers = async() => {
const { room } = this.state;
const { viewBroadcastMemberListPermission } = this.props;
const { rid, t, broadcast } = room;
if (broadcast) {
const viewBroadcastMemberListPermission = 'view-broadcast-member-list';
const permissions = await RocketChat.hasPermission([viewBroadcastMemberListPermission], rid);
if (!permissions[viewBroadcastMemberListPermission]) {
if (!permissions[0]) {
return false;
}
}
......@@ -226,16 +236,10 @@ class RoomActionsView extends React.Component {
canForwardGuest = async() => {
const { room } = this.state;
const { transferLivechatGuestPermission } = this.props;
const { rid } = room;
let result = true;
const transferLivechatGuest = 'transfer-livechat-guest';
const permissions = await RocketChat.hasPermission([transferLivechatGuest], rid);
if (!permissions[transferLivechatGuest]) {
result = false;
}
this.setState({ canForwardGuest: result });
const permissions = await RocketChat.hasPermission([transferLivechatGuestPermission], rid);
this.setState({ canForwardGuest: permissions[0] });
}
canReturnQueue = async() => {
......@@ -866,7 +870,15 @@ class RoomActionsView extends React.Component {
const mapStateToProps = state => ({
jitsiEnabled: state.settings.Jitsi_Enabled || false,
encryptionEnabled: state.encryption.enabled,
serverVersion: state.server.version
serverVersion: state.server.version,
addUserToJoinedRoomPermission: state.permissions['add-user-to-joined-room'],
addUserToAnyCRoomPermission: state.permissions['add-user-to-any-c-room'],
addUserToAnyPRoomPermission: state.permissions['add-user-to-any-p-room'],
createInviteLinksPermission: state.permissions['create-invite-links'],
editRoomPermission: state.permissions['edit-room'],
toggleRoomE2EEncryptionPermission: state.permissions['toggle-room-e2e-encryption'],
viewBroadcastMemberListPermission: state.permissions['view-broadcast-member-list'],
transferLivechatGuestPermission: state.permissions['transfer-livechat-guest']
});
const mapDispatchToProps = dispatch => ({
......
......@@ -44,14 +44,6 @@ const PERMISSION_ARCHIVE = 'archive-room';
const PERMISSION_UNARCHIVE = 'unarchive-room';
const PERMISSION_DELETE_C = 'delete-c';
const PERMISSION_DELETE_P = 'delete-p';
const PERMISSIONS_ARRAY = [
PERMISSION_SET_READONLY,
PERMISSION_SET_REACT_WHEN_READONLY,
PERMISSION_ARCHIVE,
PERMISSION_UNARCHIVE,
PERMISSION_DELETE_C,
PERMISSION_DELETE_P
];
class RoomInfoEditView extends React.Component {
static navigationOptions = () => ({
......@@ -63,7 +55,13 @@ class RoomInfoEditView extends React.Component {
deleteRoom: PropTypes.func,
serverVersion: PropTypes.string,
encryptionEnabled: PropTypes.bool,
theme: PropTypes.string
theme: PropTypes.string,
setReadOnlyPermission: PropTypes.array,
setReactWhenReadOnlyPermission: PropTypes.array,
archiveRoomPermission: PropTypes.array,
unarchiveRoomPermission: PropTypes.array,
deleteCPermission: PropTypes.array,
deletePPermission: PropTypes.array
};
constructor(props) {
......@@ -108,7 +106,15 @@ class RoomInfoEditView extends React.Component {
// eslint-disable-next-line react/sort-comp
loadRoom = async() => {
const { route } = this.props;
const {
route,
setReadOnlyPermission,
setReactWhenReadOnlyPermission,
archiveRoomPermission,
unarchiveRoomPermission,
deleteCPermission,
deletePPermission
} = this.props;
const rid = route.params?.rid;
if (!rid) {
return;
......@@ -123,8 +129,25 @@ class RoomInfoEditView extends React.Component {
this.init(this.room);
});
const permissions = await RocketChat.hasPermission(PERMISSIONS_ARRAY, rid);
this.setState({ permissions });
const result = await RocketChat.hasPermission([
setReadOnlyPermission,
setReactWhenReadOnlyPermission,
archiveRoomPermission,
unarchiveRoomPermission,
deleteCPermission,
deletePPermission
], rid);
this.setState({
permissions: {
[PERMISSION_SET_READONLY]: result[0],
[PERMISSION_SET_REACT_WHEN_READONLY]: result[1],
[PERMISSION_ARCHIVE]: result[2],
[PERMISSION_UNARCHIVE]: result[3],
[PERMISSION_DELETE_C]: result[4],
[PERMISSION_DELETE_P]: result[5]
}
});
} catch (e) {
log(e);
}
......@@ -667,7 +690,13 @@ class RoomInfoEditView extends React.Component {
const mapStateToProps = state => ({
serverVersion: state.server.version,
encryptionEnabled: state.encryption.enabled
encryptionEnabled: state.encryption.enabled,
setReadOnlyPermission: state.permissions[PERMISSION_SET_READONLY],
setReactWhenReadOnlyPermission: state.permissions[PERMISSION_SET_REACT_WHEN_READONLY],
archiveRoomPermission: state.permissions[PERMISSION_ARCHIVE],
unarchiveRoomPermission: state.permissions[PERMISSION_UNARCHIVE],
deleteCPermission: state.permissions[PERMISSION_DELETE_C],
deletePPermission: state.permissions[PERMISSION_DELETE_P]
});
const mapDispatchToProps = dispatch => ({
......
......@@ -31,7 +31,6 @@ import SafeAreaView from '../../containers/SafeAreaView';
import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation';
const PERMISSION_EDIT_ROOM = 'edit-room';
const getRoomTitle = (room, type, name, username, statusText, theme) => (type === 'd'
? (
<>
......@@ -55,7 +54,8 @@ class RoomInfoView extends React.Component {
rooms: PropTypes.array,
theme: PropTypes.string,
isMasterDetail: PropTypes.bool,
jitsiEnabled: PropTypes.bool
jitsiEnabled: PropTypes.bool,
editRoomPermission: PropTypes.array
}
constructor(props) {
......@@ -193,7 +193,7 @@ class RoomInfoView extends React.Component {