diff --git a/client/admin/permissions/EditRolePage.js b/client/admin/permissions/EditRolePage.js
index 5c98042b6a42966b5c88ef5170d9096ba6d7540d..0953551a129dfbac124149aa493e5908c5d7f56f 100644
--- a/client/admin/permissions/EditRolePage.js
+++ b/client/admin/permissions/EditRolePage.js
@@ -27,7 +27,7 @@ const EditRolePage = ({ data }) => {
 	const usersInRoleRouter = useRoute('admin-permissions');
 	const router = useRoute('admin-permissions');
 
-	const { values, handlers } = useForm({
+	const { values, handlers, hasUnsavedChanges } = useForm({
 		name: data.name,
 		description: data.description || '',
 		scope: data.scope || 'Users',
@@ -69,7 +69,7 @@ const EditRolePage = ({ data }) => {
 				<RoleForm values={values} handlers={handlers} editing isProtected={data.protected}/>
 				<Field>
 					<Field.Row>
-						<Button primary w='full' onClick={handleSave}>{t('Save')}</Button>
+						<Button primary w='full' disabled={!hasUnsavedChanges} onClick={handleSave}>{t('Save')}</Button>
 					</Field.Row>
 				</Field>
 				{!data.protected && <Field>
diff --git a/client/channel/UserCard/index.js b/client/channel/UserCard/index.js
index aa496ef6acb57bc76f002e891418e43ba92d88c8..e01358eab9f4dba729eb5edddd0d5b15003972cc 100644
--- a/client/channel/UserCard/index.js
+++ b/client/channel/UserCard/index.js
@@ -12,10 +12,13 @@ import { LocalTime } from '../../components/basic/UTCClock';
 import { useUserInfoActions, useUserInfoActionsSpread } from '../hooks/useUserInfoActions';
 import { useComponentDidUpdate } from '../../hooks/useComponentDidUpdate';
 import { useCurrentRoute } from '../../contexts/RouterContext';
+import { useRolesDescription } from '../../contexts/AuthorizationContext';
 
 const UserCardWithData = ({ username, onClose, target, open, rid }) => {
 	const ref = useRef(target);
 
+	const getRoles = useRolesDescription();
+
 	const t = useTranslation();
 
 	const showRealNames = useSetting('UI_Use_Real_Name');
@@ -54,7 +57,7 @@ const UserCardWithData = ({ username, onClose, target, open, rid }) => {
 			_id,
 			name: showRealNames ? name : username,
 			username,
-			roles: roles && roles.map((role, index) => (
+			roles: roles && getRoles(roles).map((role, index) => (
 				<UserCard.Role key={index}>{role}</UserCard.Role>
 			)),
 			bio,
@@ -66,7 +69,7 @@ const UserCardWithData = ({ username, onClose, target, open, rid }) => {
 			customStatus: statusText,
 			nickname,
 		};
-	}, [data, username, showRealNames, state]);
+	}, [data, username, showRealNames, state, getRoles]);
 
 	const handleOpen = useMutableCallback((e) => {
 		open && open(e);
diff --git a/client/channel/UserInfo/index.js b/client/channel/UserInfo/index.js
index e8549af9b4e1e9052ac8167e8fbe42859f4cb557..640902f7a1b796649befd5fb0d582891d4a18228 100644
--- a/client/channel/UserInfo/index.js
+++ b/client/channel/UserInfo/index.js
@@ -13,10 +13,13 @@ import UserCard from '../../components/basic/UserCard';
 import { FormSkeleton } from '../../admin/users/Skeleton';
 import VerticalBar from '../../components/basic/VerticalBar';
 import UserActions from './actions/UserActions';
+import { useRolesDescription } from '../../contexts/AuthorizationContext';
 
 export const UserInfoWithData = React.memo(function UserInfoWithData({ uid, username, tabBar, rid, onClose, video, showBackButton, ...props }) {
 	const t = useTranslation();
 
+	const getRoles = useRolesDescription();
+
 	const showRealNames = useSetting('UI_Use_Real_Name');
 
 	const { data, state, error } = useEndpointDataExperimental(
@@ -44,7 +47,7 @@ export const UserInfoWithData = React.memo(function UserInfoWithData({ uid, user
 			name: showRealNames ? name : username,
 			username,
 			lastLogin,
-			roles: roles.map((role, index) => (
+			roles: roles && getRoles(roles).map((role, index) => (
 				<UserCard.Role key={index}>{role}</UserCard.Role>
 			)),
 			bio,
@@ -58,7 +61,7 @@ export const UserInfoWithData = React.memo(function UserInfoWithData({ uid, user
 			customStatus: statusText,
 			nickname,
 		};
-	}, [data, showRealNames]);
+	}, [data, showRealNames, getRoles]);
 
 	return (
 		<VerticalBar>
diff --git a/client/contexts/AuthorizationContext.ts b/client/contexts/AuthorizationContext.ts
index e9aa28cdeb42fc77f2819b3b485ad5f26f3e780e..83c6e07c84e34eed360d03b9af1fa64506d08d9c 100644
--- a/client/contexts/AuthorizationContext.ts
+++ b/client/contexts/AuthorizationContext.ts
@@ -1,5 +1,15 @@
-import { createContext, useContext, useMemo } from 'react';
+import { createContext, useContext, useMemo, useCallback } from 'react';
 import { useSubscription, Subscription, Unsubscribe } from 'use-subscription';
+import EventEmitter from 'wolfy87-eventemitter';
+
+import { IRole } from '../../definition/IUser';
+
+type IRoles = { [_id: string]: IRole }
+
+
+export class RoleStore extends EventEmitter {
+	roles: IRoles = {};
+}
 
 export type AuthorizationContextValue = {
 	queryPermission(
@@ -15,8 +25,10 @@ export type AuthorizationContextValue = {
 		scope?: string | Mongo.ObjectID
 	): Subscription<boolean>;
 	queryRole(role: string | Mongo.ObjectID): Subscription<boolean>;
+	roleStore: RoleStore;
 };
 
+
 export const AuthorizationContext = createContext<AuthorizationContextValue>({
 	queryPermission: () => ({
 		getCurrentValue: (): boolean => false,
@@ -34,6 +46,7 @@ export const AuthorizationContext = createContext<AuthorizationContextValue>({
 		getCurrentValue: (): boolean => false,
 		subscribe: (): Unsubscribe => (): void => undefined,
 	}),
+	roleStore: new RoleStore(),
 });
 
 export const usePermission = (
@@ -72,6 +85,28 @@ export const useAllPermissions = (
 	return useSubscription(subscription);
 };
 
+export const useRolesDescription = (): (ids: Array<string>) => [string] => {
+	const { roleStore } = useContext(AuthorizationContext);
+
+	const subscription = useMemo(
+		() => ({
+			getCurrentValue: (): IRoles => roleStore.roles,
+			subscribe: (callback: Function): () => void => {
+				roleStore.on('change', callback);
+				return (): void => {
+					roleStore.off('change', callback);
+				};
+			},
+		}),
+		[roleStore],
+	);
+
+	const roles = useSubscription<IRoles>(subscription);
+
+	return useCallback((values) => values.map((role: string) => (roles[role] && roles[role].description) || role)
+		, [roles]) as (ids: Array<string>) => [string];
+};
+
 export const useRole = (role: string | Mongo.ObjectID): boolean => {
 	const { queryRole } = useContext(AuthorizationContext);
 	const subscription = useMemo(() => queryRole(role), [queryRole, role]);
diff --git a/client/providers/AuthorizationProvider.tsx b/client/providers/AuthorizationProvider.tsx
index a934bd69af1d9e8c5c24183e64ed1415d8a2e94d..c80c57f9c63e79bebe15d1b5782a0e02ea7a6a40 100644
--- a/client/providers/AuthorizationProvider.tsx
+++ b/client/providers/AuthorizationProvider.tsx
@@ -1,4 +1,4 @@
-import React, { FC } from 'react';
+import React, { FC, useCallback, useEffect } from 'react';
 import { Meteor } from 'meteor/meteor';
 
 import {
@@ -7,8 +7,11 @@ import {
 	hasAllPermission,
 	hasRole,
 } from '../../app/authorization/client';
-import { AuthorizationContext } from '../contexts/AuthorizationContext';
+import { AuthorizationContext, RoleStore } from '../contexts/AuthorizationContext';
 import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory';
+import { useReactiveValue } from '../hooks/useReactiveValue';
+import { Roles } from '../../app/models/client/models/Roles';
+
 
 const contextValue = {
 	queryPermission: createReactiveSubscriptionFactory(
@@ -23,9 +26,22 @@ const contextValue = {
 	queryRole: createReactiveSubscriptionFactory(
 		(role) => hasRole(Meteor.userId(), role),
 	),
+	roleStore: new RoleStore(),
 };
 
-const AuthorizationProvider: FC = ({ children }) =>
-	<AuthorizationContext.Provider children={children} value={contextValue} />;
+
+const AuthorizationProvider: FC = ({ children }) => {
+	const roles = useReactiveValue(useCallback(() => Roles.find().fetch().reduce((ret, obj) => {
+		ret[obj._id] = obj;
+		return ret;
+	}, {}), []));
+
+	useEffect(() => {
+		contextValue.roleStore.roles = roles;
+		contextValue.roleStore.emit('change', roles);
+	}, [roles]);
+
+	return <AuthorizationContext.Provider children={children} value={contextValue} />;
+};
 
 export default AuthorizationProvider;
diff --git a/definition/IUser.ts b/definition/IUser.ts
index fb1628d3b674abc1a0644f1fb9953ea9fab7f229..cb55aa40b9ec5548a5a2ae44e2956ced94c44a28 100644
--- a/definition/IUser.ts
+++ b/definition/IUser.ts
@@ -73,6 +73,15 @@ export interface IUserSettings {
 	};
 }
 
+export interface IRole {
+	description: string;
+	mandatory2fa?: boolean;
+	name: string;
+	protected: boolean;
+	scope?: string;
+	_id: string;
+}
+
 export interface IUser {
 	_id: string;
 	createdAt: Date;
diff --git a/package-lock.json b/package-lock.json
index b5250204b1f893279b5a516885304ac54ae2ac44..8399ea55cac3ce3d68a0e7abea3637e8927062ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8742,6 +8742,14 @@
 				}
 			}
 		},
+		"@types/wolfy87-eventemitter": {
+			"version": "5.2.0",
+			"resolved": "https://registry.npmjs.org/@types/wolfy87-eventemitter/-/wolfy87-eventemitter-5.2.0.tgz",
+			"integrity": "sha512-N1moqePknZH927suyuzL9rdGLMwc+Ls7VW5WNlmfG3Zv/ZZhCigPXWiBJ7wy1Up4JVQJTo+S1vCiqYIyQw/Lfw==",
+			"requires": {
+				"wolfy87-eventemitter": "*"
+			}
+		},
 		"@types/ws": {
 			"version": "5.1.2",
 			"resolved": "https://registry.npmjs.org/@types/ws/-/ws-5.1.2.tgz",
diff --git a/package.json b/package.json
index b362b6b3c020016379118b7073adee5a2465cd57..46e6319991c5a8089098dde5cc291dba56ddbd5d 100644
--- a/package.json
+++ b/package.json
@@ -145,6 +145,7 @@
 		"@types/mkdirp": "^1.0.1",
 		"@types/underscore.string": "0.0.38",
 		"@types/use-subscription": "^1.0.0",
+		"@types/wolfy87-eventemitter": "^5.2.0",
 		"@types/xml-crypto": "^1.4.1",
 		"@types/xmldom": "^0.1.30",
 		"adm-zip": "RocketChat/adm-zip",