feat(proxy): move secure share into create-key flow as one-time link

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Mubashir Osmani 2026-07-10 20:11:25 +00:00
parent e89a991702
commit c643f6d1af
13 changed files with 182 additions and 211 deletions

View file

@ -31,6 +31,7 @@ BACKEND_PATH_PREFIXES: tuple[str, ...] = (
"/oauth/",
"/invitation/",
"/jwt/",
"/secure_share/",
# Models & routing config
"/model/",
"/v1/model/info",

View file

@ -11,8 +11,11 @@ alongside an expiry. The plaintext secret and the password never leave the
client, so a database or server compromise yields ciphertext without the
password required to open it.
Links are one-time: a successful GET returns the ciphertext once and deletes
the row, so the encrypted payload is exposed to the network at most once.
POST /secure_share/create - store an encrypted share (proxy admin only)
GET /secure_share/{share_id} - fetch an unexpired share (admins + internal users)
GET /secure_share/{share_id} - fetch and consume a share once (admins + internal users)
DELETE /secure_share/{share_id} - revoke a share early (proxy admin only)
"""
@ -178,8 +181,8 @@ async def get_secure_share(
raise HTTPException(status_code=404, detail={"error": "Secure share not found."})
share = SecureShareGetResponse.model_validate(row, from_attributes=True)
await repository.table.delete(where={"share_id": share_id})
if _is_expired(share.expires_at):
await repository.table.delete(where={"share_id": share_id})
raise HTTPException(status_code=status.HTTP_410_GONE, detail={"error": "Secure share has expired."})
return share

View file

@ -169,7 +169,33 @@ async def test_get_returns_unexpired_share_for_allowed_roles(role: LitellmUserRo
assert result.share_id == "share-1"
assert result.ciphertext == _b64(b"c")
table.delete.assert_not_called()
table.delete.assert_awaited_once_with(where={"share_id": "share-1"})
@pytest.mark.asyncio
async def test_get_is_one_time_second_read_returns_404():
expires_at = datetime.now(timezone.utc) + timedelta(hours=1)
table = _fake_table()
table.find_unique.side_effect = [
SimpleNamespace(
share_id="share-1",
ciphertext=_b64(b"c"),
salt=_b64(b"s"),
iv=_b64(b"i"),
expires_at=expires_at,
created_by="admin-user",
),
None,
]
with _patched_prisma(table):
first = await get_secure_share(share_id="share-1", user_api_key_dict=_admin())
with pytest.raises(HTTPException) as exc:
await get_secure_share(share_id="share-1", user_api_key_dict=_admin())
assert first.share_id == "share-1"
table.delete.assert_awaited_once_with(where={"share_id": "share-1"})
assert exc.value.status_code == 404
@pytest.mark.asyncio

View file

@ -1,163 +0,0 @@
import { CopyOutlined, LockOutlined } from "@ant-design/icons";
import { Button, Card, Input, Select, Typography } from "antd";
import React, { useState } from "react";
import { createSecureShareCall } from "@/components/networking";
import NotificationManager from "@/components/molecules/notifications_manager";
import { encryptSecret } from "./crypto";
const { Title, Text, Paragraph } = Typography;
const EXPIRY_OPTIONS = [
{ value: "1h", label: "1 hour" },
{ value: "6h", label: "6 hours" },
{ value: "1d", label: "1 day" },
{ value: "7d", label: "7 days" },
];
const MIN_PASSWORD_LENGTH = 8;
interface SecureShareCreateProps {
accessToken: string | null;
}
interface CreatedShare {
link: string;
expiresAt: string;
}
function buildShareLink(shareId: string): string {
const base = window.location.href.split(/[?#]/)[0].replace(/\/$/, "");
return `${base}/view?id=${encodeURIComponent(shareId)}`;
}
const SecureShareCreate: React.FC<SecureShareCreateProps> = ({ accessToken }) => {
const [secret, setSecret] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [expiry, setExpiry] = useState("1d");
const [isSubmitting, setIsSubmitting] = useState(false);
const [created, setCreated] = useState<CreatedShare | null>(null);
const reset = () => {
setSecret("");
setPassword("");
setConfirmPassword("");
setExpiry("1d");
setCreated(null);
};
const handleCreate = async () => {
if (!accessToken) {
NotificationManager.error("You must be logged in to create a secure share.");
return;
}
if (secret.trim().length === 0) {
NotificationManager.error("Enter the credential you want to share.");
return;
}
if (password.length < MIN_PASSWORD_LENGTH) {
NotificationManager.error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters.`);
return;
}
if (password !== confirmPassword) {
NotificationManager.error("Passwords do not match.");
return;
}
setIsSubmitting(true);
try {
const encrypted = await encryptSecret(secret, password);
const payload = { ...encrypted, expiry };
const response = await createSecureShareCall(accessToken, payload);
setCreated({ link: buildShareLink(response.share_id), expiresAt: response.expires_at });
NotificationManager.success("Secure share created. The secret was encrypted in your browser.");
} catch (error) {
NotificationManager.fromBackend(error);
} finally {
setIsSubmitting(false);
}
};
const copyLink = async () => {
if (!created) return;
await navigator.clipboard.writeText(created.link);
NotificationManager.success("Link copied to clipboard.");
};
if (created) {
return (
<Card>
<Title level={4}>
<LockOutlined /> Secure share created
</Title>
<Paragraph>
Send this link to the recipient. Share the password separately (not in the same channel). The recipient must
be logged in as a proxy admin or internal user to open it.
</Paragraph>
<div className="flex items-center gap-2">
<Input readOnly value={created.link} />
<Button icon={<CopyOutlined />} onClick={copyLink}>
Copy
</Button>
</div>
<Paragraph className="mt-4">
<Text type="secondary">Expires at {new Date(created.expiresAt).toLocaleString()}</Text>
</Paragraph>
<Button type="primary" onClick={reset}>
Share another
</Button>
</Card>
);
}
return (
<Card>
<Title level={4}>
<LockOutlined /> Secure Share
</Title>
<Paragraph>
Share a credential over a temporary, end-to-end encrypted link. The secret is encrypted in your browser with a
key derived from the password you choose; the server only ever stores ciphertext.
</Paragraph>
<div className="mb-4">
<Text>Credential</Text>
<Input.TextArea
rows={4}
value={secret}
onChange={(e) => setSecret(e.target.value)}
placeholder="Paste the API key or secret to share"
/>
</div>
<div className="mb-4">
<Text>Password</Text>
<Input.Password
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={`At least ${MIN_PASSWORD_LENGTH} characters`}
/>
</div>
<div className="mb-4">
<Text>Confirm password</Text>
<Input.Password
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Re-enter the password"
/>
</div>
<div className="mb-4">
<Text>Expires after</Text>
<Select value={expiry} onChange={setExpiry} options={EXPIRY_OPTIONS} className="w-full" />
</div>
<Button type="primary" loading={isSubmitting} onClick={handleCreate}>
Create secure link
</Button>
</Card>
);
};
export default SecureShareCreate;

View file

@ -1,34 +0,0 @@
"use client";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import LoadingScreen from "@/components/common_components/LoadingScreen";
import { isAdminRole } from "@/utils/roles";
import { Card, Typography } from "antd";
import { Suspense } from "react";
import SecureShareCreate from "./_components/SecureShareCreate";
const { Title, Paragraph } = Typography;
function SecureSharePageContent() {
const { isLoading, isAuthorized, accessToken, userRole } = useAuthorized();
if (isLoading || !isAuthorized) {
return <LoadingScreen />;
}
if (!userRole || !isAdminRole(userRole)) {
return (
<Card>
<Title level={4}>Secure Share</Title>
<Paragraph>Only proxy admins can create secure shares.</Paragraph>
</Card>
);
}
return <SecureShareCreate accessToken={accessToken} />;
}
export default function SecureSharePage() {
return (
<Suspense fallback={<LoadingScreen />}>
<SecureSharePageContent />
</Suspense>
);
}

View file

@ -3,7 +3,7 @@
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import LoadingScreen from "@/components/common_components/LoadingScreen";
import { Suspense } from "react";
import SecureShareView from "../_components/SecureShareView";
import SecureShareView from "@/components/secure_share/SecureShareView";
function SecureShareViewPageContent() {
const { isLoading, isAuthorized, accessToken } = useAuthorized();

View file

@ -20,7 +20,6 @@ import {
FolderOutlined,
KeyOutlined,
LineChartOutlined,
LockOutlined,
PlayCircleOutlined,
RobotOutlined,
SafetyOutlined,
@ -273,13 +272,6 @@ const menuGroups: MenuGroup[] = [
icon: <CreditCardOutlined />,
roles: all_admin_roles,
},
{
key: "secure-share",
page: "secure-share",
label: "Secure Share",
icon: <LockOutlined />,
roles: all_admin_roles,
},
],
},
{

View file

@ -54,6 +54,7 @@ import {
userFilterUICall,
} from "../networking";
import CreatedKeyDisplay from "../shared/CreatedKeyDisplay";
import SecureShareLinkButton from "../secure_share/SecureShareLinkButton";
import NumericalInput from "../shared/numerical_input";
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
import { simplifyKeyGenerateError } from "./utils";
@ -1738,7 +1739,10 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
<Title>Save your Key</Title>
<Col numColSpan={1}>
{apiKey != null ? (
<CreatedKeyDisplay apiKey={apiKey} />
<>
<CreatedKeyDisplay apiKey={apiKey} />
{keyOwner === "another_user" && <SecureShareLinkButton secret={apiKey} accessToken={accessToken} />}
</>
) : (
<Text>Key being created, this might take 30s</Text>
)}

View file

@ -0,0 +1,143 @@
import { CopyOutlined, LockOutlined } from "@ant-design/icons";
import { Button, Form, Input, Modal, Select, Typography } from "antd";
import React, { useState } from "react";
import { createSecureShareCall, SecureShareCreatePayload } from "@/components/networking";
import NotificationManager from "@/components/molecules/notifications_manager";
import { migratedHref } from "@/utils/migratedPages";
import { encryptSecret } from "./crypto";
const { Paragraph, Text } = Typography;
const MIN_PASSWORD_LENGTH = 8;
const EXPIRY_OPTIONS = [
{ value: "1h", label: "1 hour" },
{ value: "6h", label: "6 hours" },
{ value: "1d", label: "1 day" },
{ value: "7d", label: "7 days" },
];
interface SecureShareLinkButtonProps {
secret: string;
accessToken: string | null;
}
interface ShareFormValues {
password: string;
confirmPassword: string;
expiry: string;
}
const buildShareLink = (shareId: string): string =>
`${window.location.origin}${migratedHref("secure-share/view")}?id=${encodeURIComponent(shareId)}`;
const SecureShareLinkButton: React.FC<SecureShareLinkButtonProps> = ({ secret, accessToken }) => {
const [form] = Form.useForm<ShareFormValues>();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [link, setLink] = useState<string | null>(null);
const closeModal = () => {
setIsModalOpen(false);
setLink(null);
form.resetFields();
};
const handleGenerate = async (values: ShareFormValues) => {
if (!accessToken) {
NotificationManager.error("You must be logged in to create a share link.");
return;
}
setIsGenerating(true);
try {
const encrypted = await encryptSecret(secret, values.password);
const payload: SecureShareCreatePayload = { ...encrypted, expiry: values.expiry };
const response = await createSecureShareCall(accessToken, payload);
setLink(buildShareLink(response.share_id));
} catch (error) {
NotificationManager.fromBackend(error);
} finally {
setIsGenerating(false);
}
};
const copyLink = async () => {
if (link === null) return;
await navigator.clipboard.writeText(link);
NotificationManager.success("Share link copied to clipboard.");
};
return (
<>
<Button icon={<LockOutlined />} onClick={() => setIsModalOpen(true)} style={{ marginTop: 12, marginLeft: 8 }}>
Generate one-time share link
</Button>
<Modal
title="Share this key over a one-time encrypted link"
open={isModalOpen}
onCancel={closeModal}
footer={null}
destroyOnClose
>
{link === null ? (
<>
<Paragraph>
<Text type="secondary">
The key is encrypted in your browser with the password below and never reaches the server in plaintext.
Share the link and password separately; the link opens once, then it is gone.
</Text>
</Paragraph>
<Form form={form} layout="vertical" onFinish={handleGenerate} initialValues={{ expiry: "1d" }}>
<Form.Item
label="Password"
name="password"
rules={[{ required: true, min: MIN_PASSWORD_LENGTH, message: "Use at least 8 characters." }]}
>
<Input.Password placeholder="Password the recipient will enter" />
</Form.Item>
<Form.Item
label="Confirm password"
name="confirmPassword"
dependencies={["password"]}
rules={[
{ required: true, message: "Confirm the password." },
({ getFieldValue }) => ({
validator: (_, value) =>
!value || getFieldValue("password") === value
? Promise.resolve()
: Promise.reject(new Error("Passwords do not match.")),
}),
]}
>
<Input.Password placeholder="Re-enter the password" />
</Form.Item>
<Form.Item label="Link expires after" name="expiry" rules={[{ required: true }]}>
<Select options={EXPIRY_OPTIONS} />
</Form.Item>
<Button type="primary" htmlType="submit" loading={isGenerating}>
Generate link
</Button>
</Form>
</>
) : (
<>
<Paragraph>
<Text type="secondary">
Send this link to the recipient and give them the password out-of-band. It reveals the key once, then
expires.
</Text>
</Paragraph>
<div className="flex items-center gap-2">
<Input readOnly value={link} />
<Button icon={<CopyOutlined />} onClick={copyLink}>
Copy
</Button>
</div>
</>
)}
</Modal>
</>
);
};
export default SecureShareLinkButton;

View file

@ -49,7 +49,6 @@ export const MIGRATED_PAGES: Record<string, string> = {
users: "users",
teams: "teams",
organizations: "organizations",
"secure-share": "secure-share",
};
function uiBase(): string {