fix(ui): API-Schlüssel nur mit Rechten anbieten, die der Nutzer selbst hat
Der Server lehnt Rechte ab, die der Anlegende nicht selbst hat. Das UI bietet jetzt auch nur diese an (z. B. kein shop-sync:write ohne eigenes Recht) und erklärt, wenn gar keines übrig bleibt. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
fe7b61c3c1
commit
6505411761
2 files changed
+36
-2
No files matched your search
@@ -7,6 +7,12 @@ import { ApiKeysTab } from "./api-keys-tab";
|
|||||||
import { createApiKey, fetchApiKeys, revokeApiKey } from "@/lib/api-keys/api";
|
import { createApiKey, fetchApiKeys, revokeApiKey } from "@/lib/api-keys/api";
|
||||||
import type { ApiKey } from "@/lib/api-keys/types";
|
import type { ApiKey } from "@/lib/api-keys/types";
|
||||||
|
|
||||||
|
// Rechte des angemeldeten Nutzers; je Test anpassbar.
|
||||||
|
let ownPermissions: string[] = [];
|
||||||
|
vi.mock("@/lib/auth/auth-context", () => ({
|
||||||
|
useAuth: () => ({ hasPermission: (permission: string) => ownPermissions.includes(permission) }),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/api-keys/api", () => ({
|
vi.mock("@/lib/api-keys/api", () => ({
|
||||||
createApiKey: vi.fn(),
|
createApiKey: vi.fn(),
|
||||||
fetchApiKeys: vi.fn(),
|
fetchApiKeys: vi.fn(),
|
||||||
@@ -38,6 +44,7 @@ function key(overrides: Partial<ApiKey>): ApiKey {
|
|||||||
|
|
||||||
describe("ApiKeysTab", () => {
|
describe("ApiKeysTab", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
ownPermissions = ["api-keys:manage", "shop-sync:read", "shop-sync:write", "erp-inventory:read"];
|
||||||
vi.mocked(fetchApiKeys).mockReset().mockResolvedValue([]);
|
vi.mocked(fetchApiKeys).mockReset().mockResolvedValue([]);
|
||||||
vi.mocked(createApiKey).mockReset();
|
vi.mocked(createApiKey).mockReset();
|
||||||
vi.mocked(revokeApiKey).mockReset();
|
vi.mocked(revokeApiKey).mockReset();
|
||||||
@@ -94,4 +101,20 @@ describe("ApiKeysTab", () => {
|
|||||||
expect(await screen.findByText("ERPNext")).toBeInTheDocument();
|
expect(await screen.findByText("ERPNext")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Darf: Inventar-Spiegel lesen (ERPNext)")).toBeInTheDocument();
|
expect(screen.getByText("Darf: Inventar-Spiegel lesen (ERPNext)")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("bietet nur Rechte an, die der angemeldete Nutzer selbst hat", () => {
|
||||||
|
ownPermissions = ["api-keys:manage", "shop-sync:read", "erp-inventory:read"];
|
||||||
|
renderWithClient(<ApiKeysTab />);
|
||||||
|
expect(screen.getByRole("checkbox", { name: /Shop-Daten lesen/ })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("checkbox", { name: /Inventar-Spiegel lesen/ })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("checkbox", { name: /Verkäufe und Rückbuchungen/ })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wählt nichts vor, was der Nutzer nicht vergeben darf, und erklärt es", () => {
|
||||||
|
ownPermissions = ["api-keys:manage"];
|
||||||
|
renderWithClient(<ApiKeysTab />);
|
||||||
|
expect(screen.queryAllByRole("checkbox")).toHaveLength(0);
|
||||||
|
expect(screen.getByText(/keine Rechte, die ein API-Schlüssel tragen kann/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "Schlüssel anlegen" })).toBeDisabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -18,6 +18,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { ConfirmDialog } from "@/components/confirm-dialog";
|
import { ConfirmDialog } from "@/components/confirm-dialog";
|
||||||
import { ToggleChip } from "@/components/form/toggle-chip";
|
import { ToggleChip } from "@/components/form/toggle-chip";
|
||||||
|
import { useAuth } from "@/lib/auth/auth-context";
|
||||||
import { ApiKeyAllowlist, parseAllowlist } from "./api-key-allowlist";
|
import { ApiKeyAllowlist, parseAllowlist } from "./api-key-allowlist";
|
||||||
|
|
||||||
// Vorschlag wie bisher: der Wabenhain-Shop liest. Alles andere wird bewusst angekreuzt.
|
// Vorschlag wie bisher: der Wabenhain-Shop liest. Alles andere wird bewusst angekreuzt.
|
||||||
@@ -56,10 +57,15 @@ function keyState(apiKey: ApiKey): { label: string; active: boolean } {
|
|||||||
*/
|
*/
|
||||||
export function ApiKeysTab() {
|
export function ApiKeysTab() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
// Der Server lässt nur Rechte zu, die der Anlegende selbst hat - das UI
|
||||||
|
// bietet deshalb auch nur diese an, statt erst beim Anlegen abzulehnen.
|
||||||
|
const grantable = API_KEY_SCOPES.filter((scope) => hasPermission(scope));
|
||||||
const [name, setName] = useState("Wabenhain-Shop");
|
const [name, setName] = useState("Wabenhain-Shop");
|
||||||
const [expiresInDays, setExpiresInDays] = useState("");
|
const [expiresInDays, setExpiresInDays] = useState("");
|
||||||
const [allowedIps, setAllowedIps] = useState("");
|
const [allowedIps, setAllowedIps] = useState("");
|
||||||
const [scopes, setScopes] = useState<ApiKeyScope[]>([...DEFAULT_SCOPES]);
|
const [chosenScopes, setScopes] = useState<ApiKeyScope[]>([...DEFAULT_SCOPES]);
|
||||||
|
const scopes = chosenScopes.filter((scope) => grantable.includes(scope));
|
||||||
const [created, setCreated] = useState<{
|
const [created, setCreated] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
token: string;
|
token: string;
|
||||||
@@ -202,8 +208,13 @@ export function ApiKeysTab() {
|
|||||||
</div>
|
</div>
|
||||||
<fieldset className="flex w-full flex-col gap-1.5">
|
<fieldset className="flex w-full flex-col gap-1.5">
|
||||||
<legend className="mb-1.5 text-sm font-medium">Was darf der Schlüssel?</legend>
|
<legend className="mb-1.5 text-sm font-medium">Was darf der Schlüssel?</legend>
|
||||||
|
{grantable.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Sie haben selbst keine Rechte, die ein API-Schlüssel tragen kann.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||||
{API_KEY_SCOPES.map((scope) => (
|
{grantable.map((scope) => (
|
||||||
<ToggleChip
|
<ToggleChip
|
||||||
key={scope}
|
key={scope}
|
||||||
label={API_KEY_SCOPE_LABELS[scope]}
|
label={API_KEY_SCOPE_LABELS[scope]}
|
||||||
|
|||||||
Reference in new issue
Block a user