diff --git a/src/components/layout/auth-gate.tsx b/src/components/layout/auth-gate.tsx index 43112ef..0de8504 100644 --- a/src/components/layout/auth-gate.tsx +++ b/src/components/layout/auth-gate.tsx @@ -31,6 +31,18 @@ export const AuthGate = ({ children }: { children: ReactNode }) => { return <>{children}; } + if (status === "denied") { + return ( +
+ +
+ No valid operator certificate + Install an operator certificate issued by this fleet's PKI to continue. +
+
+ ); + } + return (
diff --git a/src/context/auth.test.tsx b/src/context/auth.test.tsx new file mode 100644 index 0000000..d3d2961 --- /dev/null +++ b/src/context/auth.test.tsx @@ -0,0 +1,64 @@ +/* +Apache License 2.0 + +Copyright 2026 Shane + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { AuthProvider, useAuth } from "@/context/auth"; + +const whoAmI = vi.fn(); +vi.mock("@/lib/fleet/client", () => ({ fleetClient: () => ({ whoAmI }) })); +vi.mock("@/lib/fleet/mode", () => ({ fleetMode: () => "live" })); + +const Probe = () => { + const { operator, status } = useAuth(); + return ( +
+ {status}:{operator?.level ?? "none"} +
+ ); +}; + +describe("AuthProvider (live)", () => { + beforeEach(() => whoAmI.mockReset()); + + it("populates operator + level from WhoAmI", async () => { + whoAmI.mockResolvedValue({ + operator: { cn: "op@acme.example", level: "admin", serial: "0A:BC" }, + }); + render( + + + , + ); + await waitFor(() => expect(screen.getByText("authenticated:admin")).toBeInTheDocument()); + }); + + it("goes denied when WhoAmI returns no operator", async () => { + // The manager withholds an operator identity when the presented cert is not + // a valid operator; the provider renders the denied gate. (The parallel + // rejected-RPC path runs the same setState in the provider's .catch.) + whoAmI.mockResolvedValue({ operator: undefined }); + render( + + + , + ); + await waitFor(() => expect(screen.getByText("denied:none")).toBeInTheDocument()); + }); +}); diff --git a/src/context/auth.tsx b/src/context/auth.tsx index 64ac410..05bcd0a 100644 --- a/src/context/auth.tsx +++ b/src/context/auth.tsx @@ -18,22 +18,28 @@ limitations under the License. import { createContext, type ReactNode, useContext, useEffect, useState } from "react"; -// Auth-gate stub for browser-side mTLS. In production the operator authenticates -// with a smart-card / YubiKey-backed client certificate presented by the browser -// during the TLS handshake; there is no password login. A dev browser cannot -// perform smart-card mTLS, so this stub resolves to a fixed operator identity -// after a short delay to exercise the gate. Wiring the real client-cert handshake -// (and reading the verified subject the manager reports back) is a later concern. +import { fleetClient } from "@/lib/fleet/client"; +import { fleetMode } from "@/lib/fleet/mode"; -export type AuthStatus = "authenticated" | "presenting"; +// Auth-gate for browser-side mTLS. In production the operator authenticates +// with a smart-card / YubiKey-backed client certificate presented by the browser +// during the TLS handshake; there is no password login. The manager verifies +// the handshake and reports the operator's identity back through WhoAmI. A dev +// browser cannot perform smart-card mTLS, so the mock data source keeps a fixed +// operator identity to exercise the gate without a real certificate. +export type AuthStatus = "authenticated" | "denied" | "presenting"; export interface Operator { /** Common name from the operator's client certificate subject. */ commonName: string; + /** Authorization level the manager assigned to this operator. */ + level: OperatorLevel; /** Certificate serial the manager verified during the mTLS handshake. */ serial: string; } +export type OperatorLevel = "admin" | "operator" | "viewer"; + interface AuthState { operator: null | Operator; status: AuthStatus; @@ -43,18 +49,45 @@ const AuthContext = createContext(undefined); const DEV_OPERATOR: Operator = { commonName: "operator@acme.example", + level: "admin", serial: "3A:7F:0C:91:D2:44:8B:1E", }; +const toLevel = (s: string): OperatorLevel => (s === "admin" || s === "operator" ? s : "viewer"); + export const AuthProvider = ({ children }: { children: ReactNode }) => { const [state, setState] = useState({ operator: null, status: "presenting" }); useEffect(() => { - // Simulate the handshake resolving to a verified operator identity. - const timer = setTimeout(() => { - setState({ operator: DEV_OPERATOR, status: "authenticated" }); - }, 600); - return () => clearTimeout(timer); + if (fleetMode() === "mock") { + // Simulate the handshake resolving to a verified operator identity. + const timer = setTimeout( + () => setState({ operator: DEV_OPERATOR, status: "authenticated" }), + 600, + ); + return () => clearTimeout(timer); + } + let cancelled = false; + void fleetClient() + .whoAmI({}) + .then((resp) => { + if (cancelled) return; + const op = resp.operator; + if (!op) { + setState({ operator: null, status: "denied" }); + return; + } + setState({ + operator: { commonName: op.cn, level: toLevel(op.level), serial: op.serial }, + status: "authenticated", + }); + }) + .catch(() => { + if (!cancelled) setState({ operator: null, status: "denied" }); + }); + return () => { + cancelled = true; + }; }, []); return {children}; diff --git a/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts b/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts index 8a88b52..26801f4 100644 --- a/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts +++ b/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts @@ -1,10 +1,3 @@ -// Bridge copy: generated in the api repo (gen/ts/cryptos/fleet/v1/fleet_pb.ts) -// from proto/cryptos/fleet/v1/fleet.proto and copied here verbatim. A cross-repo -// path alias can't resolve this file's own bare-specifier imports (its -// node_modules lookup walks up from the api repo, not this one), so until the -// api repo publishes this as a package, regenerate there and re-copy here on -// any fleet.proto change. -// // @generated by protoc-gen-es v2.12.1 with parameter "target=ts" // @generated from file cryptos/fleet/v1/fleet.proto (package cryptos.fleet.v1, syntax proto3) /* eslint-disable */ @@ -17,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file cryptos/fleet/v1/fleet.proto. */ export const file_cryptos_fleet_v1_fleet: GenFile = /*@__PURE__*/ - fileDesc("ChxjcnlwdG9zL2ZsZWV0L3YxL2ZsZWV0LnByb3RvEhBjcnlwdG9zLmZsZWV0LnYxIhIKEExpc3ROb2Rlc1JlcXVlc3QiQQoRTGlzdE5vZGVzUmVzcG9uc2USLAoFbm9kZXMYASADKAsyHS5jcnlwdG9zLmZsZWV0LnYxLk5vZGVTdW1tYXJ5Ih4KDkdldE5vZGVSZXF1ZXN0EgwKBG5hbWUYASABKAkiPQoPR2V0Tm9kZVJlc3BvbnNlEioKBG5vZGUYASABKAsyHC5jcnlwdG9zLmZsZWV0LnYxLk5vZGVEZXRhaWwiJwoXTGlzdENlcnRpZmljYXRlc1JlcXVlc3QSDAoEbm9kZRgBIAEoCSJPChhMaXN0Q2VydGlmaWNhdGVzUmVzcG9uc2USMwoMY2VydGlmaWNhdGVzGAEgAygLMh0uY3J5cHRvcy5mbGVldC52MS5DZXJ0aWZpY2F0ZSIVChNMaXN0UHJvZmlsZXNSZXF1ZXN0IkQKFExpc3RQcm9maWxlc1Jlc3BvbnNlEiwKBWl0ZW1zGAEgAygLMh0uY3J5cHRvcy5mbGVldC52MS5DZXJ0UHJvZmlsZSIVChNMaXN0QWRhcHRlcnNSZXF1ZXN0IkoKFExpc3RBZGFwdGVyc1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50QWRhcHRlciISChBMaXN0QXVkaXRSZXF1ZXN0IkAKEUxpc3RBdWRpdFJlc3BvbnNlEisKBWl0ZW1zGAEgAygLMhwuY3J5cHRvcy5mbGVldC52MS5BdWRpdEV2ZW50IhgKFkxpc3RFbnJvbGxtZW50c1JlcXVlc3QiTQoXTGlzdEVucm9sbG1lbnRzUmVzcG9uc2USMgoFaXRlbXMYASADKAsyIy5jcnlwdG9zLmZsZWV0LnYxLkVucm9sbG1lbnRSZXF1ZXN0Iq8BCgtOb2RlU3VtbWFyeRIMCgRuYW1lGAEgASgJEg8KB2FkZHJlc3MYAiABKAkSDAoEcm9sZRgDIAEoCRIWCg5pZGVudGl0eV9zdGF0ZRgEIAEoCRIKCgJjbhgFIAEoCRIOCgZpc3N1ZXIYBiABKAkSKAoGaGVhbHRoGAcgASgOMhguY3J5cHRvcy5mbGVldC52MS5IZWFsdGgSFQoNaGVhbHRoX2RldGFpbBgIIAEoCSJJCgxOb2RlSWRlbnRpdHkSEQoJY2hhaW5fcGVtGAEgASgJEhEKCWNoYWluX2RlchgCIAMoDBITCgtsZWFmX3NoYTI1NhgDIAEoCSKpAQoKTm9kZURldGFpbBIuCgdzdW1tYXJ5GAEgASgLMh0uY3J5cHRvcy5mbGVldC52MS5Ob2RlU3VtbWFyeRIwCghpZGVudGl0eRgCIAEoCzIeLmNyeXB0b3MuZmxlZXQudjEuTm9kZUlkZW50aXR5EhUKDXRwbV9hdmFpbGFibGUYAyABKAgSEgoKYm9vdF9jb3VudBgEIAEoBBIOCgZ1cHRpbWUYBSABKAkiwAEKC0NlcnRpZmljYXRlEg4KBnNlcmlhbBgBIAEoCRISCgpzdWJqZWN0X2NuGAIgASgJEhMKC2lzc3Vlcl9ub2RlGAMgASgJEgwKBGtpbmQYBCABKAkSDgoGc3RhdHVzGAUgASgJEhIKCm5vdF9iZWZvcmUYBiABKAkSEQoJbm90X2FmdGVyGAcgASgJEg8KB3Byb2ZpbGUYCCABKAkSEgoKcmV2b2tlZF9hdBgJIAEoCRIOCgZyZWFzb24YCiABKAkinAEKC0NlcnRQcm9maWxlEgwKBG5hbWUYASABKAkSDwoHa2V5X2FsZxgCIAEoCRIRCglrZXlfdXNhZ2UYAyADKAkSFQoNZXh0X2tleV91c2FnZRgEIAMoCRINCgVpc19jYRgFIAEoCBIQCghwYXRoX2xlbhgGIAEoBRIMCgRzYW5zGAcgAygJEhUKDXZhbGlkaXR5X2RheXMYCCABKAUijQEKEUVucm9sbG1lbnRBZGFwdGVyEgwKBGtpbmQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghlbmRwb2ludBgDIAEoCRIPCgdwcm9maWxlGAQgASgJEg8KB2VuYWJsZWQYBSABKAgSEgoKY2hhbGxlbmdlcxgGIAMoCRIUCgxncG9fdGVtcGxhdGUYByABKAkibQoKQXVkaXRFdmVudBIKCgJpZBgBIAEoCRIKCgJhdBgCIAEoCRIMCgRraW5kGAMgASgJEg8KB3N1bW1hcnkYBCABKAkSEwoLdGFyZ2V0X2tpbmQYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkirAIKEUVucm9sbG1lbnRSZXF1ZXN0EgoKAmlkGAEgASgJEhUKDXByb3Bvc2VkX25hbWUYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwYXJlbnRfY24YBCABKAkSDwoHYWRkcmVzcxgFIAEoCRIOCgZzdGF0dXMYBiABKAkSGwoTYXR0ZXN0YXRpb25fc3VtbWFyeRgHIAEoCRIbChNhdHRlc3RhdGlvbl9ub2RlX2lkGAggASgJEhQKDGNzcl9rZXlfdHlwZRgJIAEoCRIWCg5jc3Jfc3ViamVjdF9jbhgKIAEoCRIUCgxyZXF1ZXN0ZWRfYXQYCyABKAkSGAoQcmVqZWN0aW9uX3JlYXNvbhgMIAEoCRIaChJhZG1pdHRlZF9ub2RlX25hbWUYDSABKAkqUgoGSGVhbHRoEhYKEkhFQUxUSF9VTlNQRUNJRklFRBAAEg0KCUhFQUxUSF9VUBABEg8KC0hFQUxUSF9ET1dOEAISEAoMSEVBTFRIX0VSUk9SEAMymwUKDEZsZWV0U2VydmljZRJUCglMaXN0Tm9kZXMSIi5jcnlwdG9zLmZsZWV0LnYxLkxpc3ROb2Rlc1JlcXVlc3QaIy5jcnlwdG9zLmZsZWV0LnYxLkxpc3ROb2Rlc1Jlc3BvbnNlEk4KB0dldE5vZGUSIC5jcnlwdG9zLmZsZWV0LnYxLkdldE5vZGVSZXF1ZXN0GiEuY3J5cHRvcy5mbGVldC52MS5HZXROb2RlUmVzcG9uc2USaQoQTGlzdENlcnRpZmljYXRlcxIpLmNyeXB0b3MuZmxlZXQudjEuTGlzdENlcnRpZmljYXRlc1JlcXVlc3QaKi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RDZXJ0aWZpY2F0ZXNSZXNwb25zZRJdCgxMaXN0UHJvZmlsZXMSJS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RQcm9maWxlc1JlcXVlc3QaJi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RQcm9maWxlc1Jlc3BvbnNlEl0KDExpc3RBZGFwdGVycxIlLmNyeXB0b3MuZmxlZXQudjEuTGlzdEFkYXB0ZXJzUmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuTGlzdEFkYXB0ZXJzUmVzcG9uc2USVAoJTGlzdEF1ZGl0EiIuY3J5cHRvcy5mbGVldC52MS5MaXN0QXVkaXRSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5MaXN0QXVkaXRSZXNwb25zZRJmCg9MaXN0RW5yb2xsbWVudHMSKC5jcnlwdG9zLmZsZWV0LnYxLkxpc3RFbnJvbGxtZW50c1JlcXVlc3QaKS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RFbnJvbGxtZW50c1Jlc3BvbnNlQjhaNmdpdGh1Yi5jb20vQ3J5cHRPUy1QS0kvYXBpL2dvL2NyeXB0b3MvZmxlZXQvdjE7ZmxlZXR2MWIGcHJvdG8z"); + fileDesc("ChxjcnlwdG9zL2ZsZWV0L3YxL2ZsZWV0LnByb3RvEhBjcnlwdG9zLmZsZWV0LnYxIhIKEExpc3ROb2Rlc1JlcXVlc3QiQQoRTGlzdE5vZGVzUmVzcG9uc2USLAoFbm9kZXMYASADKAsyHS5jcnlwdG9zLmZsZWV0LnYxLk5vZGVTdW1tYXJ5Ih4KDkdldE5vZGVSZXF1ZXN0EgwKBG5hbWUYASABKAkiPQoPR2V0Tm9kZVJlc3BvbnNlEioKBG5vZGUYASABKAsyHC5jcnlwdG9zLmZsZWV0LnYxLk5vZGVEZXRhaWwiJwoXTGlzdENlcnRpZmljYXRlc1JlcXVlc3QSDAoEbm9kZRgBIAEoCSJPChhMaXN0Q2VydGlmaWNhdGVzUmVzcG9uc2USMwoMY2VydGlmaWNhdGVzGAEgAygLMh0uY3J5cHRvcy5mbGVldC52MS5DZXJ0aWZpY2F0ZSIVChNMaXN0UHJvZmlsZXNSZXF1ZXN0IkQKFExpc3RQcm9maWxlc1Jlc3BvbnNlEiwKBWl0ZW1zGAEgAygLMh0uY3J5cHRvcy5mbGVldC52MS5DZXJ0UHJvZmlsZSIVChNMaXN0QWRhcHRlcnNSZXF1ZXN0IkoKFExpc3RBZGFwdGVyc1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50QWRhcHRlciISChBMaXN0QXVkaXRSZXF1ZXN0IkAKEUxpc3RBdWRpdFJlc3BvbnNlEisKBWl0ZW1zGAEgAygLMhwuY3J5cHRvcy5mbGVldC52MS5BdWRpdEV2ZW50IhgKFkxpc3RFbnJvbGxtZW50c1JlcXVlc3QiTQoXTGlzdEVucm9sbG1lbnRzUmVzcG9uc2USMgoFaXRlbXMYASADKAsyIy5jcnlwdG9zLmZsZWV0LnYxLkVucm9sbG1lbnRSZXF1ZXN0Iq8BCgtOb2RlU3VtbWFyeRIMCgRuYW1lGAEgASgJEg8KB2FkZHJlc3MYAiABKAkSDAoEcm9sZRgDIAEoCRIWCg5pZGVudGl0eV9zdGF0ZRgEIAEoCRIKCgJjbhgFIAEoCRIOCgZpc3N1ZXIYBiABKAkSKAoGaGVhbHRoGAcgASgOMhguY3J5cHRvcy5mbGVldC52MS5IZWFsdGgSFQoNaGVhbHRoX2RldGFpbBgIIAEoCSJJCgxOb2RlSWRlbnRpdHkSEQoJY2hhaW5fcGVtGAEgASgJEhEKCWNoYWluX2RlchgCIAMoDBITCgtsZWFmX3NoYTI1NhgDIAEoCSKpAQoKTm9kZURldGFpbBIuCgdzdW1tYXJ5GAEgASgLMh0uY3J5cHRvcy5mbGVldC52MS5Ob2RlU3VtbWFyeRIwCghpZGVudGl0eRgCIAEoCzIeLmNyeXB0b3MuZmxlZXQudjEuTm9kZUlkZW50aXR5EhUKDXRwbV9hdmFpbGFibGUYAyABKAgSEgoKYm9vdF9jb3VudBgEIAEoBBIOCgZ1cHRpbWUYBSABKAkiwAEKC0NlcnRpZmljYXRlEg4KBnNlcmlhbBgBIAEoCRISCgpzdWJqZWN0X2NuGAIgASgJEhMKC2lzc3Vlcl9ub2RlGAMgASgJEgwKBGtpbmQYBCABKAkSDgoGc3RhdHVzGAUgASgJEhIKCm5vdF9iZWZvcmUYBiABKAkSEQoJbm90X2FmdGVyGAcgASgJEg8KB3Byb2ZpbGUYCCABKAkSEgoKcmV2b2tlZF9hdBgJIAEoCRIOCgZyZWFzb24YCiABKAkinAEKC0NlcnRQcm9maWxlEgwKBG5hbWUYASABKAkSDwoHa2V5X2FsZxgCIAEoCRIRCglrZXlfdXNhZ2UYAyADKAkSFQoNZXh0X2tleV91c2FnZRgEIAMoCRINCgVpc19jYRgFIAEoCBIQCghwYXRoX2xlbhgGIAEoBRIMCgRzYW5zGAcgAygJEhUKDXZhbGlkaXR5X2RheXMYCCABKAUijQEKEUVucm9sbG1lbnRBZGFwdGVyEgwKBGtpbmQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghlbmRwb2ludBgDIAEoCRIPCgdwcm9maWxlGAQgASgJEg8KB2VuYWJsZWQYBSABKAgSEgoKY2hhbGxlbmdlcxgGIAMoCRIUCgxncG9fdGVtcGxhdGUYByABKAkibQoKQXVkaXRFdmVudBIKCgJpZBgBIAEoCRIKCgJhdBgCIAEoCRIMCgRraW5kGAMgASgJEg8KB3N1bW1hcnkYBCABKAkSEwoLdGFyZ2V0X2tpbmQYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkirAIKEUVucm9sbG1lbnRSZXF1ZXN0EgoKAmlkGAEgASgJEhUKDXByb3Bvc2VkX25hbWUYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwYXJlbnRfY24YBCABKAkSDwoHYWRkcmVzcxgFIAEoCRIOCgZzdGF0dXMYBiABKAkSGwoTYXR0ZXN0YXRpb25fc3VtbWFyeRgHIAEoCRIbChNhdHRlc3RhdGlvbl9ub2RlX2lkGAggASgJEhQKDGNzcl9rZXlfdHlwZRgJIAEoCRIWCg5jc3Jfc3ViamVjdF9jbhgKIAEoCRIUCgxyZXF1ZXN0ZWRfYXQYCyABKAkSGAoQcmVqZWN0aW9uX3JlYXNvbhgMIAEoCRIaChJhZG1pdHRlZF9ub2RlX25hbWUYDSABKAkiDwoNV2hvQW1JUmVxdWVzdCI9ChBPcGVyYXRvcklkZW50aXR5EgoKAmNuGAEgASgJEg4KBnNlcmlhbBgCIAEoCRINCgVsZXZlbBgDIAEoCSJGCg5XaG9BbUlSZXNwb25zZRI0CghvcGVyYXRvchgBIAEoCzIiLmNyeXB0b3MuZmxlZXQudjEuT3BlcmF0b3JJZGVudGl0eSpSCgZIZWFsdGgSFgoSSEVBTFRIX1VOU1BFQ0lGSUVEEAASDQoJSEVBTFRIX1VQEAESDwoLSEVBTFRIX0RPV04QAhIQCgxIRUFMVEhfRVJST1IQAzLoBQoMRmxlZXRTZXJ2aWNlElQKCUxpc3ROb2RlcxIiLmNyeXB0b3MuZmxlZXQudjEuTGlzdE5vZGVzUmVxdWVzdBojLmNyeXB0b3MuZmxlZXQudjEuTGlzdE5vZGVzUmVzcG9uc2USTgoHR2V0Tm9kZRIgLmNyeXB0b3MuZmxlZXQudjEuR2V0Tm9kZVJlcXVlc3QaIS5jcnlwdG9zLmZsZWV0LnYxLkdldE5vZGVSZXNwb25zZRJpChBMaXN0Q2VydGlmaWNhdGVzEikuY3J5cHRvcy5mbGVldC52MS5MaXN0Q2VydGlmaWNhdGVzUmVxdWVzdBoqLmNyeXB0b3MuZmxlZXQudjEuTGlzdENlcnRpZmljYXRlc1Jlc3BvbnNlEl0KDExpc3RQcm9maWxlcxIlLmNyeXB0b3MuZmxlZXQudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USXQoMTGlzdEFkYXB0ZXJzEiUuY3J5cHRvcy5mbGVldC52MS5MaXN0QWRhcHRlcnNSZXF1ZXN0GiYuY3J5cHRvcy5mbGVldC52MS5MaXN0QWRhcHRlcnNSZXNwb25zZRJUCglMaXN0QXVkaXQSIi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlcXVlc3QaIy5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlc3BvbnNlEmYKD0xpc3RFbnJvbGxtZW50cxIoLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVxdWVzdBopLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVzcG9uc2USSwoGV2hvQW1JEh8uY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXF1ZXN0GiAuY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXNwb25zZUI4WjZnaXRodWIuY29tL0NyeXB0T1MtUEtJL2FwaS9nby9jcnlwdG9zL2ZsZWV0L3YxO2ZsZWV0djFiBnByb3RvMw"); /** * @generated from message cryptos.fleet.v1.ListNodesRequest @@ -683,6 +676,68 @@ export type EnrollmentRequest = Message<"cryptos.fleet.v1.EnrollmentRequest"> & export const EnrollmentRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_cryptos_fleet_v1_fleet, 21); +/** + * @generated from message cryptos.fleet.v1.WhoAmIRequest + */ +export type WhoAmIRequest = Message<"cryptos.fleet.v1.WhoAmIRequest"> & { +}; + +/** + * Describes the message cryptos.fleet.v1.WhoAmIRequest. + * Use `create(WhoAmIRequestSchema)` to create a new message. + */ +export const WhoAmIRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 22); + +/** + * OperatorIdentity is the operator the manager verified from the mTLS client + * certificate presented on the management-plane connection. + * + * @generated from message cryptos.fleet.v1.OperatorIdentity + */ +export type OperatorIdentity = Message<"cryptos.fleet.v1.OperatorIdentity"> & { + /** + * @generated from field: string cn = 1; + */ + cn: string; + + /** + * @generated from field: string serial = 2; + */ + serial: string; + + /** + * access level: "viewer" | "operator" | "admin" + * + * @generated from field: string level = 3; + */ + level: string; +}; + +/** + * Describes the message cryptos.fleet.v1.OperatorIdentity. + * Use `create(OperatorIdentitySchema)` to create a new message. + */ +export const OperatorIdentitySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 23); + +/** + * @generated from message cryptos.fleet.v1.WhoAmIResponse + */ +export type WhoAmIResponse = Message<"cryptos.fleet.v1.WhoAmIResponse"> & { + /** + * @generated from field: cryptos.fleet.v1.OperatorIdentity operator = 1; + */ + operator?: OperatorIdentity | undefined; +}; + +/** + * Describes the message cryptos.fleet.v1.WhoAmIResponse. + * Use `create(WhoAmIResponseSchema)` to create a new message. + */ +export const WhoAmIResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 24); + /** * Health reports the manager's view of node reachability, independent of * the node's own reported identity state. @@ -799,6 +854,17 @@ export const FleetService: GenService<{ input: typeof ListEnrollmentsRequestSchema; output: typeof ListEnrollmentsResponseSchema; }, + /** + * WhoAmI echoes the operator identity the manager verified from the + * presented client certificate (subject CN, cert serial, access level). + * + * @generated from rpc cryptos.fleet.v1.FleetService.WhoAmI + */ + whoAmI: { + methodKind: "unary"; + input: typeof WhoAmIRequestSchema; + output: typeof WhoAmIResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_cryptos_fleet_v1_fleet, 0); diff --git a/src/lib/fleet/client.ts b/src/lib/fleet/client.ts index 9dd5d67..34d8e5b 100644 --- a/src/lib/fleet/client.ts +++ b/src/lib/fleet/client.ts @@ -30,5 +30,6 @@ export const fleetClient = () => FleetService, createConnectTransport({ baseUrl: import.meta.env.VITE_FLEET_API ?? "http://localhost:8080", + fetch: (input, init) => fetch(input, { ...init, credentials: "include" }), }), );