From 2f565994b7e8b2dfcc15a58f9c22125a3f9870db Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 6 Aug 2020 10:19:52 +0300 Subject: [PATCH] CollectionMembers: add pages to control collection memberships. --- src/Collections/CollectionMemberAddDialog.tsx | 120 ++++++++++++++++++ src/Collections/CollectionMembers.tsx | 118 +++++++++++++++++ src/Collections/Main.tsx | 3 +- src/Journals/JournalMemberAddDialog.tsx | 2 +- src/Settings/index.tsx | 11 +- src/widgets/PrettyFingerprint.tsx | 47 +------ 6 files changed, 252 insertions(+), 49 deletions(-) create mode 100644 src/Collections/CollectionMemberAddDialog.tsx create mode 100644 src/Collections/CollectionMembers.tsx diff --git a/src/Collections/CollectionMemberAddDialog.tsx b/src/Collections/CollectionMemberAddDialog.tsx new file mode 100644 index 0000000..5a02ded --- /dev/null +++ b/src/Collections/CollectionMemberAddDialog.tsx @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: © 2017 EteSync Authors +// SPDX-License-Identifier: AGPL-3.0-only + +import * as React from "react"; + +import * as Etebase from "etebase"; + +import TextField from "@material-ui/core/TextField"; +import Checkbox from "@material-ui/core/Checkbox"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; + +import LoadingIndicator from "../widgets/LoadingIndicator"; +import ConfirmationDialog from "../widgets/ConfirmationDialog"; +import PrettyFingerprint from "../widgets/PrettyFingerprint"; +import { CachedCollection } from "../Pim/helpers"; +import { useCredentials } from "../credentials"; + +interface PropsType { + collection: CachedCollection; + onOk: (username: string, publicKey: Uint8Array, accessLevel: Etebase.CollectionAccessLevel) => void; + onClose: () => void; +} + +export default function CollectionMemberAddDialog(props: PropsType) { + const etebase = useCredentials()!; + const [addUser, setAddUser] = React.useState(""); + const [publicKey, setPublicKey] = React.useState(); + const [readOnly, setReadOnly] = React.useState(false); + const [userChosen, setUserChosen] = React.useState(false); + const [error, setError] = React.useState(); + + async function onAddRequest(_user: string) { + setUserChosen(true); + const inviteMgr = etebase.getInvitationManager(); + try { + const userProfile = await inviteMgr.fetchUserProfile(addUser); + setPublicKey(userProfile.pubkey); + } catch (e) { + setError(e); + } + } + + function onOk() { + props.onOk(addUser, publicKey!, readOnly ? Etebase.CollectionAccessLevel.ReadOnly : Etebase.CollectionAccessLevel.ReadWrite); + } + + const { onClose } = props; + + if (error) { + return ( + <> + + User ({addUser}) not found. Have they setup their encryption password from one of the apps? + + + ); + } + + if (publicKey) { + return ( + <> + +

+ Verify {addUser}'s security fingerprint to ensure the encryption is secure. +

