Skip to content
Source
Sign inSign up
Open History to see the last commit

ManagedPolicy.ts

219 lines7.9 KB

1/**
2 * ManagedPolicy - the administrator's machine-wide policy file, read once at startup. A file this
3 * build cannot fully enforce stops the server instead of leaving it looking managed.
4 *
5 * @module ManagedPolicy
6 */
7import * as NodeModule from "node:module";
8​
9import { ManagedPolicyDocument, type ManagedPolicySummary } from "@t3tools/contracts";
10import * as Config from "effect/Config";
11import * as Context from "effect/Context";
12import * as Digest from "effect/crypto/Digest";
13import * as Effect from "effect/Effect";
14import * as Hex from "effect/encoding/Hex";
15import * as Layer from "effect/Layer";
16import * as Option from "effect/Option";
17import * as Schema from "effect/Schema";
18​
19import * as HostProcess from "@t3tools/shared/HostProcess";
20import {
21 ManagedPolicyFileError,
22 managedPolicyPath,
23 layerWindowsRegistry,
24 readManagedPolicy,
25 readManagedPolicyFile,
26} from "@t3tools/shared/managedPolicyFile";
27​
28import { telemetryChannel } from "../config.ts";
29import * as ManagedLimits from "./managedLimits.ts";
30import * as ManagedProviders from "./managedProviders.ts";
31import * as ManagedSettings from "./managedSettings.ts";
32​
33export class ManagedPolicy extends Context.Service<
34 ManagedPolicy,
35 {
36 /** Where the policy is read from, whether or not a file exists there. */
37 readonly path: string;
38 readonly document: ManagedPolicyDocument | undefined;
39 readonly settings: ManagedSettings.ManagedSettings;
40 readonly providers: ManagedProviders.ManagedProviders;
41 /** What clients are told; absent when no policy applies. */
42 readonly summary: ManagedPolicySummary | undefined;
43 }
44>()("t3/policy/ManagedPolicy") {}
45​
46type LimitsSummary = NonNullable<ManagedPolicySummary["limits"]>;
47​
48const encodeDocumentJson = Schema.encodeEffect(Schema.fromJsonString(ManagedPolicyDocument));
49​
50/** What `t3 policy show` compares to tell whether a running server loaded the installed policy. */
51export const loadedPolicy = Effect.fn("ManagedPolicy.loadedPolicy")(function* (
52 policy: Pick<ManagedPolicy["Service"], "path" | "document">,
53) {
54 if (policy.document === undefined) return null;
55 const json = yield* encodeDocumentJson(policy.document).pipe(Effect.orDie);
56 const digest = yield* (yield* Digest.Digest).digest(
57 Digest.sha256,
58 new TextEncoder().encode(json),
59 );
60 return {
61 source: policy.path,
62 organization: policy.document.organization,
63 fingerprint: Hex.encode(digest).slice(0, 12),
64 };
65});
66​
67/**
68 * The organization whose policy switches `capability` off, or undefined when it is allowed.
69 * Only an explicit `false` restricts.
70 */
71export function deniedBy(
72 policy: Pick<ManagedPolicy["Service"], "document">,
73 capability: "webhookTasks" | "telemetry",
74): string | undefined {
75 return policy.document?.limits?.[capability] === false ? policy.document.organization : undefined;
77​
78/** Build the service from a decoded document; fails when the document cannot be enforced. */
79export const fromDocument = Effect.fn("ManagedPolicy.fromDocument")(function* (
80 path: string,
81 document: ManagedPolicyDocument | undefined,
82) {
83 if (document === undefined) {
84 return ManagedPolicy.of({
85 path,
86 document: undefined,
87 settings: ManagedSettings.unmanagedSettings,
88 providers: ManagedProviders.unmanagedProviders,
89 summary: undefined,
90 });
91 }
92 const maxRuntimeMode = document.limits?.maxRuntimeMode;
93 const settings = ManagedSettings.makeManagedSettings(document.settings ?? {}, maxRuntimeMode);
94 if (settings._tag === "Invalid") {
95 return yield* new ManagedPolicyFileError({ path, reason: settings.reason });
96 }
97 // A ceiling alone also shapes providers: their launch configuration becomes the administrator's.
98 const providerLimits = document.limits?.providers;
99 const providers =
100 providerLimits === undefined && maxRuntimeMode === undefined
101 ? undefined
102 : ManagedProviders.makeManagedProviders(
103 providerLimits ?? {},
104 maxRuntimeMode,
105 yield* HostProcess.Platform,
106 );
107 if (providers?._tag === "Invalid") {
108 return yield* new ManagedPolicyFileError({ path, reason: providers.reason });
109 }
110 const lockedInstanceFields = providers?.providers.lockedInstanceFields ?? {};
111 return ManagedPolicy.of({
112 path,
113 document,
114 settings: settings.settings,
115 providers: providers?.providers ?? ManagedProviders.unmanagedProviders,
116 summary: {
117 organization: document.organization,
118 ...(document.supportUrl === undefined ? {} : { supportUrl: document.supportUrl }),
119 lockedSettings: settings.settings.lockedPaths,
120 ...(document.limits === undefined
121 ? {}
122 : { limits: summarizeLimits(document, lockedInstanceFields) }),
123 },
124 });
125});
126​
127// Copies only what clients may see; other limits can carry values that stay on the server.
128function summarizeLimits(
129 document: ManagedPolicyDocument,
130 lockedInstanceFields: Readonly<Record<string, ReadonlyArray<string>>>,
131): LimitsSummary {
132 const {
133 maxRuntimeMode,
134 clients,
135 webhookTasks,
136 telemetry,
137 browserCookieImport,
138 providers,
139 updates,
140 } = document.limits ?? {};
141 const connect = ManagedLimits.allowsConnect({ document });
142 return {
143 ...(maxRuntimeMode === undefined ? {} : { maxRuntimeMode }),
144 ...(clients === undefined ? {} : { clients }),
145 ...(connect ? {} : { connect: false }),
146 ...(webhookTasks === undefined ? {} : { webhookTasks }),
147 ...(telemetry === undefined ? {} : { telemetry }),
148 ...(browserCookieImport === undefined ? {} : { browserCookieImport }),
149 ...(providers?.allowedDrivers === undefined && Object.keys(lockedInstanceFields).length === 0
150 ? {}
151 : {
152 providers: {
153 ...(providers?.allowedDrivers === undefined
154 ? {}
155 : { allowedDrivers: providers.allowedDrivers }),
156 ...(Object.keys(lockedInstanceFields).length === 0 ? {} : { lockedInstanceFields }),
157 },
158 }),
159 ...(updates === undefined
160 ? {}
161 : {
162 updates: {
163 mode: updates.mode,
164 ...(updates.allowNightly === undefined ? {} : { allowNightly: updates.allowNightly }),
165 },
166 }),
167 };
169​
170/** Read and validate the policy at `path`; a missing file means no policy. */
171export const load = Effect.fn("ManagedPolicy.load")(function* (path: string) {
172 return yield* fromDocument(path, yield* readManagedPolicyFile(path));
173});
174​
175const make = Effect.gen(function* () {
176 // Source builds may read a policy from elsewhere so it can be tried without root. Release
177 // builds never do: any override a user can set is a way around the policy.
178 const devPath =
179 telemetryChannel === "dev"
180 ? yield* Config.option(Config.String("T3CODE_DEV_MANAGED_POLICY_PATH")).pipe(
181 Effect.orElseSucceed(() => Option.none<string>()),
182 )
183 : Option.none<string>();
184 const platform = yield* HostProcess.Platform;
185 const policy = yield* Option.match(devPath, {
186 onSome: load,
187 onNone: () =>
188 readManagedPolicy(
189 platform,
190 ManagedProviders.knownDriverLabels().map(({ driver }) => driver),
191 ).pipe(
192 Effect.flatMap((found) =>
193 fromDocument(found?.source ?? managedPolicyPath(platform), found?.document),
194 ),
195 ),
196 });
197 const path = policy.path;
198 if (policy.summary !== undefined) {
199 yield* Effect.logInfo("Managed policy applies.", {
200 path,
201 organization: policy.summary.organization,
202 lockedSettings: policy.summary.lockedSettings,
203 });
204 }
205 return policy;
206});
207​
208// A single-executable server resolves packages on disk only through `require`.
209const requireForFfi = NodeModule.createRequire(import.meta.url);
210​
211export const layer = Layer.effect(ManagedPolicy, make).pipe(
212 Layer.provide(
213 layerWindowsRegistry(async () => requireForFfi("ffi-rs") as typeof import("ffi-rs")),
214 ),
215);
216​
217/** A policy for tests; `undefined` means unmanaged. */
218export const layerTest = (document?: ManagedPolicyDocument) =>
219 Layer.effect(ManagedPolicy, fromDocument("/test/policy.json", document));