Audit provider sign-in launches by employee
This commit is contained in:
1 parent
335d57b77f
commit
de90d6e944
6 files changed
+61
-10
No files matched your search
@@ -121,7 +121,7 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
});
|
||||
ipcMain.handle(IPC_CHANNELS.launchProviderSignIn, (_event, input) => {
|
||||
try {
|
||||
providers.launchSignInWindow(input.orgId, input.providerId);
|
||||
providers.launchSignInWindow(input);
|
||||
return ok(null);
|
||||
} catch (error) {
|
||||
return fail("provider_sign_in_failed", error instanceof Error ? error.message : String(error));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { CancelProviderResponseInput, CreateProviderResponseInput, RevokeProviderCredentialInput, SaveProviderCredentialInput, StreamProviderResponseInput, UpdateProviderPolicyInput } from "../../shared/commands";
|
||||
import type { CancelProviderResponseInput, CreateProviderResponseInput, LaunchProviderSignInInput, RevokeProviderCredentialInput, SaveProviderCredentialInput, StreamProviderResponseInput, UpdateProviderPolicyInput } from "../../shared/commands";
|
||||
import { FEATURE_FLAG_KEY, PROVIDER_AUTH_TYPE, PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, PROVIDER_REQUEST_STATE, type ProviderAuthType, type ProviderExecutionMode } from "../../shared/domain";
|
||||
import type { ProviderStreamRendererEvent } from "../../shared/provider-request-domain";
|
||||
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
|
||||
@@ -14,7 +14,7 @@ import { ProviderRateLimitRegistry } from "./provider-rate-limit-registry";
|
||||
import { ProviderResponseObserver } from "./provider-response-observer";
|
||||
import { hashPrompt, readProviderResponseId, requireStreamText } from "./provider-response-utils";
|
||||
import { ProviderRequestStore } from "./provider-request-store";
|
||||
import { launchProviderSignInWindow } from "./provider-sign-in-window";
|
||||
import { launchProviderSignInWindow, type LaunchProviderSignInWindowInput } from "./provider-sign-in-window";
|
||||
import type { ProviderObservedRateLimitState } from "../../shared/provider-runtime-domain";
|
||||
|
||||
const AUTH_STATE_CONFIGURED = "configured";
|
||||
@@ -41,6 +41,7 @@ export class ProviderService {
|
||||
private readonly db: DatabaseSync,
|
||||
private readonly adapters: ProviderAdapterRegistry = providerAdapters,
|
||||
vault: ProviderSecretVault = new SecretVault(),
|
||||
private readonly launchSignIn: (input: LaunchProviderSignInWindowInput) => void = launchProviderSignInWindow,
|
||||
) {
|
||||
this.audit = new AuditService(db);
|
||||
this.authBroker = new AuthBrokerService(db, vault);
|
||||
@@ -141,23 +142,24 @@ export class ProviderService {
|
||||
}
|
||||
}
|
||||
|
||||
launchSignInWindow(orgId: string, providerId: string): void {
|
||||
launchSignInWindow(input: LaunchProviderSignInInput): void {
|
||||
this.assertEmployee(input.orgId, input.employeeId);
|
||||
const provider = this.db.prepare(`
|
||||
select id, provider_key, label, auth_type
|
||||
from providers
|
||||
where id = ? and org_id = ?
|
||||
`).get(providerId, orgId) as { id: string; provider_key: string; label: string; auth_type: string } | undefined;
|
||||
`).get(input.providerId, input.orgId) as { id: string; provider_key: string; label: string; auth_type: string } | undefined;
|
||||
if (!provider) throw new Error("Provider was not found.");
|
||||
const catalogEntry = this.requireCatalogEntry(provider.provider_key);
|
||||
if (!catalogEntry.signInUrl) throw new Error(`${provider.label} does not support browser sign-in.`);
|
||||
launchProviderSignInWindow({ providerId: provider.id, label: provider.label, signInUrl: catalogEntry.signInUrl });
|
||||
this.launchSignIn({ providerId: provider.id, label: provider.label, signInUrl: catalogEntry.signInUrl });
|
||||
|
||||
this.audit.record({
|
||||
orgId,
|
||||
orgId: input.orgId,
|
||||
entityType: "provider",
|
||||
entityId: provider.id,
|
||||
actorType: "employee",
|
||||
actorId: null,
|
||||
actorId: input.employeeId,
|
||||
action: "sign_in_window_opened",
|
||||
payload: { providerKey: provider.provider_key, authType: provider.auth_type },
|
||||
});
|
||||
@@ -465,6 +467,12 @@ export class ProviderService {
|
||||
return entry;
|
||||
}
|
||||
|
||||
private assertEmployee(orgId: string, employeeId: string): void {
|
||||
const employee = this.db.prepare("select id from employees where id = ? and org_id = ?")
|
||||
.get(employeeId, orgId) as { id: string } | undefined;
|
||||
if (!employee) throw new Error("Provider sign-in employee was not found.");
|
||||
}
|
||||
|
||||
private recordProviderHealth(providerId: string, orgId: string, healthState: string, error: string | null): void {
|
||||
this.db.prepare(`
|
||||
update providers
|
||||
|
||||
@@ -131,6 +131,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
{tab === "providers" && (
|
||||
<ProvidersPanel
|
||||
orgId={orgId}
|
||||
employeeId={employee.id}
|
||||
providers={providers}
|
||||
catalog={snapshot.providerCatalog}
|
||||
agents={agents}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { SnapshotActions } from "../../app/useSnapshot";
|
||||
|
||||
interface ProvidersPanelProps {
|
||||
orgId: string;
|
||||
employeeId: string;
|
||||
providers: ProviderRecord[];
|
||||
catalog: ProviderCatalogEntry[];
|
||||
agents: AgentRecord[];
|
||||
@@ -32,7 +33,7 @@ interface CloudFields {
|
||||
|
||||
const CLOUD_DEFAULTS: CloudFields = { baseUrl: "", region: "", projectId: "", deployment: "" };
|
||||
|
||||
export function ProvidersPanel({ orgId, providers, catalog, agents, providerRateLimits, providerRequests, actions }: ProvidersPanelProps) {
|
||||
export function ProvidersPanel({ orgId, employeeId, providers, catalog, agents, providerRateLimits, providerRequests, actions }: ProvidersPanelProps) {
|
||||
const [active, setActive] = useState<ProviderRecord | null>(null);
|
||||
const [signInLaunched, setSignInLaunched] = useState(false);
|
||||
const connected = providers.filter((provider) => provider.enabled);
|
||||
@@ -60,7 +61,7 @@ export function ProvidersPanel({ orgId, providers, catalog, agents, providerRate
|
||||
const launchSignIn = () => {
|
||||
if (!active) return;
|
||||
setSignInLaunched(true);
|
||||
void actions.run(() => window.agentCompany.launchProviderSignIn({ orgId, providerId: active.id }));
|
||||
void actions.run(() => window.agentCompany.launchProviderSignIn({ orgId, providerId: active.id, employeeId }));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -170,6 +170,7 @@ export interface RevokeProviderCredentialInput {
|
||||
export interface LaunchProviderSignInInput {
|
||||
orgId: string;
|
||||
providerId: string;
|
||||
employeeId: string;
|
||||
}
|
||||
|
||||
export interface CheckProviderHealthInput {
|
||||
|
||||
@@ -169,6 +169,46 @@ describe("provider service", () => {
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("audits provider sign-in launches with the employee actor", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
let launched: { providerId: string; label: string; signInUrl: string } | null = null;
|
||||
const providers = new ProviderService(connection.db, providerAdapters, testVault, (input) => { launched = input; });
|
||||
let orgId = "";
|
||||
let employeeId = "";
|
||||
let copilotProviderId = "";
|
||||
|
||||
connection.transaction(() => {
|
||||
orgId = createOrganization(organization);
|
||||
employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
|
||||
copilotProviderId = providerId(connection, orgId, "github-copilot");
|
||||
});
|
||||
|
||||
expect(() => providers.launchSignInWindow({
|
||||
orgId,
|
||||
providerId: copilotProviderId,
|
||||
employeeId: "emp_missing",
|
||||
})).toThrow("Provider sign-in employee was not found.");
|
||||
|
||||
connection.transaction(() => providers.launchSignInWindow({ orgId, providerId: copilotProviderId, employeeId }));
|
||||
|
||||
const state = snapshot.load();
|
||||
expect(launched).toMatchObject({
|
||||
providerId: copilotProviderId,
|
||||
label: "GitHub Copilot",
|
||||
signInUrl: "https://github.com/login",
|
||||
});
|
||||
expect(state.auditEvents).toContainEqual(expect.objectContaining({
|
||||
entityType: "provider",
|
||||
entityId: copilotProviderId,
|
||||
actorType: "employee",
|
||||
actorId: employeeId,
|
||||
action: "sign_in_window_opened",
|
||||
}));
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("records provider request results through direct execution adapters", async () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
|
||||
Reference in new issue
Block a user