git-auth.ts (3.04 KB)
import { Repo } from "@/db/repo";
import { getBasicCredentials } from "@/git/protocol";
import { auth } from "./auth";
import { UnauthorizedError } from "./errors";
import { Result } from "./result";

type VerifyAuthArgs = {
  owner: string;
  repo: string;
  req: Request;
  service: "upload-pack" | "receive-pack";
};

type VerifyAuthResult = {
  valid: boolean;
  message: string;
};

export async function verifyAuth({
  owner,
  repo,
  req,
  service,
}: VerifyAuthArgs): Promise<boolean> {
  const result = await checkPermissionByPAT({ owner, repo, req, service });
  return result.valid;
}

async function checkPermissionByPAT({
  owner,
  repo,
  req,
  service,
}: VerifyAuthArgs): Promise<VerifyAuthResult> {
  const basic = getBasicCredentials(req);
  const token = basic && basic.username === owner ? basic.password : undefined;

  // Step 1: Check if repository exists
  const repoResult = await Repo.getByOwnerAndName({ owner, name: repo });

  if (repoResult.isErr()) {
    return { valid: false, message: "Database error" };
  }

  const repository = repoResult.value;

  if (!repository) {
    return { valid: false, message: "Repository not found" };
  }

  const isPullOperation = service === "upload-pack";
  const isPushOperation = service === "receive-pack";
  const hasToken = !!token;

  // Step 2: Handle anonymous access (no token provided)
  if (!hasToken) {
    // Rule: Push always requires authentication
    if (isPushOperation) {
      return {
        valid: false,
        message: "Authentication required for push operations",
      };
    }

    // Rule: Pull on private repos requires authentication
    if (isPullOperation && repository.isPrivate) {
      return {
        valid: false,
        message: "Authentication required for private repository",
      };
    }

    // Rule: Pull on public repos is allowed anonymously
    return {
      valid: true,
      message: "Anonymous access allowed for public repository",
    };
  }

  // Step 3: Verify the provided token
  const tokenVerificationResult = await Result.tryCatchAsync(
    () => auth.api.verifyApiKey({ body: { key: token.trim() } }),
    (e) => new UnauthorizedError({ cause: e })
  );

  if (tokenVerificationResult.isErr()) {
    return { valid: false, message: "Token verification failed" };
  }

  const tokenVerification = tokenVerificationResult.value;

  if (!tokenVerification.valid) {
    return {
      valid: false,
      message: tokenVerification.error?.message || "Invalid token",
    };
  }

  const isOwner = repository.ownerId === tokenVerification.key?.userId;

  // Step 4: Apply ownership rules for authenticated users
  // Rule: Only owner can push
  if (isPushOperation && !isOwner) {
    return {
      valid: false,
      message: "Not authorized to push to this repository",
    };
  }

  // Rule: Only owner can pull private repos
  if (isPullOperation && repository.isPrivate && !isOwner) {
    return {
      valid: false,
      message: "Not authorized to access private repository",
    };
  }

  // All checks passed
  return { valid: true, message: "Authorized" };
}