mirror of
https://github.com/kunkunsh/kunkun.git
synced 2025-04-12 09:49:42 +00:00

Translate windows style back slash to posix style slash in order for minimatch to work. https://www.npmjs.com/package/minimatch#windows
197 lines
6.1 KiB
TypeScript
197 lines
6.1 KiB
TypeScript
import * as pathAPI from "@tauri-apps/api/path"
|
|
import { BaseDirectory } from "@tauri-apps/api/path"
|
|
import { exists, mkdir } from "@tauri-apps/plugin-fs"
|
|
import { platform } from "@tauri-apps/plugin-os"
|
|
import { minimatch } from "minimatch"
|
|
import type {
|
|
FsPermissionScoped,
|
|
OpenPermissionScoped,
|
|
ShellPermissionScoped
|
|
} from "../permissions/schema"
|
|
|
|
export async function combinePathAndBaseDir(target: string, baseDir?: BaseDirectory) {
|
|
if (!baseDir) return target
|
|
switch (baseDir) {
|
|
case BaseDirectory.Desktop:
|
|
return await pathAPI.join(await pathAPI.desktopDir(), target)
|
|
case BaseDirectory.Document:
|
|
return await pathAPI.join(await pathAPI.documentDir(), target)
|
|
case BaseDirectory.Download:
|
|
return await pathAPI.join(await pathAPI.downloadDir(), target)
|
|
case BaseDirectory.Home:
|
|
return await pathAPI.join(await pathAPI.homeDir(), target)
|
|
case BaseDirectory.AppData:
|
|
return await pathAPI.join(await pathAPI.appDataDir(), target)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
export const mapDirAliasToDirFn: Record<string, () => Promise<string>> = {
|
|
$DESKTOP: pathAPI.desktopDir,
|
|
$DOCUMENT: pathAPI.documentDir,
|
|
$DOWNLOAD: pathAPI.downloadDir,
|
|
$HOME: pathAPI.homeDir,
|
|
$APPDATA: pathAPI.appDataDir
|
|
}
|
|
|
|
export const AllPathAliases = [
|
|
...Object.keys(mapDirAliasToDirFn),
|
|
"$EXTENSION",
|
|
"$EXTENSION_SUPPORT"
|
|
]
|
|
|
|
export function pathStartsWithAlias(path: string) {
|
|
return AllPathAliases.some((alias) => path.startsWith(alias))
|
|
}
|
|
|
|
/**
|
|
* @example
|
|
* Translate $DESKTOP/* to /Users/username/Desktop/*
|
|
* Translate $DOWNLOAD/** to /Users/username/Downloads/**
|
|
* @param scope expected to be like $DESKTOP/*, $DOWNLOAD/**, $DOCUMENT/abc/*.txt
|
|
*/
|
|
export async function translateScopeToPath(scope: string, extensionDir: string): Promise<string> {
|
|
if (scope.startsWith("$EXTENSION_SUPPORT")) {
|
|
const appDataDir = await pathAPI.appDataDir()
|
|
if (extensionDir.startsWith(appDataDir)) {
|
|
return pathAPI.join(appDataDir, "extensions_support", await pathAPI.basename(extensionDir))
|
|
} else {
|
|
const extSupportDir = await pathAPI.join(extensionDir, "extensions_support")
|
|
if (!(await exists(extSupportDir))) {
|
|
await mkdir(extSupportDir, { recursive: true })
|
|
}
|
|
return extSupportDir
|
|
}
|
|
}
|
|
if (scope.startsWith("$EXTENSION")) {
|
|
return pathAPI.join(extensionDir, scope.slice("$EXTENSION".length))
|
|
}
|
|
for (const key of Object.keys(mapDirAliasToDirFn)) {
|
|
if (scope.startsWith(key)) {
|
|
const alias = key
|
|
const pattern = scope.slice(key.length)
|
|
const dirFn = mapDirAliasToDirFn[alias]
|
|
if (!dirFn) {
|
|
throw new Error(`Invalid scope alias: ${alias}`)
|
|
}
|
|
const fullDir = await dirFn()
|
|
return pathAPI.join(fullDir, pattern)
|
|
}
|
|
}
|
|
return scope
|
|
throw new Error(`Invalid scope: ${scope}`)
|
|
}
|
|
|
|
/**
|
|
* TODO: but this function also does security check to prevent parent directory traversal
|
|
* @param target full path to file
|
|
* @param scope expected to be like $DESKTOP/*, $DOWNLOAD/**
|
|
*/
|
|
export async function matchPathAndScope(
|
|
target: string,
|
|
scope: string,
|
|
extensionDir: string
|
|
): Promise<boolean> {
|
|
let translatedTarget = await translateScopeToPath(target, extensionDir)
|
|
let translatedScope = await translateScopeToPath(scope, extensionDir)
|
|
if (platform() === "windows") {
|
|
translatedTarget = translatedTarget.replaceAll("\\", "/")
|
|
translatedScope = translatedScope.replaceAll("\\", "/")
|
|
}
|
|
return minimatch(translatedTarget, translatedScope)
|
|
}
|
|
|
|
/**
|
|
* This is a helper function to verify scoped permission for path
|
|
* If a scoped permission needs access to paths, this function verify whether the path is allowed by the permission
|
|
* @param requiredPermissions
|
|
* @param userPermissionScopes
|
|
* @param path
|
|
* @param options
|
|
* @returns
|
|
*/
|
|
export async function verifyGeneralPathScopedPermission<T extends string[]>(
|
|
requiredPermissions: T,
|
|
userPermissionScopes: (FsPermissionScoped | OpenPermissionScoped | ShellPermissionScoped)[],
|
|
path: string | URL,
|
|
extensionDir: string,
|
|
options?: { baseDir?: BaseDirectory }
|
|
) {
|
|
path = path.toString()
|
|
const fullPath = await combinePathAndBaseDir(path, options?.baseDir)
|
|
if (!fullPath) {
|
|
throw new Error("Invalid path or base directory")
|
|
}
|
|
const matchedPermissionScope = userPermissionScopes.filter((p) =>
|
|
requiredPermissions.includes(p.permission)
|
|
)
|
|
if (matchedPermissionScope.length === 0) {
|
|
throw new Error(
|
|
`Path Permission denied. Require one of these: [${requiredPermissions.join(", ")}] for path: ${fullPath}`
|
|
)
|
|
}
|
|
|
|
for (const permission of matchedPermissionScope) {
|
|
// deny has priority, if deny rule is matched, we ignore allow rule
|
|
for (const deny of permission.deny || []) {
|
|
if (!deny.path) continue
|
|
if (await matchPathAndScope(fullPath, deny.path, extensionDir)) {
|
|
throw new Error(`Permission denied for path: ${fullPath} by rule ${deny.path}`)
|
|
}
|
|
}
|
|
for (const allow of permission.allow || []) {
|
|
if (!allow.path) continue
|
|
if (await matchPathAndScope(fullPath, allow.path, extensionDir)) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
// No Allow rule and path matched
|
|
throw new Error(`Permission denied for path: ${path}, no rule matched.`)
|
|
}
|
|
|
|
/**
|
|
* This permission verifier helps to verify scoped permission for URL or path
|
|
* Pass in user scoped permission, value and key to verify, return true if permission is allowed
|
|
* @param userPermissionScopes
|
|
* @param value
|
|
* @param key
|
|
* @returns
|
|
*/
|
|
export async function verifyScopedPermission(
|
|
userPermissionScopes: (FsPermissionScoped | OpenPermissionScoped | ShellPermissionScoped)[],
|
|
value: string,
|
|
key: "url" | "path",
|
|
extensionDir: string
|
|
): Promise<boolean> {
|
|
async function match(value: string, scope: string): Promise<boolean> {
|
|
if (key === "url") {
|
|
return minimatch(value, scope)
|
|
} else if (key === "path") {
|
|
return matchPathAndScope(value, scope, extensionDir)
|
|
} else {
|
|
throw new Error(`Invalid key: ${key}`)
|
|
}
|
|
}
|
|
let pass = false
|
|
for (const permission of userPermissionScopes) {
|
|
for (const allow of permission.allow || []) {
|
|
if (allow[key] && (await match(value, allow[key]))) {
|
|
pass = true
|
|
break
|
|
}
|
|
}
|
|
if (pass) {
|
|
break
|
|
}
|
|
for (const deny of permission.deny || []) {
|
|
if (deny[key] && (await match(value, deny[key]))) {
|
|
pass = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return pass
|
|
}
|