wip
This commit is contained in:
@@ -845,6 +845,32 @@
|
||||
"offline_access",
|
||||
"microprofile-jwt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientId": "encryption",
|
||||
"name": "Encryption",
|
||||
"enabled": true,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"standardFlowEnabled": true,
|
||||
"implicitFlowEnabled": false,
|
||||
"directAccessGrantsEnabled": false,
|
||||
"publicClient": true,
|
||||
"protocol": "openid-connect",
|
||||
"redirectUris": [
|
||||
"http://localhost:7202/auth/callback"
|
||||
],
|
||||
"webOrigins": [
|
||||
"http://localhost:7202"
|
||||
],
|
||||
"frontchannelLogout": true,
|
||||
"attributes": {},
|
||||
"defaultClientScopes": [
|
||||
"web-origins",
|
||||
"profile",
|
||||
"roles",
|
||||
"email"
|
||||
],
|
||||
"optionalClientScopes": []
|
||||
}
|
||||
],
|
||||
"clientScopes": [
|
||||
|
||||
@@ -29,11 +29,12 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
full_name = serializers.SerializerMethodField(read_only=True)
|
||||
short_name = serializers.SerializerMethodField(read_only=True)
|
||||
suite_user_id = serializers.CharField(source='sub', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["id", "email", "full_name", "short_name", "language"]
|
||||
read_only_fields = ["id", "email", "full_name", "short_name"]
|
||||
fields = ["id", "email", "full_name", "short_name", "language", "suite_user_id"]
|
||||
read_only_fields = ["id", "email", "full_name", "short_name", "suite_user_id"]
|
||||
|
||||
def get_full_name(self, instance):
|
||||
"""Return the full name of the user."""
|
||||
|
||||
@@ -252,35 +252,6 @@ class UserViewSet(
|
||||
self.serializer_class(request.user, context=context).data
|
||||
)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=False,
|
||||
methods=["get"],
|
||||
url_name="get-access-token",
|
||||
url_path="get-access-token",
|
||||
permission_classes=[permissions.IsAuthenticated],
|
||||
)
|
||||
def get_access_token(self, request):
|
||||
"""
|
||||
WARNING: Temporary endpoint — to be removed.
|
||||
|
||||
TODO: Find a better way to propagate the OIDC access token to the frontend
|
||||
so it can be passed to the VaultClient encryption library.
|
||||
We should also consider having ProConnect flow directly on interface.encryption,
|
||||
so it can obtain its own valid token when opened in a new tab (not as an iframe).
|
||||
|
||||
Returns the OIDC access token stored in the Django session.
|
||||
Requires OIDC_STORE_ACCESS_TOKEN=True in settings.
|
||||
"""
|
||||
access_token = request.session.get("oidc_access_token")
|
||||
|
||||
if not access_token:
|
||||
return drf.response.Response(
|
||||
{"detail": "No access token available in session."},
|
||||
status=404,
|
||||
)
|
||||
|
||||
return drf.response.Response({"access_token": access_token})
|
||||
|
||||
|
||||
class ResourceAccessViewsetMixin:
|
||||
"""Mixin with methods common to all access viewsets."""
|
||||
@@ -2127,7 +2098,8 @@ class DocumentViewSet(
|
||||
})
|
||||
|
||||
# Validate that we have encrypted symmetric keys for all users with access
|
||||
users_with_access = {str(access.user_id) for access in document_accesses}
|
||||
# Keys in encryptedSymmetricKeyPerUser are keyed by the user's OIDC `sub`
|
||||
users_with_access = {str(access.user.sub) for access in document_accesses}
|
||||
|
||||
# Check that encryptedSymmetricKeyPerUser contains all required users
|
||||
provided_user_ids = set(encryptedSymmetricKeyPerUser.keys())
|
||||
@@ -2178,10 +2150,11 @@ class DocumentViewSet(
|
||||
transaction.on_commit(_cleanup_old_attachments)
|
||||
|
||||
# Store the encrypted symmetric keys in DocumentAccess for each user
|
||||
for user_id, encrypted_key in encryptedSymmetricKeyPerUser.items():
|
||||
# Keys are keyed by the user's OIDC `sub`, so look up by user__sub
|
||||
for sub, encrypted_key in encryptedSymmetricKeyPerUser.items():
|
||||
try:
|
||||
# Find the DocumentAccess record for this user and document
|
||||
access = models.DocumentAccess.objects.get(document=document, user_id=user_id)
|
||||
access = models.DocumentAccess.objects.get(document=document, user__sub=sub)
|
||||
access.encrypted_document_symmetric_key_for_user = encrypted_key
|
||||
access.save()
|
||||
except models.DocumentAccess.DoesNotExist:
|
||||
|
||||
@@ -746,7 +746,7 @@ class Document(MP_Node, BaseModel):
|
||||
return list(
|
||||
DocumentAccess.objects
|
||||
.filter(document=self, user__isnull=False)
|
||||
.values_list('user_id', flat=True)
|
||||
.values_list('user__sub', flat=True)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@@ -760,12 +760,12 @@ class Document(MP_Node, BaseModel):
|
||||
accesses = (
|
||||
DocumentAccess.objects
|
||||
.filter(document=self, user__isnull=False, encryption_public_key_fingerprint__isnull=False)
|
||||
.values_list('user_id', 'encryption_public_key_fingerprint')
|
||||
.values_list('user__sub', 'encryption_public_key_fingerprint')
|
||||
)
|
||||
|
||||
return {
|
||||
str(user_id): fingerprint
|
||||
for user_id, fingerprint in accesses
|
||||
str(sub): fingerprint
|
||||
for sub, fingerprint in accesses
|
||||
if fingerprint
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
suite_user_id: string | null;
|
||||
email: string;
|
||||
full_name: string;
|
||||
short_name: string;
|
||||
|
||||
+2
-2
@@ -63,10 +63,10 @@ export const UserEncryptionProvider = ({
|
||||
null;
|
||||
let encryptionError: EncryptionError = null;
|
||||
|
||||
if (isReady && user?.id) {
|
||||
if (isReady && user?.suite_user_id) {
|
||||
if (hasKeys) {
|
||||
encryptionSettings = {
|
||||
userId: user.id,
|
||||
userId: user.suite_user_id,
|
||||
userPrivateKey: null, // Keys are in the vault — use VaultClient for crypto
|
||||
userPublicKey: null,
|
||||
};
|
||||
|
||||
+15
-27
@@ -22,6 +22,7 @@ import {
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useAuth } from '@/features/auth';
|
||||
|
||||
// Environment configuration
|
||||
@@ -98,6 +99,7 @@ export function VaultClientProvider({
|
||||
}) {
|
||||
const { user, authenticated } = useAuth();
|
||||
const { i18n } = useTranslation();
|
||||
const { theme: cunninghamTheme } = useCunninghamTheme();
|
||||
const clientRef = useRef<VaultClient | null>(null);
|
||||
const [clientInitialized, setClientInitialized] = useState(false);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
@@ -123,7 +125,7 @@ export function VaultClientProvider({
|
||||
const client = new window.EncryptionClient.VaultClient({
|
||||
vaultUrl: VAULT_URL,
|
||||
interfaceUrl: INTERFACE_URL,
|
||||
theme: 'light',
|
||||
theme: cunninghamTheme,
|
||||
lang: i18n.language,
|
||||
});
|
||||
|
||||
@@ -189,38 +191,24 @@ export function VaultClientProvider({
|
||||
useEffect(() => {
|
||||
const client = clientRef.current;
|
||||
|
||||
if (!client || !clientInitialized || !authenticated || !user?.id) {
|
||||
if (
|
||||
!client ||
|
||||
!clientInitialized ||
|
||||
!authenticated ||
|
||||
!user?.id ||
|
||||
!user?.suite_user_id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function setupAuth() {
|
||||
// Fetch the OIDC access token from the Django session.
|
||||
// WARNING: This uses a temporary endpoint (get-access-token) — see the
|
||||
// backend TODO for finding a better way to propagate the access token.
|
||||
let token = user!.id; // fallback: userId (vault dev mode without JWKS)
|
||||
if (cancelled || !client) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_ORIGIN || ''}/api/v1.0/users/get-access-token/`,
|
||||
{ credentials: 'include' },
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.access_token) {
|
||||
token = data.access_token;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Endpoint not available — fall back to userId for dev mode
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
client.setAuthContext({ token, userId: user!.id });
|
||||
client.setAuthContext({
|
||||
suiteUserId: user!.suite_user_id!,
|
||||
});
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -246,7 +234,7 @@ export function VaultClientProvider({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [clientInitialized, authenticated, user?.id]);
|
||||
}, [clientInitialized, authenticated, user?.id, user?.suite_user_id]);
|
||||
|
||||
const refreshKeyState = useCallback(async () => {
|
||||
const client = clientRef.current;
|
||||
|
||||
+3
-3
@@ -5,8 +5,8 @@ declare global {
|
||||
interface VaultClient {
|
||||
init(): Promise<void>;
|
||||
destroy(): void;
|
||||
setTheme(theme: 'light' | 'dark' | 'system'): void;
|
||||
setAuthContext(context: { token: string; userId: string }): void;
|
||||
setTheme(theme: string): void;
|
||||
setAuthContext(context: { suiteUserId: string }): void;
|
||||
hasKeys(): Promise<{ hasKeys: boolean }>;
|
||||
getPublicKey(): Promise<{ publicKey: ArrayBuffer }>;
|
||||
encrypt(data: ArrayBuffer): Promise<{ encryptedData: ArrayBuffer }>;
|
||||
@@ -36,7 +36,7 @@ declare global {
|
||||
vaultUrl: string;
|
||||
interfaceUrl: string;
|
||||
timeout?: number;
|
||||
theme?: 'light' | 'dark' | 'system';
|
||||
theme?: string;
|
||||
lang?: string;
|
||||
}) => VaultClient;
|
||||
};
|
||||
|
||||
+4
-4
@@ -161,8 +161,8 @@ export const ModalEncryptDoc = ({ doc, onClose }: ModalEncryptDocProps) => {
|
||||
if (!accesses || !vaultClient) return;
|
||||
|
||||
const userIds = accesses
|
||||
.filter((a) => a.user)
|
||||
.map((a) => a.user.id);
|
||||
.filter((a) => a.user?.suite_user_id)
|
||||
.map((a) => a.user.suite_user_id!);
|
||||
|
||||
if (userIds.length === 0) return;
|
||||
|
||||
@@ -177,7 +177,7 @@ export const ModalEncryptDoc = ({ doc, onClose }: ModalEncryptDocProps) => {
|
||||
}
|
||||
|
||||
return accesses.filter(
|
||||
(access) => access.user && !publicKeysMap[access.user.id],
|
||||
(access) => access.user?.suite_user_id && !publicKeysMap[access.user.suite_user_id],
|
||||
);
|
||||
}, [accesses, publicKeysMap]);
|
||||
|
||||
@@ -231,7 +231,7 @@ export const ModalEncryptDoc = ({ doc, onClose }: ModalEncryptDocProps) => {
|
||||
}
|
||||
|
||||
// Get the current user's encrypted key for attachment encryption
|
||||
const currentUserEncryptedKey = encryptedKeys[user.id];
|
||||
const currentUserEncryptedKey = user.suite_user_id ? encryptedKeys[user.suite_user_id] : undefined;
|
||||
|
||||
// Encrypt existing attachments using the same symmetric key via vault
|
||||
let attachmentKeyMapping: Record<string, string> = {};
|
||||
|
||||
+3
-3
@@ -95,8 +95,8 @@ export const DocShareAddMemberList = ({
|
||||
|
||||
if (doc.is_encrypted && documentEncryptionSettings && vaultClient) {
|
||||
const memberUserIds = selectedUsers
|
||||
.filter((user) => user.id !== user.email)
|
||||
.map((user) => user.id);
|
||||
.filter((user) => user.id !== user.email && user.suite_user_id)
|
||||
.map((user) => user.suite_user_id!);
|
||||
|
||||
if (memberUserIds.length > 0) {
|
||||
const { publicKeys } = await vaultClient.fetchPublicKeys(memberUserIds);
|
||||
@@ -141,7 +141,7 @@ export const DocShareAddMemberList = ({
|
||||
let memberEncryptedSymmetricKey: string | null = null;
|
||||
|
||||
if (doc.is_encrypted && documentEncryptionSettings && vaultClient) {
|
||||
const userPublicKey = publicKeysMap[user.id];
|
||||
const userPublicKey = user.suite_user_id ? publicKeysMap[user.suite_user_id] : undefined;
|
||||
|
||||
if (userPublicKey) {
|
||||
const { rewrappedKey } = await vaultClient.rewrapKey(
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ export const DocShareInvitationItem = ({
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const invitedUser: User = {
|
||||
id: invitation.email,
|
||||
suite_user_id: null,
|
||||
full_name: invitation.email,
|
||||
email: invitation.email,
|
||||
short_name: invitation.email,
|
||||
|
||||
@@ -143,10 +143,11 @@ export const QuickSearchGroupMember = ({
|
||||
<QuickSearchGroup
|
||||
group={membersData}
|
||||
renderElement={(access) => {
|
||||
const hasMismatch = keyMismatchUserIds?.has(access.user.id);
|
||||
const uid = access.user.suite_user_id;
|
||||
const hasMismatch = uid ? keyMismatchUserIds?.has(uid) : false;
|
||||
const hasNoEncryptionKey =
|
||||
doc.is_encrypted &&
|
||||
!doc.accesses_fingerprints_per_user?.[access.user.id];
|
||||
(!uid || !doc.accesses_fingerprints_per_user?.[uid]);
|
||||
|
||||
let suffix: string | undefined;
|
||||
if (hasMismatch) {
|
||||
@@ -163,12 +164,12 @@ export const QuickSearchGroupMember = ({
|
||||
access={access}
|
||||
suffix={suffix}
|
||||
onSuffixClick={
|
||||
hasMismatch
|
||||
? () => setMismatchUserId(access.user.id)
|
||||
hasMismatch && uid
|
||||
? () => setMismatchUserId(uid)
|
||||
: undefined
|
||||
}
|
||||
fingerprintKey={
|
||||
doc.accesses_fingerprints_per_user?.[access.user.id]
|
||||
uid ? doc.accesses_fingerprints_per_user?.[uid] : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -105,7 +105,7 @@ export const DocShareModal = ({
|
||||
|
||||
const { mismatches: keyMismatches, acceptNewKey } = usePublicKeyRegistry(
|
||||
undefined,
|
||||
user?.id,
|
||||
user?.suite_user_id ?? undefined,
|
||||
);
|
||||
const keyMismatchUserIds = useMemo(
|
||||
() => new Set(keyMismatches.map((m) => m.userId)),
|
||||
@@ -470,11 +470,11 @@ const QuickSearchInviteInputSection = ({
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(user: User) => {
|
||||
if (isEncrypted && !doc.accesses_fingerprints_per_user?.[user.id]) {
|
||||
if (isEncrypted && (!user.suite_user_id || !doc.accesses_fingerprints_per_user?.[user.suite_user_id])) {
|
||||
setShowNoKeyModal(true);
|
||||
return;
|
||||
}
|
||||
if (keyMismatchUserIds?.has(user.id)) {
|
||||
if (user.suite_user_id && keyMismatchUserIds?.has(user.suite_user_id)) {
|
||||
setMismatchUser(user);
|
||||
return;
|
||||
}
|
||||
@@ -488,6 +488,7 @@ const QuickSearchInviteInputSection = ({
|
||||
const isEmail = isValidEmail(userQuery);
|
||||
const newUser: User = {
|
||||
id: userQuery,
|
||||
suite_user_id: null,
|
||||
full_name: '',
|
||||
email: userQuery,
|
||||
short_name: '',
|
||||
@@ -516,10 +517,10 @@ const QuickSearchInviteInputSection = ({
|
||||
|
||||
const getUserSuffix = useCallback(
|
||||
(user: User): string | undefined => {
|
||||
if (keyMismatchUserIds?.has(user.id)) {
|
||||
if (user.suite_user_id && keyMismatchUserIds?.has(user.suite_user_id)) {
|
||||
return t('DIFFERENT PUBLIC KEY, PLEASE VERIFY');
|
||||
}
|
||||
if (isEncrypted && !doc.accesses_fingerprints_per_user?.[user.id]) {
|
||||
if (isEncrypted && (!user.suite_user_id || !doc.accesses_fingerprints_per_user?.[user.suite_user_id])) {
|
||||
return t(`(encryption not enabled)`);
|
||||
}
|
||||
return undefined;
|
||||
@@ -539,7 +540,7 @@ const QuickSearchInviteInputSection = ({
|
||||
<DocShareModalInviteUserRow
|
||||
user={user}
|
||||
suffix={getUserSuffix(user)}
|
||||
fingerprintKey={doc.accesses_fingerprints_per_user?.[user.id]}
|
||||
fingerprintKey={user.suite_user_id ? doc.accesses_fingerprints_per_user?.[user.suite_user_id] : undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -596,7 +597,7 @@ const QuickSearchInviteInputSection = ({
|
||||
{mismatchUser &&
|
||||
(() => {
|
||||
const mismatch = keyMismatches?.find(
|
||||
(m) => m.userId === mismatchUser.id,
|
||||
(m) => m.userId === mismatchUser.suite_user_id,
|
||||
);
|
||||
return (
|
||||
<ModalKeyMismatch
|
||||
@@ -604,7 +605,7 @@ const QuickSearchInviteInputSection = ({
|
||||
onAcceptKey={
|
||||
acceptNewKey
|
||||
? () => {
|
||||
void acceptNewKey(mismatchUser.id).then(() => {
|
||||
void acceptNewKey(mismatchUser.suite_user_id!).then(() => {
|
||||
onSelect(mismatchUser);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user