114 lines · 5.2 KB
import { all, first, placeholders } from "../db";
import { accountDb, repoDb } from "../scoped-db";
import { CAPS } from "../limits";
import { repoReadableSql } from "../repos";
import type { DirectNotifyMessage } from "../events/emit";

type Access = DirectNotifyMessage["access"];

const READ_SQL = `SELECT 1 AS ok FROM repos r WHERE r.id = ?1 AND r.hidden_at IS NULL AND ${repoReadableSql("r", "?2")}`;

const ADMIN_SQL = `SELECT 1 AS ok FROM repos r WHERE r.id = ?1 AND (r.owner_id = ?2
  OR EXISTS (SELECT 1 FROM collaborators c WHERE c.repo_id = r.id AND c.user_id = ?2 AND c.permission = 'admin')
  OR EXISTS (SELECT 1 FROM org_members m WHERE m.org_id = r.owner_id AND m.user_id = ?2 AND m.role = 'owner'))`;

/**
 * Org security managers: listed in the org's AccountState, and only while still members of the org (D1).
 * Returns the subset of `userIds` (all managers when omitted) that qualify for the repository's owner.
 */
async function securityManagers(repoId: string, userIds?: string[]): Promise<string[]> {
  const repo = await first<{ owner_id: string }>("SELECT owner_id FROM repos WHERE id = ?", repoId);
  if (!repo) return [];
  const listed = (
    await accountDb(repo.owner_id).all<{ user_id: string }>(
      "SELECT user_id FROM security_managers WHERE account_id = ?",
      repo.owner_id,
    )
  )
    .map((row) => row.user_id)
    .filter((id) => !userIds || userIds.includes(id));
  if (listed.length === 0) return [];
  const members = await all<{ user_id: string }>(
    `SELECT user_id FROM org_members WHERE org_id = ? AND user_id IN (${placeholders(listed.length)})`,
    repo.owner_id,
    ...listed,
  );
  return members.map((row) => row.user_id);
}

async function hasSecurityAccess(repoId: string, userId: string): Promise<boolean> {
  if ((await first(ADMIN_SQL, repoId, userId)) !== null) return true;