/ opencodehub / src / lib / path-scoping.ts
Swadhin Biswas fix(ci): resolve test failures - add workspace, coverage deps, minimatch import · 4/30/2026
e1eb4d4
169 lines · 4.8 KB
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/**
 * Path Scoping Library
 * Validates user permissions for specific file paths in monorepos
 */

import { getDatabase, schema } from "@/db";
import { eq, and, or } from "drizzle-orm";
import { logger } from "./logger";
import { minimatch } from "minimatch";

interface PathPermissionCheck {
    allowed: boolean;
    deniedPaths: string[];
    reason?: string;
}

/**
 * Check if a user has permission to modify specific paths
 */
export async function checkPathPermissions(
    userId: string,
    repositoryId: string,
    paths: string[],
    requiredPermission: "read" | "write" | "admin" = "write"
): Promise<PathPermissionCheck> {
    const db = getDatabase();

    // Get user's teams
    const userTeams = await db.query.teamMembers.findMany({
        where: eq(schema.teamMembers.userId, userId),
        columns: { teamId: true },
    });
    const teamIds = userTeams.map(t => t.teamId);

    // Get all path permissions for this repo
    const permissions = await db.query.repositoryPathPermissions.findMany({
        where: eq(schema.repositoryPathPermissions.repositoryId, repositoryId),
    });

    if (permissions.length === 0) {
        // No path restrictions - allow all
        return { allowed: true, deniedPaths: [] };
    }

    const deniedPaths: string[] = [];

    for (const path of paths) {
        let hasPermission = false;

        for (const perm of permissions) {
            // Check if path matches pattern
            if (!minimatch(path, perm.pathPattern)) {
                continue;
            }

            // Check if user or team has permission
            const isUserMatch = perm.userId === userId;
            const isTeamMatch = perm.teamId && teamIds.includes(perm.teamId);

            if (isUserMatch || isTeamMatch) {
                // Check permission level
                const permLevel = getPermissionLevel(perm.permission);
                const requiredLevel = getPermissionLevel(requiredPermission);

                if (permLevel >= requiredLevel) {
                    hasPermission = true;
                    break;
                }
            }
        }

        // Check if any permission covers this path
        const hasCoverage = permissions.some(p => minimatch(path, p.pathPattern));

        if (hasCoverage && !hasPermission) {
            deniedPaths.push(path);
        }
    }

    return {
        allowed: deniedPaths.length === 0,
        deniedPaths,
        reason: deniedPaths.length > 0
            ? `Insufficient permissions for paths: ${deniedPaths.join(", ")}`
            : undefined,
    };
}

function getPermissionLevel(permission: string): number {
    switch (permission) {
        case "admin": return 3;
        case "write": return 2;
        case "read": return 1;
        default: return 0;
    }
}

/**
 * Get all path permissions for a repository
 */
export async function getPathPermissions(repositoryId: string) {
    const db = getDatabase();

    return db.query.repositoryPathPermissions.findMany({
        where: eq(schema.repositoryPathPermissions.repositoryId, repositoryId),
        with: {
            user: { columns: { id: true, username: true } },
            team: { columns: { id: true, name: true } },
        },
    });
}

/**
 * Add a path permission
 */
export async function addPathPermission(options: {
    repositoryId: string;
    pathPattern: string;
    userId?: string;
    teamId?: string;
    permission: "read" | "write" | "admin";
    requireApproval?: boolean;
}): Promise<boolean> {
    const db = getDatabase();

    if (!options.userId && !options.teamId) {
        throw new Error("Either userId or teamId is required");
    }

    try {
        // @ts-expect-error - Drizzle multi-db union type issue
        await db.insert(schema.repositoryPathPermissions).values({
            id: crypto.randomUUID(),
            repositoryId: options.repositoryId,
            pathPattern: options.pathPattern,
            userId: options.userId,
            teamId: options.teamId,
            permission: options.permission,
            requireApproval: options.requireApproval ? "true" : "false",
        });

        logger.info({ repositoryId: options.repositoryId, pathPattern: options.pathPattern },
            "Path permission added");

        return true;
    } catch (error) {
        logger.error({ error }, "Failed to add path permission");
        return false;
    }
}

/**
 * Remove a path permission
 */
export async function removePathPermission(permissionId: string): Promise<boolean> {
    const db = getDatabase();

    try {
        // @ts-expect-error - Drizzle multi-db union type issue
        await db.delete(schema.repositoryPathPermissions)
            .where(eq(schema.repositoryPathPermissions.id, permissionId));

        return true;
    } catch (error) {
        logger.error({ error }, "Failed to remove path permission");
        return false;
    }
}