Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F33497221
D14505.1769054706.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
D14505.1769054706.diff
View Options
diff --git a/lib/permissions/minimally-encoded-thread-permissions.js b/lib/permissions/minimally-encoded-thread-permissions.js
--- a/lib/permissions/minimally-encoded-thread-permissions.js
+++ b/lib/permissions/minimally-encoded-thread-permissions.js
@@ -1,23 +1,40 @@
// @flow
import invariant from 'invariant';
+import _isEqual from 'lodash/fp/isEqual.js';
+import _mapValues from 'lodash/fp/mapValues.js';
import {
- parseThreadPermissionString,
constructThreadPermissionString,
+ parseThreadPermissionString,
} from './prefixes.js';
+import { specialRoles } from './special-roles.js';
import {
+ getAllThreadPermissions,
+ getRolePermissionBlobs,
+ makePermissionsBlob,
+ makePermissionsForChildrenBlob,
+} from './thread-permissions.js';
+import type { ThreadStoreOperation } from '../ops/thread-store-ops.js';
+import { getChildThreads } from '../selectors/thread-selectors.js';
+import type {
+ MinimallyEncodedThickMemberInfo,
+ ThickRawThreadInfo,
+} from '../types/minimally-encoded-thread-permissions-types.js';
+import {
+ assertThreadPermission,
+ assertThreadPermissionFilterPrefix,
+ assertThreadPermissionMembershipPrefix,
+ assertThreadPermissionPropagationPrefix,
type ThreadPermission,
type ThreadPermissionInfo,
+ threadPermissions,
+ type ThreadPermissionsBlob,
type ThreadPermissionsInfo,
type ThreadRolePermissionsBlob,
- threadPermissions,
- assertThreadPermission,
- assertThreadPermissionPropagationPrefix,
- assertThreadPermissionFilterPrefix,
- assertThreadPermissionMembershipPrefix,
} from '../types/thread-permission-types.js';
-import { entries, invertObjectToMap } from '../utils/objects.js';
+import type { RawThreadInfos } from '../types/thread-types.js';
+import { entries, invertObjectToMap, values } from '../utils/objects.js';
import type { TRegex } from '../utils/validation-utils.js';
import { tRegex } from '../utils/validation-utils.js';
@@ -246,6 +263,213 @@
]),
);
+function updateRolesAndPermissions(
+ threads: RawThreadInfos,
+ rolePermissionsUpdater: ThreadRolePermissionsBlob => ThreadRolePermissionsBlob,
+): {
+ +operations: $ReadOnlyArray<ThreadStoreOperation>,
+} {
+ const updatedThreads = { ...threads };
+
+ const childThreads = getChildThreads(threads);
+ const threadChildrenIDs = _mapValues(
+ threadChildren => threadChildren.map(thread => thread.id),
+ childThreads,
+ );
+ function updateRoles(threadID: string) {
+ const threadInfo = updatedThreads[threadID];
+
+ const roles = { ...threadInfo.roles };
+ for (const roleID in roles) {
+ const role = roles[roleID];
+
+ const rolePermissionBlobs = getRolePermissionBlobs(threadInfo.type);
+ let updatedPermissionsBlob;
+ if (threadInfo.thick) {
+ // Each thick threads has exactly one role - for its members. We
+ // don't allow managing this role, which means we can simply get the
+ // computed permissions from the rolePermissionBlobs.
+ updatedPermissionsBlob = rolePermissionBlobs.Members;
+ } else if (
+ role.specialRole === specialRoles.ADMIN_ROLE &&
+ rolePermissionBlobs.Admins
+ ) {
+ // We don't allow managing the admins role, so in this case we can
+ // also use the result of getRolePermissionBlobs that should contain
+ // all the required changes.
+ updatedPermissionsBlob = rolePermissionBlobs.Admins;
+ } else {
+ // We allow admins to manage non-admin roles, which means we can't
+ // simply rely on the result of getRolePermissionBlobs - it doesn't
+ // contain any changes made by admins. In this case, we're running the
+ // rolePermissionsUpdater that updates the role accordingly. It is
+ // similar to what we do on the keyserver during a migration in
+ // addNewUserSurfacedPermission inside migration-config.js.
+ updatedPermissionsBlob = rolePermissionsUpdater(
+ decodeThreadRolePermissionsBitmaskArray(role.permissions),
+ );
+ }
+ const encodedUpdatedPermissions = threadRolePermissionsBlobToBitmaskArray(
+ updatedPermissionsBlob,
+ );
+ roles[roleID] = {
+ ...role,
+ permissions: encodedUpdatedPermissions,
+ };
+ updatedThreads[threadID] = {
+ ...threadInfo,
+ roles,
+ };
+ }
+ }
+
+ type MemberToThreadPermissions = {
+ [member: string]: ?ThreadPermissionsBlob,
+ };
+
+ function updateThickThreadMembers(
+ threadInfo: ThickRawThreadInfo,
+ memberToThreadPermissionsFromParent: ?MemberToThreadPermissions,
+ ): {
+ +members: $ReadOnlyArray<MinimallyEncodedThickMemberInfo>,
+ +memberToThreadPermissionsForChildren: MemberToThreadPermissions,
+ } {
+ const updatedMembers = [];
+ const memberToThreadPermissionsForChildren: MemberToThreadPermissions = {};
+ for (const member of threadInfo.members) {
+ const { id, role } = member;
+
+ const rolePermissions = role
+ ? decodeThreadRolePermissionsBitmaskArray(
+ threadInfo.roles[role].permissions,
+ )
+ : null;
+ const permissionsFromParent = memberToThreadPermissionsFromParent?.[id];
+
+ const computedPermissions = makePermissionsBlob(
+ rolePermissions,
+ permissionsFromParent,
+ threadInfo.id,
+ threadInfo.type,
+ );
+
+ updatedMembers.push({
+ ...member,
+ permissions: permissionsToBitmaskHex(
+ getAllThreadPermissions(computedPermissions, threadInfo.id),
+ ),
+ });
+
+ memberToThreadPermissionsForChildren[member.id] =
+ makePermissionsForChildrenBlob(computedPermissions);
+ }
+ return {
+ members: updatedMembers,
+ memberToThreadPermissionsForChildren,
+ };
+ }
+
+ function recursivelyUpdateThickThreadMemberPermissions(
+ threadID: string,
+ memberToThreadPermissionsFromParent: ?MemberToThreadPermissions,
+ ) {
+ const threadInfo = updatedThreads[threadID];
+ if (!threadInfo.thick) {
+ // We don't update members of thin threads because we aren't storing
+ // their permissions inside their member info. Member permissions are
+ // instead computed on the fly based on the roles. We're planning to
+ // use the same approach for thick threads in the future.
+ return;
+ }
+
+ const { members: updatedMembers, memberToThreadPermissionsForChildren } =
+ updateThickThreadMembers(threadInfo, memberToThreadPermissionsFromParent);
+ updatedThreads[threadID] = {
+ ...threadInfo,
+ members: updatedMembers,
+ };
+ for (const childID of threadChildrenIDs[threadID]) {
+ recursivelyUpdateThickThreadMemberPermissions(
+ childID,
+ memberToThreadPermissionsForChildren,
+ );
+ }
+ }
+
+ function recursivelyUpdateCurrentUserPermissions(
+ threadID: string,
+ permissionsFromParent: ?ThreadPermissionsBlob,
+ ) {
+ const threadInfo = updatedThreads[threadID];
+ const { currentUser, roles } = threadInfo;
+ const { role } = currentUser;
+
+ const rolePermissions = role
+ ? decodeThreadRolePermissionsBitmaskArray(roles[role].permissions)
+ : null;
+ const computedPermissions = makePermissionsBlob(
+ rolePermissions,
+ permissionsFromParent,
+ threadInfo.id,
+ threadInfo.type,
+ );
+
+ updatedThreads[threadID] = {
+ ...threadInfo,
+ currentUser: {
+ ...currentUser,
+ permissions: permissionsToBitmaskHex(
+ getAllThreadPermissions(computedPermissions, threadInfo.id),
+ ),
+ },
+ };
+
+ for (const childID of threadChildrenIDs[threadID]) {
+ recursivelyUpdateCurrentUserPermissions(
+ childID,
+ makePermissionsForChildrenBlob(computedPermissions),
+ );
+ }
+ }
+
+ for (const thread of values(threads)) {
+ if (!thread.parentThreadID) {
+ // We don't need to update these recursively because roles don't
+ // cascade from parents - modifying a parent thread role doesn't have
+ // any impact on children roles. This stays in contrast to how our
+ // permissions work - setting a parent role can affect which
+ // permissions are granted in the children threads, because we're
+ // propagating the permissions (based on a couple of strategies).
+ updateRoles(thread.id);
+ }
+ }
+ const rootThreadIDs = values(threads)
+ .filter(thread => !thread.parentThreadID)
+ .map(thread => thread.id);
+ rootThreadIDs.forEach(threadID =>
+ recursivelyUpdateThickThreadMemberPermissions(threadID, null),
+ );
+ rootThreadIDs.forEach(threadID =>
+ recursivelyUpdateCurrentUserPermissions(threadID, null),
+ );
+
+ const operations = values(updatedThreads)
+ .filter(
+ updatedThread => !_isEqual(updatedThread, threads[updatedThread.id]),
+ )
+ .map(thread => ({
+ type: 'replace',
+ payload: {
+ id: thread.id,
+ threadInfo: thread,
+ },
+ }));
+
+ return {
+ operations,
+ };
+}
+
export {
permissionsToBitmaskHex,
threadPermissionsFromBitmaskHex,
@@ -254,6 +478,7 @@
decodeRolePermissionBitmask,
threadRolePermissionsBlobToBitmaskArray,
decodeThreadRolePermissionsBitmaskArray,
+ updateRolesAndPermissions,
tHexEncodedRolePermission,
tHexEncodedPermissionsBitmask,
};
diff --git a/lib/selectors/thread-selectors.js b/lib/selectors/thread-selectors.js
--- a/lib/selectors/thread-selectors.js
+++ b/lib/selectors/thread-selectors.js
@@ -180,28 +180,29 @@
},
);
+function getChildThreads<T: RawThreadInfo | ThreadInfo>(threadInfos: {
+ +[id: string]: T,
+}): { +[id: string]: Array<T> } {
+ const result: {
+ [string]: T[],
+ } = {};
+ for (const id in threadInfos) {
+ const threadInfo = threadInfos[id];
+ const parentThreadID = threadInfo.parentThreadID;
+ if (parentThreadID === null || parentThreadID === undefined) {
+ continue;
+ }
+ if (result[parentThreadID] === undefined) {
+ result[parentThreadID] = ([]: T[]);
+ }
+ result[parentThreadID].push(threadInfo);
+ }
+ return result;
+}
+
const childThreadInfos: (state: BaseAppState<>) => {
+[id: string]: $ReadOnlyArray<ThreadInfo>,
-} = createSelector(
- threadInfoSelector,
- (threadInfos: { +[id: string]: ThreadInfo }) => {
- const result: {
- [string]: ThreadInfo[],
- } = {};
- for (const id in threadInfos) {
- const threadInfo = threadInfos[id];
- const parentThreadID = threadInfo.parentThreadID;
- if (parentThreadID === null || parentThreadID === undefined) {
- continue;
- }
- if (result[parentThreadID] === undefined) {
- result[parentThreadID] = ([]: ThreadInfo[]);
- }
- result[parentThreadID].push(threadInfo);
- }
- return result;
- },
-);
+} = createSelector(threadInfoSelector, getChildThreads);
const containedThreadInfos: (state: BaseAppState<>) => {
+[id: string]: $ReadOnlyArray<ThreadInfo>,
@@ -587,4 +588,5 @@
threadInfosSelectorForThreadType,
thickRawThreadInfosSelector,
unreadThickThreadIDsSelector,
+ getChildThreads,
};
diff --git a/native/redux/persist.js b/native/redux/persist.js
--- a/native/redux/persist.js
+++ b/native/redux/persist.js
@@ -2,6 +2,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import invariant from 'invariant';
+import _keyBy from 'lodash/fp/keyBy.js';
import { Platform } from 'react-native';
import Orientation from 'react-native-orientation-locker';
import { createTransform } from 'redux-persist';
@@ -56,6 +57,7 @@
convertUserInfosToReplaceUserOps,
userStoreOpsHandlers,
} from 'lib/ops/user-store-ops.js';
+import { updateRolesAndPermissions } from 'lib/permissions/minimally-encoded-thread-permissions.js';
import { patchRawThreadInfosWithSpecialRole } from 'lib/permissions/special-roles.js';
import { filterThreadIDsInFilterList } from 'lib/reducers/calendar-filters-reducer.js';
import { highestLocalIDSelector } from 'lib/selectors/local-id-selectors.js';
@@ -103,6 +105,10 @@
} from 'lib/types/report-types.js';
import { defaultConnectionInfo } from 'lib/types/socket-types.js';
import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js';
+import {
+ userSurfacedPermissions,
+ type ThreadRolePermissionsBlob,
+} from 'lib/types/thread-permission-types.js';
import type {
ClientDBThreadInfo,
LegacyRawThreadInfo,
@@ -126,9 +132,11 @@
MigrationsManifest,
} from 'lib/utils/migration-utils.js';
import { entries } from 'lib/utils/objects.js';
+import { toggleUserSurfacedPermission } from 'lib/utils/role-utils.js';
import {
deprecatedConvertClientDBThreadInfoToRawThreadInfo,
convertRawThreadInfoToClientDBThreadInfo,
+ convertClientDBThreadInfoToRawThreadInfo,
} from 'lib/utils/thread-ops-utils.js';
import { getUUID } from 'lib/utils/uuid.js';
@@ -1517,6 +1525,32 @@
},
ops: {},
}): MigrationFunction<NavInfo, AppState>),
+ [87]: (async (state: AppState) => {
+ const clientDBThreadInfos = commCoreModule.getAllThreadsSync();
+ const rawThreadInfos = clientDBThreadInfos.map(
+ convertClientDBThreadInfoToRawThreadInfo,
+ );
+ const keyedRawThreadInfos = _keyBy('id')(rawThreadInfos);
+
+ // This function also results in setting DELETE_OWN_MESSAGES and
+ // DELETE_ALL_MESSAGES for admins. It is added automatically based on the
+ // result of getRolePermissionBlobs call.
+ const { operations } = updateRolesAndPermissions(
+ keyedRawThreadInfos,
+ (permissions: ThreadRolePermissionsBlob) =>
+ toggleUserSurfacedPermission(
+ permissions,
+ userSurfacedPermissions.DELETE_OWN_MESSAGES,
+ ),
+ );
+
+ return {
+ state,
+ ops: {
+ threadStoreOperations: operations,
+ },
+ };
+ }: MigrationFunction<NavInfo, AppState>),
});
// NOTE: renaming this object, and especially the `version` property
@@ -1527,7 +1561,7 @@
storage: AsyncStorage,
blacklist: persistBlacklist,
debug: __DEV__,
- version: 86,
+ version: 87,
transforms: [
messageStoreMessagesBlocklistTransform,
reportStoreTransform,
diff --git a/web/redux/persist-constants.js b/web/redux/persist-constants.js
--- a/web/redux/persist-constants.js
+++ b/web/redux/persist-constants.js
@@ -3,6 +3,6 @@
const rootKey = 'root';
const rootKeyPrefix = 'persist:';
const completeRootKey = `${rootKeyPrefix}${rootKey}`;
-const storeVersion = 86;
+const storeVersion = 87;
export { rootKey, rootKeyPrefix, completeRootKey, storeVersion };
diff --git a/web/redux/persist.js b/web/redux/persist.js
--- a/web/redux/persist.js
+++ b/web/redux/persist.js
@@ -12,15 +12,16 @@
type ReplaceKeyserverOperation,
} from 'lib/ops/keyserver-store-ops.js';
import {
- messageStoreOpsHandlers,
- type ReplaceMessageStoreLocalMessageInfoOperation,
type ClientDBMessageStoreOperation,
type MessageStoreOperation,
+ messageStoreOpsHandlers,
+ type ReplaceMessageStoreLocalMessageInfoOperation,
} from 'lib/ops/message-store-ops.js';
import type {
ClientDBThreadStoreOperation,
ThreadStoreOperation,
} from 'lib/ops/thread-store-ops.js';
+import { updateRolesAndPermissions } from 'lib/permissions/minimally-encoded-thread-permissions.js';
import { patchRawThreadInfoWithSpecialRole } from 'lib/permissions/special-roles.js';
import { createUpdateDBOpsForThreadStoreThreadInfos } from 'lib/shared/redux/client-db-utils.js';
import { deprecatedUpdateRolesAndPermissions } from 'lib/shared/redux/deprecated-update-roles-and-permissions.js';
@@ -36,6 +37,8 @@
import { defaultConnectionInfo } from 'lib/types/socket-types.js';
import type { StoreOperations } from 'lib/types/store-ops-types.js';
import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js';
+import type { ThreadRolePermissionsBlob } from 'lib/types/thread-permission-types.js';
+import { userSurfacedPermissions } from 'lib/types/thread-permission-types.js';
import type {
ClientDBThreadInfo,
RawThreadInfos,
@@ -45,14 +48,15 @@
import { isDev } from 'lib/utils/dev-utils.js';
import { stripMemberPermissionsFromRawThreadInfos } from 'lib/utils/member-info-utils.js';
import {
- generateIDSchemaMigrationOpsForDrafts,
convertDraftStoreToNewIDSchema,
createAsyncMigrate,
- type StorageMigrationFunction,
+ generateIDSchemaMigrationOpsForDrafts,
type MigrationFunction,
type MigrationsManifest,
+ type StorageMigrationFunction,
} from 'lib/utils/migration-utils.js';
import { entries, values } from 'lib/utils/objects.js';
+import { toggleUserSurfacedPermission } from 'lib/utils/role-utils.js';
import {
convertClientDBThreadInfoToRawThreadInfo,
convertRawThreadInfoToClientDBThreadInfo,
@@ -732,6 +736,58 @@
},
ops: {},
}): MigrationFunction<WebNavInfo, AppState>),
+ [87]: (async (state: AppState) => {
+ const sharedWorker = await getCommSharedWorker();
+ const isDatabaseSupported = await sharedWorker.isSupported();
+
+ if (!isDatabaseSupported) {
+ return {
+ state,
+ ops: {},
+ };
+ }
+
+ const stores = await sharedWorker.schedule({
+ type: workerRequestMessageTypes.GET_CLIENT_STORE,
+ });
+
+ const clientDBThreadInfos: ?$ReadOnlyArray<ClientDBThreadInfo> =
+ stores?.store?.threads;
+
+ if (
+ clientDBThreadInfos === null ||
+ clientDBThreadInfos === undefined ||
+ clientDBThreadInfos.length === 0
+ ) {
+ return {
+ state,
+ ops: {},
+ };
+ }
+
+ const rawThreadInfos = clientDBThreadInfos.map(
+ convertClientDBThreadInfoToRawThreadInfo,
+ );
+
+ const keyedRawThreadInfos = _keyBy('id')(rawThreadInfos);
+ // This function also results in setting DELETE_OWN_MESSAGES and
+ // DELETE_ALL_MESSAGES for admins. It is added automatically based on the
+ // result of getRolePermissionBlobs call.
+ const { operations } = updateRolesAndPermissions(
+ keyedRawThreadInfos,
+ (permissions: ThreadRolePermissionsBlob) =>
+ toggleUserSurfacedPermission(
+ permissions,
+ userSurfacedPermissions.DELETE_OWN_MESSAGES,
+ ),
+ );
+ return {
+ state,
+ ops: {
+ threadStoreOperations: operations,
+ },
+ };
+ }: MigrationFunction<WebNavInfo, AppState>),
};
const persistConfig: PersistConfig = {
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Jan 22, 4:05 AM (10 h, 17 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5973916
Default Alt Text
D14505.1769054706.diff (17 KB)
Attached To
Mode
D14505: [lib] Migrate delete message permissions on clients
Attached
Detach File
Event Timeline
Log In to Comment