+
+ +
+
+ + ); + } else { + return ( + <> + + {userChosen ? + + : + <> + setAddUser(ev.target.value)} + /> + setReadOnly(event.target.checked)} + /> + } + label="Read only?" + /> + + } + + + ); + } +} diff --git a/src/Collections/CollectionMembers.tsx b/src/Collections/CollectionMembers.tsx new file mode 100644 index 0000000..3e46207 --- /dev/null +++ b/src/Collections/CollectionMembers.tsx @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: © 2017 EteSync Authors +// SPDX-License-Identifier: AGPL-3.0-only + +import * as React from "react"; + +import * as Etebase from "etebase"; + +import { List, ListItem } from "../widgets/List"; + +import IconMemberAdd from "@material-ui/icons/PersonAdd"; +import VisibilityIcon from "@material-ui/icons/Visibility"; + +import AppBarOverride from "../widgets/AppBarOverride"; +import Container from "../widgets/Container"; +import LoadingIndicator from "../widgets/LoadingIndicator"; +import ConfirmationDialog from "../widgets/ConfirmationDialog"; + +import { useCredentials } from "../credentials"; +import { getCollectionManager } from "../etebase-helpers"; +import { CachedCollection } from "../Pim/helpers"; + +import CollectionMemberAddDialog from "./CollectionMemberAddDialog"; + +interface PropsType { + collection: CachedCollection; +} + +export default function CollectionMembers(props: PropsType) { + const etebase = useCredentials()!; + const [members_, setMembers] = React.useState(); + const [revokeUser, setRevokeUser] = React.useState(null); + const [addMemberOpen, setAddMemberOpen] = React.useState(false); + // FIXME: add error handling + + const { collection, metadata } = props.collection; + + async function fetchMembers() { + const colMgr = getCollectionManager(etebase); + const memberManager = colMgr.getMemberManager(collection); + const members = await memberManager.list(); + setMembers(members.data); + } + + React.useEffect(() => { + fetchMembers(); + }, []); + + function onRevokeRequest(user: string) { + setRevokeUser(user); + } + + async function onRevokeDo() { + const colMgr = getCollectionManager(etebase); + const memberManager = colMgr.getMemberManager(collection); + await memberManager.remove(revokeUser!); + await fetchMembers(); + setRevokeUser(null); + } + + async function onMemberAdd(username: string, pubkey: Uint8Array, accessLevel: Etebase.CollectionAccessLevel) { + const inviteMgr = etebase.getInvitationManager(); + await inviteMgr.invite(collection, username, pubkey, accessLevel); + await fetchMembers(); + setAddMemberOpen(false); + } + + const members = members_?.filter((x) => x.username !== etebase.user.username); + + return ( + <> + + + {members ? + + } onClick={() => setAddMemberOpen(true)}> + Invite user + + {(members.length > 0 ? + members.map((member) => ( + onRevokeRequest(member.username)} + rightIcon={(member.accessLevel === Etebase.CollectionAccessLevel.ReadOnly) ? () : undefined} + > + {member.username} + + )) + : + + No members + + )} + + : + + } + + setRevokeUser(null)} + > + Would you like to revoke {revokeUser}'s access?
+ Please be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information. +
+ + {addMemberOpen && + setAddMemberOpen(false)} + /> + } + + ); +} diff --git a/src/Collections/Main.tsx b/src/Collections/Main.tsx index 11f4743..f9a1011 100644 --- a/src/Collections/Main.tsx +++ b/src/Collections/Main.tsx @@ -16,6 +16,7 @@ import CollectionList from "./CollectionList"; import CollectionImport from "./CollectionImport"; import PageNotFound from "../PageNotFound"; import CollectionEdit from "./CollectionEdit"; +import CollectionMembers from "./CollectionMembers"; import Collection from "./Collection"; const decryptCollections = getDecryptCollectionsFunction(); @@ -118,7 +119,7 @@ export default function CollectionsMain() { path={routeResolver.getRoute("collections._id.members")} exact > - Members +
- +
diff --git a/src/Settings/index.tsx b/src/Settings/index.tsx index 232d5ea..611ab04 100644 --- a/src/Settings/index.tsx +++ b/src/Settings/index.tsx @@ -18,15 +18,12 @@ import { setSettings } from "../store/actions"; import Container from "../widgets/Container"; import AppBarOverride from "../widgets/AppBarOverride"; import PrettyFingerprint from "../widgets/PrettyFingerprint"; +import { useCredentials } from "../credentials"; function SecurityFingerprint() { - const userInfo = useSelector((state: StoreState) => state.cache.userInfo); - - if (!userInfo) { - return

Security fingerprint error.

; - } - - const publicKey = userInfo.publicKey; + const etebase = useCredentials()!; + const inviteMgr = etebase.getInvitationManager(); + const publicKey = inviteMgr.pubkey; return ( <> diff --git a/src/widgets/PrettyFingerprint.tsx b/src/widgets/PrettyFingerprint.tsx index 27f9bf4..88ab650 100644 --- a/src/widgets/PrettyFingerprint.tsx +++ b/src/widgets/PrettyFingerprint.tsx @@ -2,50 +2,17 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as React from "react"; -import sjcl from "sjcl"; -import { byte, base64 } from "etesync"; - -function byteArray4ToNumber(bytes: byte[], offset: number) { - // tslint:disable:no-bitwise - return ( - ((bytes[offset + 0] & 0xff) * (1 << 24)) + - ((bytes[offset + 1] & 0xff) * (1 << 16)) + - ((bytes[offset + 2] & 0xff) * (1 << 8)) + - ((bytes[offset + 3] & 0xff)) - ); -} - -function getEncodedChunk(publicKey: byte[], offset: number) { - const chunk = byteArray4ToNumber(publicKey, offset) % 100000; - return chunk.toString().padStart(5, "0"); -} +import * as Etebase from "etebase"; interface PropsType { - publicKey: base64; + publicKey: Uint8Array; } -class PrettyFingerprint extends React.PureComponent { - public render() { - const fingerprint = sjcl.codec.bytes.fromBits( - sjcl.hash.sha256.hash(sjcl.codec.base64.toBits(this.props.publicKey)) - ); - - const spacing = " "; - const prettyPublicKey = - getEncodedChunk(fingerprint, 0) + spacing + - getEncodedChunk(fingerprint, 4) + spacing + - getEncodedChunk(fingerprint, 8) + spacing + - getEncodedChunk(fingerprint, 12) + "\n" + - getEncodedChunk(fingerprint, 16) + spacing + - getEncodedChunk(fingerprint, 20) + spacing + - getEncodedChunk(fingerprint, 24) + spacing + - getEncodedChunk(fingerprint, 28); +export default function PrettyFingerprint(props: PropsType) { + const prettyFingerprint = Etebase.getPrettyFingerprint(props.publicKey); - return ( -
{prettyPublicKey}
- ); - } + return ( +
{prettyFingerprint}
+ ); } - -export default PrettyFingerprint;