All files / src/entities/duo access.ts

100% Statements 48/48
100% Branches 31/31
100% Functions 7/7
100% Lines 41/41

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86  21x 21x 21x 21x 21x 21x               21x     21x         116x 114x         114x 111x     21x 94x 94x         74x 72x     21x 200x 199x             21x 87x 2x       21x       172x 170x 170x 2x 168x 38x 130x 130x 40x 40x 40x     21x       39x 37x 2x 1x 34x    
import type { EffectivePolicy } from '../../configuration/policy';
import { callRestriction, withoutScope } from '../../configuration/policy';
import { readAllowedProject } from '../../configuration/operator-projects';
import { gitlab } from '../../utils/gitlab-api';
import { extractSimpleId } from '../../utils/idConversion';
import { canonicalProjectPath } from '../../utils/api-path';
import { DUO_RESPONSE_BYTES, type Workflow } from './queries';
 
export interface DuoProject {
  id: string;
  path: string;
}
 
/** A missing project or invalid session project ID cannot establish provenance. */
export class UnverifiableDuoProjectError extends Error {}
 
/** The MCP invocation supplies its already combined operator, account and chat policy. */
export function duoPolicy(
  tool: string,
  args: Record<string, unknown>,
  policy?: EffectivePolicy,
): EffectivePolicy {
  if (!policy) throw new Error('Duo tools require the authenticated call execution policy');
  const reason = callRestriction(
    withoutScope(policy),
    { name: tool, group: 'duo', readOnly: tool === 'browse_duo_sessions' },
    args,
  );
  if (reason) throw new Error(`Duo operation is not allowed: ${reason}`);
  return policy;
}
 
export async function readDuoProject(id: string): Promise<DuoProject> {
  const project = await readAllowedProject(id, (path) =>
    gitlab.get(path, {
      responseByteLimit: DUO_RESPONSE_BYTES,
      rawResponse: true,
    }),
  );
  if (!project) throw new UnverifiableDuoProjectError('Duo project is unavailable');
  return project;
}
 
export function permitsDuoProject(project: DuoProject, policy: EffectivePolicy): boolean {
  if (policy.operatorProjectIds && !policy.operatorProjectIds.has(project.id)) return false;
  return (
    !policy.scopeEnforcer ||
    policy.scopeEnforcer.listsId(project.id, 'project') ||
    policy.scopeEnforcer.isAllowed(project.path)
  );
}
 
export function assertDuoProject(project: DuoProject, policy: EffectivePolicy): void {
  if (!permitsDuoProject(project, policy))
    throw new Error('Duo session project is outside the permitted projects or working scope');
}
 
/** Resolve provenance from GitLab, never from a caller's project hint. */
export async function workflowProject(
  workflow: Workflow,
  projectReads?: Map<string, Promise<DuoProject>>,
): Promise<DuoProject | null> {
  if (workflow.projectId === null) return null;
  const id = extractSimpleId(workflow.projectId);
  if (!/^[1-9]\d*$/.test(id))
    throw new UnverifiableDuoProjectError('GitLab returned an invalid Duo project identity');
  if (workflow.project && extractSimpleId(workflow.project.id) === id)
    return { id, path: canonicalProjectPath(workflow.project.fullPath) };
  const cached = projectReads?.get(id);
  if (cached) return cached;
  const pending = readDuoProject(id);
  projectReads?.set(id, pending);
  return pending;
}
 
export async function assertWorkflowProject(
  workflow: Workflow,
  policy: EffectivePolicy,
): Promise<DuoProject | null> {
  const project = await workflowProject(workflow);
  if (project) assertDuoProject(project, policy);
  else if (policy.scopeEnforcer || policy.operatorProjectIds)
    throw new Error('GitLab did not expose a verifiable project for this Duo session');
  return project;
}