297 lines
8.5 KiB
TypeScript
297 lines
8.5 KiB
TypeScript
import { readFileSync } from '../fileRead.js'
|
|
import { getFsImplementation, safeResolvePath } from '../fsOperations.js'
|
|
import { safeParseJSON } from '../json.js'
|
|
import { logError } from '../log.js'
|
|
import {
|
|
type EditableSettingSource,
|
|
getEnabledSettingSources,
|
|
type SettingSource,
|
|
} from '../settings/constants.js'
|
|
import {
|
|
getSettingsFilePathForSource,
|
|
getSettingsForSource,
|
|
updateSettingsForSource,
|
|
} from '../settings/settings.js'
|
|
import type { SettingsJson } from '../settings/types.js'
|
|
import type {
|
|
PermissionBehavior,
|
|
PermissionRule,
|
|
PermissionRuleSource,
|
|
PermissionRuleValue,
|
|
} from './PermissionRule.js'
|
|
import {
|
|
permissionRuleValueFromString,
|
|
permissionRuleValueToString,
|
|
} from './permissionRuleParser.js'
|
|
|
|
/**
|
|
* Returns true if allowManagedPermissionRulesOnly is enabled in managed settings (policySettings).
|
|
* When enabled, only permission rules from managed settings are respected.
|
|
*/
|
|
export function shouldAllowManagedPermissionRulesOnly(): boolean {
|
|
return (
|
|
getSettingsForSource('policySettings')?.allowManagedPermissionRulesOnly ===
|
|
true
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Returns true if "always allow" options should be shown in permission prompts.
|
|
* When allowManagedPermissionRulesOnly is enabled, these options are hidden.
|
|
*/
|
|
export function shouldShowAlwaysAllowOptions(): boolean {
|
|
return !shouldAllowManagedPermissionRulesOnly()
|
|
}
|
|
|
|
const SUPPORTED_RULE_BEHAVIORS = [
|
|
'allow',
|
|
'deny',
|
|
'ask',
|
|
] as const satisfies PermissionBehavior[]
|
|
|
|
/**
|
|
* Lenient version of getSettingsForSource that doesn't fail on ANY validation errors.
|
|
* Simply parses the JSON and returns it as-is without schema validation.
|
|
*
|
|
* Used when loading settings to append new rules (avoids losing existing rules
|
|
* due to validation failures in unrelated fields like hooks).
|
|
*
|
|
* FOR EDITING ONLY - do not use this for reading settings for execution.
|
|
*/
|
|
function getSettingsForSourceLenient_FOR_EDITING_ONLY_NOT_FOR_READING(
|
|
source: SettingSource,
|
|
): SettingsJson | null {
|
|
const filePath = getSettingsFilePathForSource(source)
|
|
if (!filePath) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const { resolvedPath } = safeResolvePath(getFsImplementation(), filePath)
|
|
const content = readFileSync(resolvedPath)
|
|
if (content.trim() === '') {
|
|
return {}
|
|
}
|
|
|
|
const data = safeParseJSON(content, false)
|
|
// Return raw parsed JSON without validation to preserve all existing settings
|
|
// This is safe because we're only using this for reading/appending, not for execution
|
|
return data && typeof data === 'object' ? (data as SettingsJson) : null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts permissions JSON to an array of PermissionRule objects
|
|
* @param data The parsed permissions data
|
|
* @param source The source of these rules
|
|
* @returns Array of PermissionRule objects
|
|
*/
|
|
function settingsJsonToRules(
|
|
data: SettingsJson | null,
|
|
source: PermissionRuleSource,
|
|
): PermissionRule[] {
|
|
if (!data || !data.permissions) {
|
|
return []
|
|
}
|
|
|
|
const { permissions } = data
|
|
const rules: PermissionRule[] = []
|
|
for (const behavior of SUPPORTED_RULE_BEHAVIORS) {
|
|
const behaviorArray = permissions[behavior]
|
|
if (behaviorArray) {
|
|
for (const ruleString of behaviorArray) {
|
|
rules.push({
|
|
source,
|
|
ruleBehavior: behavior,
|
|
ruleValue: permissionRuleValueFromString(ruleString),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return rules
|
|
}
|
|
|
|
/**
|
|
* Loads all permission rules from all relevant sources (managed and project settings)
|
|
* @returns Array of all permission rules
|
|
*/
|
|
export function loadAllPermissionRulesFromDisk(): PermissionRule[] {
|
|
// If allowManagedPermissionRulesOnly is set, only use managed permission rules
|
|
if (shouldAllowManagedPermissionRulesOnly()) {
|
|
return getPermissionRulesForSource('policySettings')
|
|
}
|
|
|
|
// Otherwise, load from all enabled sources (backwards compatible)
|
|
const rules: PermissionRule[] = []
|
|
|
|
for (const source of getEnabledSettingSources()) {
|
|
rules.push(...getPermissionRulesForSource(source))
|
|
}
|
|
return rules
|
|
}
|
|
|
|
/**
|
|
* Loads permission rules from a specific source
|
|
* @param source The source to load from
|
|
* @returns Array of permission rules from that source
|
|
*/
|
|
export function getPermissionRulesForSource(
|
|
source: SettingSource,
|
|
): PermissionRule[] {
|
|
const settingsData = getSettingsForSource(source)
|
|
return settingsJsonToRules(settingsData, source)
|
|
}
|
|
|
|
export type PermissionRuleFromEditableSettings = PermissionRule & {
|
|
source: EditableSettingSource
|
|
}
|
|
|
|
// Editable sources that can be modified (excludes policySettings and flagSettings)
|
|
const EDITABLE_SOURCES: EditableSettingSource[] = [
|
|
'userSettings',
|
|
'projectSettings',
|
|
'localSettings',
|
|
]
|
|
|
|
/**
|
|
* Deletes a rule from the project permissions file
|
|
* @param rule The rule to delete
|
|
* @returns Promise resolving to a boolean indicating success
|
|
*/
|
|
export function deletePermissionRuleFromSettings(
|
|
rule: PermissionRuleFromEditableSettings,
|
|
): boolean {
|
|
// Runtime check to ensure source is actually editable
|
|
if (!EDITABLE_SOURCES.includes(rule.source as EditableSettingSource)) {
|
|
return false
|
|
}
|
|
|
|
const ruleString = permissionRuleValueToString(rule.ruleValue)
|
|
const settingsData = getSettingsForSource(rule.source)
|
|
|
|
// If there's no settings data or permissions, nothing to do
|
|
if (!settingsData || !settingsData.permissions) {
|
|
return false
|
|
}
|
|
|
|
const behaviorArray = settingsData.permissions[rule.ruleBehavior]
|
|
if (!behaviorArray) {
|
|
return false
|
|
}
|
|
|
|
// Normalize raw settings entries via roundtrip parse→serialize so legacy
|
|
// names (e.g. "KillShell") match their canonical form ("TaskStop").
|
|
const normalizeEntry = (raw: string): string =>
|
|
permissionRuleValueToString(permissionRuleValueFromString(raw))
|
|
|
|
if (!behaviorArray.some(raw => normalizeEntry(raw) === ruleString)) {
|
|
return false
|
|
}
|
|
|
|
try {
|
|
// Keep a copy of the original permissions data to preserve unrecognized keys
|
|
const updatedSettingsData = {
|
|
...settingsData,
|
|
permissions: {
|
|
...settingsData.permissions,
|
|
[rule.ruleBehavior]: behaviorArray.filter(
|
|
raw => normalizeEntry(raw) !== ruleString,
|
|
),
|
|
},
|
|
}
|
|
|
|
const { error } = updateSettingsForSource(rule.source, updatedSettingsData)
|
|
if (error) {
|
|
// Error already logged inside updateSettingsForSource
|
|
return false
|
|
}
|
|
|
|
return true
|
|
} catch (error) {
|
|
logError(error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
function getEmptyPermissionSettingsJson(): SettingsJson {
|
|
return {
|
|
permissions: {},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds rules to the project permissions file
|
|
* @param ruleValues The rule values to add
|
|
* @returns Promise resolving to a boolean indicating success
|
|
*/
|
|
export function addPermissionRulesToSettings(
|
|
{
|
|
ruleValues,
|
|
ruleBehavior,
|
|
}: {
|
|
ruleValues: PermissionRuleValue[]
|
|
ruleBehavior: PermissionBehavior
|
|
},
|
|
source: EditableSettingSource,
|
|
): boolean {
|
|
// When allowManagedPermissionRulesOnly is enabled, don't persist new permission rules
|
|
if (shouldAllowManagedPermissionRulesOnly()) {
|
|
return false
|
|
}
|
|
|
|
if (ruleValues.length < 1) {
|
|
// No rules to add
|
|
return true
|
|
}
|
|
|
|
const ruleStrings = ruleValues.map(permissionRuleValueToString)
|
|
// First try the normal settings loader which validates the schema
|
|
// If validation fails, fall back to lenient loading to preserve existing rules
|
|
// even if some fields (like hooks) have validation errors
|
|
const settingsData =
|
|
getSettingsForSource(source) ||
|
|
getSettingsForSourceLenient_FOR_EDITING_ONLY_NOT_FOR_READING(source) ||
|
|
getEmptyPermissionSettingsJson()
|
|
|
|
try {
|
|
// Ensure permissions object exists
|
|
const existingPermissions = settingsData.permissions || {}
|
|
const existingRules = existingPermissions[ruleBehavior] || []
|
|
|
|
// Filter out duplicates - normalize existing entries via roundtrip
|
|
// parse→serialize so legacy names match their canonical form.
|
|
const existingRulesSet = new Set(
|
|
existingRules.map(raw =>
|
|
permissionRuleValueToString(permissionRuleValueFromString(raw)),
|
|
),
|
|
)
|
|
const newRules = ruleStrings.filter(rule => !existingRulesSet.has(rule))
|
|
|
|
// If no new rules to add, return success
|
|
if (newRules.length === 0) {
|
|
return true
|
|
}
|
|
|
|
// Keep a copy of the original settings data to preserve unrecognized keys
|
|
const updatedSettingsData = {
|
|
...settingsData,
|
|
permissions: {
|
|
...existingPermissions,
|
|
[ruleBehavior]: [...existingRules, ...newRules],
|
|
},
|
|
}
|
|
const result = updateSettingsForSource(source, updatedSettingsData)
|
|
|
|
if (result.error) {
|
|
throw result.error
|
|
}
|
|
|
|
return true
|
|
} catch (error) {
|
|
logError(error)
|
|
return false
|
|
}
|
|
}
|