mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
401 lines
15 KiB
C++
401 lines
15 KiB
C++
// Native DACL IO avoids icacls.exe’s multi-second main-thread stalls (#26027) and EDR-scored
|
||
// /reset + /inheritance:r pattern (#25583). JS owns SID/flag policy; native reads/writes structure.
|
||
|
||
#include <napi.h>
|
||
#include <windows.h>
|
||
#include <aclapi.h>
|
||
#include <sddl.h>
|
||
|
||
#include <functional>
|
||
#include <memory>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
namespace {
|
||
|
||
struct Win32Result {
|
||
DWORD code = ERROR_SUCCESS;
|
||
const char* syscall = "";
|
||
bool ok() const { return code == ERROR_SUCCESS; }
|
||
};
|
||
|
||
Win32Result Fail(const char* syscall, DWORD code) { return Win32Result{code, syscall}; }
|
||
|
||
struct LocalDeleter {
|
||
void operator()(void* pointer) const { LocalFree(pointer); }
|
||
};
|
||
using LocalPtr = std::unique_ptr<void, LocalDeleter>;
|
||
|
||
Napi::Error ToError(Napi::Env env, const Win32Result& result) {
|
||
return Napi::Error::New(
|
||
env, std::string(result.syscall) + " failed with Win32 error " + std::to_string(result.code));
|
||
}
|
||
|
||
Win32Result ParseSddl(const std::u16string& sddl, LocalPtr* descriptor) {
|
||
PSECURITY_DESCRIPTOR raw = nullptr;
|
||
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||
reinterpret_cast<LPCWSTR>(sddl.c_str()), SDDL_REVISION_1, &raw, nullptr)) {
|
||
return Fail("ConvertStringSecurityDescriptorToSecurityDescriptorW", GetLastError());
|
||
}
|
||
descriptor->reset(raw);
|
||
return {};
|
||
}
|
||
|
||
// The descriptor's own protection bit decides: a replacement DACL carries the caller's choice, and
|
||
// a grant re-reads the object's, so adding one rule never flips inheritance on or off.
|
||
Win32Result DaclInformationFor(PSECURITY_DESCRIPTOR descriptor, SECURITY_INFORMATION* information) {
|
||
SECURITY_DESCRIPTOR_CONTROL control = 0;
|
||
DWORD revision = 0;
|
||
if (!GetSecurityDescriptorControl(descriptor, &control, &revision)) {
|
||
return Fail("GetSecurityDescriptorControl", GetLastError());
|
||
}
|
||
*information = DACL_SECURITY_INFORMATION | ((control & SE_DACL_PROTECTED)
|
||
? PROTECTED_DACL_SECURITY_INFORMATION
|
||
: UNPROTECTED_DACL_SECURITY_INFORMATION);
|
||
return {};
|
||
}
|
||
|
||
Win32Result ApplyDacl(std::u16string path, const std::u16string& sddl) {
|
||
LocalPtr descriptor;
|
||
auto parsed = ParseSddl(sddl, &descriptor);
|
||
if (!parsed.ok()) {
|
||
return parsed;
|
||
}
|
||
BOOL present = FALSE;
|
||
BOOL defaulted = FALSE;
|
||
PACL dacl = nullptr;
|
||
if (!GetSecurityDescriptorDacl(descriptor.get(), &present, &dacl, &defaulted)) {
|
||
return Fail("GetSecurityDescriptorDacl", GetLastError());
|
||
}
|
||
// A missing DACL means "everyone, full control"; never install that by accident.
|
||
if (!present || dacl == nullptr) {
|
||
return Fail("GetSecurityDescriptorDacl", ERROR_INVALID_SECURITY_DESCR);
|
||
}
|
||
SECURITY_INFORMATION information = 0;
|
||
auto control = DaclInformationFor(descriptor.get(), &information);
|
||
if (!control.ok()) {
|
||
return control;
|
||
}
|
||
DWORD status = SetNamedSecurityInfoW(reinterpret_cast<LPWSTR>(path.data()), SE_FILE_OBJECT,
|
||
information, nullptr, nullptr, dacl, nullptr);
|
||
return status == ERROR_SUCCESS ? Win32Result{} : Fail("SetNamedSecurityInfoW", status);
|
||
}
|
||
|
||
Win32Result GrantAccess(std::u16string path, const std::u16string& sid, DWORD mask,
|
||
bool inheritable, bool replace) {
|
||
PSECURITY_DESCRIPTOR rawDescriptor = nullptr;
|
||
PACL existing = nullptr;
|
||
DWORD status = GetNamedSecurityInfoW(reinterpret_cast<LPCWSTR>(path.c_str()), SE_FILE_OBJECT,
|
||
DACL_SECURITY_INFORMATION, nullptr, nullptr, &existing,
|
||
nullptr, &rawDescriptor);
|
||
if (status != ERROR_SUCCESS) {
|
||
return Fail("GetNamedSecurityInfoW", status);
|
||
}
|
||
LocalPtr descriptor(rawDescriptor);
|
||
// A NULL DACL already grants everyone everything; merging into it would build a DACL holding
|
||
// only this rule and lock every other principal out.
|
||
if (existing == nullptr) {
|
||
return {};
|
||
}
|
||
PSID rawSid = nullptr;
|
||
if (!ConvertStringSidToSidW(reinterpret_cast<LPCWSTR>(sid.c_str()), &rawSid)) {
|
||
return Fail("ConvertStringSidToSidW", GetLastError());
|
||
}
|
||
LocalPtr trusteeSid(rawSid);
|
||
|
||
EXPLICIT_ACCESSW access{};
|
||
access.grfAccessPermissions = mask;
|
||
access.grfAccessMode = replace ? SET_ACCESS : GRANT_ACCESS;
|
||
access.grfInheritance = inheritable ? SUB_CONTAINERS_AND_OBJECTS_INHERIT : NO_INHERITANCE;
|
||
access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
|
||
access.Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN;
|
||
access.Trustee.ptstrName = reinterpret_cast<LPWSTR>(rawSid);
|
||
|
||
PACL merged = nullptr;
|
||
status = SetEntriesInAclW(1, &access, existing, &merged);
|
||
if (status != ERROR_SUCCESS) {
|
||
return Fail("SetEntriesInAclW", status);
|
||
}
|
||
LocalPtr mergedAcl(merged);
|
||
SECURITY_INFORMATION information = 0;
|
||
auto control = DaclInformationFor(descriptor.get(), &information);
|
||
if (!control.ok()) {
|
||
return control;
|
||
}
|
||
status = SetNamedSecurityInfoW(reinterpret_cast<LPWSTR>(path.data()), SE_FILE_OBJECT,
|
||
information, nullptr, nullptr, merged, nullptr);
|
||
return status == ERROR_SUCCESS ? Win32Result{} : Fail("SetNamedSecurityInfoW", status);
|
||
}
|
||
|
||
Win32Result CreateFileWithDacl(const std::u16string& path, const std::u16string& sddl,
|
||
const uint8_t* data, size_t length) {
|
||
LocalPtr descriptor;
|
||
auto parsed = ParseSddl(sddl, &descriptor);
|
||
if (!parsed.ok()) {
|
||
return parsed;
|
||
}
|
||
if (length > MAXDWORD) {
|
||
return Fail("WriteFile", ERROR_INVALID_PARAMETER);
|
||
}
|
||
SECURITY_ATTRIBUTES attributes{sizeof(attributes), descriptor.get(), FALSE};
|
||
// CREATE_NEW ensures the descriptor applies (existing files retain their DACL); share mode 0 excludes readers until complete.
|
||
HANDLE file = CreateFileW(reinterpret_cast<LPCWSTR>(path.c_str()), GENERIC_WRITE, 0,
|
||
&attributes, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||
if (file == INVALID_HANDLE_VALUE) {
|
||
return Fail("CreateFileW", GetLastError());
|
||
}
|
||
Win32Result result;
|
||
DWORD written = 0;
|
||
if (!WriteFile(file, data, static_cast<DWORD>(length), &written, nullptr)) {
|
||
result = Fail("WriteFile", GetLastError());
|
||
} else if (written != length) {
|
||
result = Fail("WriteFile", ERROR_WRITE_FAULT);
|
||
}
|
||
// A failed close can mean lost data on a network volume, so it tears the file like a failed write.
|
||
if (!CloseHandle(file) && result.ok()) {
|
||
result = Fail("CloseHandle", GetLastError());
|
||
}
|
||
if (!result.ok()) {
|
||
// A torn temp file is useless to the caller and must not linger beside the real one.
|
||
DeleteFileW(reinterpret_cast<LPCWSTR>(path.c_str()));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
bool ReadGrantOptions(const Napi::Value& value, bool* inheritable, bool* replace) {
|
||
if (!value.IsObject()) {
|
||
return false;
|
||
}
|
||
auto options = value.As<Napi::Object>();
|
||
auto inherit = options.Get("inheritable");
|
||
auto replaceValue = options.Get("replace");
|
||
if (!inherit.IsBoolean() || !replaceValue.IsBoolean()) {
|
||
return false;
|
||
}
|
||
*inheritable = inherit.As<Napi::Boolean>().Value();
|
||
*replace = replaceValue.As<Napi::Boolean>().Value();
|
||
return true;
|
||
}
|
||
|
||
Napi::Value ThrowTypeError(Napi::Env env, const char* signature) {
|
||
Napi::TypeError::New(env, signature).ThrowAsJavaScriptException();
|
||
return env.Undefined();
|
||
}
|
||
|
||
Napi::Value SidString(Napi::Env env, PSID sid) {
|
||
LPWSTR text = nullptr;
|
||
if (!ConvertSidToStringSidW(sid, &text)) {
|
||
ToError(env, Fail("ConvertSidToStringSidW", GetLastError())).ThrowAsJavaScriptException();
|
||
return env.Undefined();
|
||
}
|
||
LocalPtr owned(text);
|
||
return Napi::String::New(env, reinterpret_cast<char16_t*>(text));
|
||
}
|
||
|
||
Napi::Value CurrentUserSid(const Napi::CallbackInfo& info) {
|
||
auto env = info.Env();
|
||
HANDLE token = nullptr;
|
||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
|
||
ToError(env, Fail("OpenProcessToken", GetLastError())).ThrowAsJavaScriptException();
|
||
return env.Undefined();
|
||
}
|
||
DWORD size = 0;
|
||
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
|
||
std::vector<BYTE> buffer(size);
|
||
BOOL queried = size > 0 && GetTokenInformation(token, TokenUser, buffer.data(), size, &size);
|
||
DWORD queryError = GetLastError();
|
||
CloseHandle(token);
|
||
if (!queried) {
|
||
ToError(env, Fail("GetTokenInformation", queryError)).ThrowAsJavaScriptException();
|
||
return env.Undefined();
|
||
}
|
||
return SidString(env, reinterpret_cast<TOKEN_USER*>(buffer.data())->User.Sid);
|
||
}
|
||
|
||
// Return structure only; null means NULL DACL. Only plain allow/deny ACEs expose SIDs;
|
||
// unknown/object/callback/conditional types retain sid null so JS can refuse them.
|
||
Napi::Value GetDaclAces(const Napi::CallbackInfo& info) {
|
||
auto env = info.Env();
|
||
if (info.Length() < 1 || !info[0].IsString()) {
|
||
return ThrowTypeError(env, "getDaclAces(path: string)");
|
||
}
|
||
auto path = info[0].As<Napi::String>().Utf16Value();
|
||
PSECURITY_DESCRIPTOR rawDescriptor = nullptr;
|
||
PACL dacl = nullptr;
|
||
DWORD status = GetNamedSecurityInfoW(reinterpret_cast<LPCWSTR>(path.c_str()), SE_FILE_OBJECT,
|
||
DACL_SECURITY_INFORMATION, nullptr, nullptr, &dacl,
|
||
nullptr, &rawDescriptor);
|
||
if (status != ERROR_SUCCESS) {
|
||
ToError(env, Fail("GetNamedSecurityInfoW", status)).ThrowAsJavaScriptException();
|
||
return env.Undefined();
|
||
}
|
||
LocalPtr descriptor(rawDescriptor);
|
||
if (dacl == nullptr) {
|
||
return env.Null();
|
||
}
|
||
SECURITY_DESCRIPTOR_CONTROL control = 0;
|
||
DWORD revision = 0;
|
||
if (!GetSecurityDescriptorControl(rawDescriptor, &control, &revision)) {
|
||
ToError(env, Fail("GetSecurityDescriptorControl", GetLastError())).ThrowAsJavaScriptException();
|
||
return env.Undefined();
|
||
}
|
||
auto aces = Napi::Array::New(env, dacl->AceCount);
|
||
for (DWORD index = 0; index < dacl->AceCount; index++) {
|
||
void* raw = nullptr;
|
||
if (!GetAce(dacl, index, &raw)) {
|
||
ToError(env, Fail("GetAce", GetLastError())).ThrowAsJavaScriptException();
|
||
return env.Undefined();
|
||
}
|
||
auto* header = static_cast<ACE_HEADER*>(raw);
|
||
auto ace = Napi::Object::New(env);
|
||
ace.Set("type", Napi::Number::New(env, header->AceType));
|
||
ace.Set("flags", Napi::Number::New(env, header->AceFlags));
|
||
// ACCESS_ALLOWED_ACE and ACCESS_DENIED_ACE share this layout: Mask, then SidStart.
|
||
if (header->AceType == ACCESS_ALLOWED_ACE_TYPE || header->AceType == ACCESS_DENIED_ACE_TYPE) {
|
||
auto* plain = static_cast<ACCESS_ALLOWED_ACE*>(raw);
|
||
ace.Set("mask", Napi::Number::New(env, plain->Mask));
|
||
auto sid = SidString(env, &plain->SidStart);
|
||
if (env.IsExceptionPending()) {
|
||
return env.Undefined();
|
||
}
|
||
ace.Set("sid", sid);
|
||
} else {
|
||
ace.Set("mask", Napi::Number::New(env, 0));
|
||
ace.Set("sid", env.Null());
|
||
}
|
||
aces.Set(index, ace);
|
||
}
|
||
auto result = Napi::Object::New(env);
|
||
result.Set("protected", Napi::Boolean::New(env, (control & SE_DACL_PROTECTED) != 0));
|
||
result.Set("aces", aces);
|
||
return result;
|
||
}
|
||
|
||
Napi::Value SetDaclSddl(const Napi::CallbackInfo& info) {
|
||
auto env = info.Env();
|
||
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
||
return ThrowTypeError(env, "setDaclSddl(path: string, sddl: string)");
|
||
}
|
||
auto result = ApplyDacl(info[0].As<Napi::String>().Utf16Value(),
|
||
info[1].As<Napi::String>().Utf16Value());
|
||
if (!result.ok()) {
|
||
ToError(env, result).ThrowAsJavaScriptException();
|
||
}
|
||
return env.Undefined();
|
||
}
|
||
|
||
Napi::Value CreateFileWithDaclJs(const Napi::CallbackInfo& info) {
|
||
auto env = info.Env();
|
||
if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() ||
|
||
!info[2].IsTypedArray()) {
|
||
return ThrowTypeError(env, "createFileWithDacl(path: string, sddl: string, data: Uint8Array)");
|
||
}
|
||
auto data = info[2].As<Napi::Uint8Array>();
|
||
auto result = CreateFileWithDacl(info[0].As<Napi::String>().Utf16Value(),
|
||
info[1].As<Napi::String>().Utf16Value(), data.Data(),
|
||
data.ByteLength());
|
||
if (!result.ok()) {
|
||
ToError(env, result).ThrowAsJavaScriptException();
|
||
}
|
||
return env.Undefined();
|
||
}
|
||
|
||
bool ReadGrantArguments(const Napi::CallbackInfo& info, std::u16string* path, std::u16string* sid,
|
||
DWORD* mask, bool* inheritable, bool* replace) {
|
||
if (info.Length() < 4 || !info[0].IsString() || !info[1].IsString() || !info[2].IsNumber() ||
|
||
!ReadGrantOptions(info[3], inheritable, replace)) {
|
||
return false;
|
||
}
|
||
*path = info[0].As<Napi::String>().Utf16Value();
|
||
*sid = info[1].As<Napi::String>().Utf16Value();
|
||
*mask = info[2].As<Napi::Number>().Uint32Value();
|
||
return true;
|
||
}
|
||
|
||
constexpr const char* kGrantSignature =
|
||
"grantAccess(path: string, sid: string, accessMask: number, "
|
||
"options: { inheritable: boolean, replace: boolean })";
|
||
|
||
Napi::Value GrantAccessJs(const Napi::CallbackInfo& info) {
|
||
auto env = info.Env();
|
||
std::u16string path, sid;
|
||
DWORD mask = 0;
|
||
bool inheritable = false, replace = false;
|
||
if (!ReadGrantArguments(info, &path, &sid, &mask, &inheritable, &replace)) {
|
||
return ThrowTypeError(env, kGrantSignature);
|
||
}
|
||
auto result = GrantAccess(path, sid, mask, inheritable, replace);
|
||
if (!result.ok()) {
|
||
ToError(env, result).ThrowAsJavaScriptException();
|
||
}
|
||
return env.Undefined();
|
||
}
|
||
|
||
// Why a worker: a directory DACL change walks every descendant (seconds on a large userData).
|
||
class DaclWorker : public Napi::AsyncWorker {
|
||
public:
|
||
DaclWorker(Napi::Env env, std::function<Win32Result()> work)
|
||
: Napi::AsyncWorker(env), deferred_(Napi::Promise::Deferred::New(env)),
|
||
work_(std::move(work)) {}
|
||
|
||
Napi::Promise Promise() { return deferred_.Promise(); }
|
||
|
||
void Execute() override { result_ = work_(); }
|
||
void OnOK() override {
|
||
if (result_.ok()) {
|
||
deferred_.Resolve(Env().Undefined());
|
||
} else {
|
||
deferred_.Reject(ToError(Env(), result_).Value());
|
||
}
|
||
}
|
||
|
||
private:
|
||
Napi::Promise::Deferred deferred_;
|
||
std::function<Win32Result()> work_;
|
||
Win32Result result_;
|
||
};
|
||
|
||
Napi::Value SetDaclSddlAsync(const Napi::CallbackInfo& info) {
|
||
auto env = info.Env();
|
||
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
||
return ThrowTypeError(env, "setDaclSddlAsync(path: string, sddl: string)");
|
||
}
|
||
auto path = info[0].As<Napi::String>().Utf16Value();
|
||
auto sddl = info[1].As<Napi::String>().Utf16Value();
|
||
auto* worker = new DaclWorker(env, [path, sddl]() { return ApplyDacl(path, sddl); });
|
||
auto promise = worker->Promise();
|
||
worker->Queue();
|
||
return promise;
|
||
}
|
||
|
||
Napi::Value GrantAccessAsync(const Napi::CallbackInfo& info) {
|
||
auto env = info.Env();
|
||
std::u16string path, sid;
|
||
DWORD mask = 0;
|
||
bool inheritable = false, replace = false;
|
||
if (!ReadGrantArguments(info, &path, &sid, &mask, &inheritable, &replace)) {
|
||
return ThrowTypeError(env, kGrantSignature);
|
||
}
|
||
auto* worker = new DaclWorker(env, [=]() {
|
||
return GrantAccess(path, sid, mask, inheritable, replace);
|
||
});
|
||
auto promise = worker->Promise();
|
||
worker->Queue();
|
||
return promise;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
||
exports.Set("currentUserSid", Napi::Function::New(env, CurrentUserSid));
|
||
exports.Set("getDaclAces", Napi::Function::New(env, GetDaclAces));
|
||
exports.Set("setDaclSddl", Napi::Function::New(env, SetDaclSddl));
|
||
exports.Set("setDaclSddlAsync", Napi::Function::New(env, SetDaclSddlAsync));
|
||
exports.Set("createFileWithDacl", Napi::Function::New(env, CreateFileWithDaclJs));
|
||
exports.Set("grantAccess", Napi::Function::New(env, GrantAccessJs));
|
||
exports.Set("grantAccessAsync", Napi::Function::New(env, GrantAccessAsync));
|
||
return exports;
|
||
}
|
||
|
||
NODE_API_MODULE(orca_windows_security, Init)
|