@nuvlore/extension-workday-infra-access 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +3 -0
- package/agents/workday-infra-access-readonly.yaml +183 -0
- package/package.json +50 -0
- package/skills/workday-infra-access/SKILL.md +102 -0
- package/skills/workday-infra-access/references/access-boundary-runbook.md +43 -0
- package/skills/workday-infra-access/references/access-runbook.md +15 -0
- package/skills/workday-infra-access/references/access-scylla-cloud-runbook.md +39 -0
- package/skills/workday-infra-access/references/access-spc-argo-runbook.md +99 -0
- package/src/workday-access-profiles.mjs +194 -0
- package/tools/aws_cust_federated_readonly_command_pack.mjs +234 -0
- package/tools/gcp_readonly_command_pack.mjs +167 -0
- package/tools/public_cloud_access_status.mjs +48 -0
- package/tools/workday_boundary_login.mjs +460 -0
- package/tools/workday_private_cloud_sudo_readonly_command.mjs +386 -0
- package/tools/workday_public_cloud_environment_map.mjs +189 -0
- package/tools/workday_public_cloud_kubernetes_read.mjs +297 -0
- package/tools/workday_public_cloud_spc_login.mjs +505 -0
- package/tools/workday_spc_argo_readonly_command_pack.mjs +215 -0
- package/vendor/extension-read-only-toolkit/LICENSE +202 -0
- package/vendor/extension-read-only-toolkit/README.md +29 -0
- package/vendor/extension-read-only-toolkit/package.json +46 -0
- package/vendor/extension-read-only-toolkit/src/devops-data-paths.mjs +222 -0
- package/vendor/extension-read-only-toolkit/src/devops-diagnostics.mjs +440 -0
- package/vendor/extension-read-only-toolkit/src/enterprise-api-read.mjs +180 -0
- package/vendor/extension-read-only-toolkit/src/read-only-command-pack.mjs +1 -0
- package/vendor/extension-read-only-toolkit/src/read-only-plan.mjs +45 -0
- package/vendor/extension-read-only-toolkit/src/read-only-shell-policy.d.mts +26 -0
- package/vendor/extension-read-only-toolkit/src/read-only-shell-policy.mjs +170 -0
- package/workflows/public-cloud-readonly-investigation.js +99 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const description = "Return local public-cloud connection readiness status; does not authenticate or call cloud APIs.";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
5
|
+
|
|
6
|
+
function normalizeText(value) {
|
|
7
|
+
return typeof value === "string" ? value.trim() : "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class PublicCloudConnectPlanService {
|
|
11
|
+
invoke(input) {
|
|
12
|
+
const platform = normalizeText(input.platform) || "unknown";
|
|
13
|
+
return JSON.stringify({
|
|
14
|
+
mode: "atomic-helper",
|
|
15
|
+
platform,
|
|
16
|
+
environment: normalizeText(input.environment),
|
|
17
|
+
target: normalizeText(input.target),
|
|
18
|
+
status: "connection_procedure_lives_in_skill",
|
|
19
|
+
nextAtomicTool: platform === "aws" ? "aws_cust_federated_readonly_command_pack" : platform === "gcp" ? "gcp_readonly_command_pack" : "",
|
|
20
|
+
remoteWrite: "not_executed"
|
|
21
|
+
}, null, 2);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const defaultService = new PublicCloudConnectPlanService();
|
|
26
|
+
|
|
27
|
+
function publicCloudConnectPlan(input) {
|
|
28
|
+
return defaultService.invoke(input);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default defineReadOnlyTool({
|
|
32
|
+
name: "public_cloud_access_status",
|
|
33
|
+
description: "Return local public-cloud connection readiness status; does not authenticate or call cloud APIs.",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
platform: z.string().trim().max(128).default(""),
|
|
36
|
+
environment: z.string().trim().max(256).default(""),
|
|
37
|
+
target: z.string().trim().max(1024).default("")
|
|
38
|
+
},
|
|
39
|
+
async handler(input) {
|
|
40
|
+
try {
|
|
41
|
+
return ok(publicCloudConnectPlan(input));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return fail(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const __test = { PublicCloudConnectPlanService, publicCloudConnectPlan };
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
export const description = "Check or prepare Workday datacenter Boundary authentication using the saved macOS keychain LDAP password. The tool consumes the password in-place for bauth init and never returns the secret.";
|
|
2
|
+
|
|
3
|
+
import { execFile, spawn } from "node:child_process";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { defineReadOnlyTool, fail, ok } from "@nuvlore/extension-read-only-toolkit/devops-diagnostics";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_DC_BOUNDARY_ADDR,
|
|
10
|
+
DEFAULT_WORKDAY_LDAP_KEYCHAIN_SERVICE,
|
|
11
|
+
DEFAULT_WORKDAY_LDAP_SERVICE_KEY,
|
|
12
|
+
extractTokenFromKeychainSecret,
|
|
13
|
+
workdayProdCredentialCandidates
|
|
14
|
+
} from "../src/workday-access-profiles.mjs";
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
|
|
18
|
+
const DEFAULT_BAUTH_COMMAND = "/Users/boqiang.liang/bin/bauth";
|
|
19
|
+
const DEFAULT_BAUTH_ARGS = ["init"];
|
|
20
|
+
const DEFAULT_BOUNDARY_ADDR = DEFAULT_DC_BOUNDARY_ADDR;
|
|
21
|
+
const DEFAULT_BOUNDARY_BINARY = "/opt/homebrew/bin/boundary";
|
|
22
|
+
const DEFAULT_EXPECT_BINARY = "/usr/bin/expect";
|
|
23
|
+
const DEFAULT_KEYCHAIN_SERVICE = DEFAULT_WORKDAY_LDAP_KEYCHAIN_SERVICE;
|
|
24
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
25
|
+
const DEFAULT_CUST_VAULT_HOST = "sesh.vault.az1.cust.pdx.wd";
|
|
26
|
+
|
|
27
|
+
function normalizeText(value) {
|
|
28
|
+
return typeof value === "string" ? value.trim() : "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizePinnedText(value, expected, label) {
|
|
32
|
+
const normalized = normalizeText(value) || expected;
|
|
33
|
+
if (normalized !== expected) {
|
|
34
|
+
throw new Error(`${label} must be exactly ${expected}.`);
|
|
35
|
+
}
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizePinnedArgs(value) {
|
|
40
|
+
if (value === undefined) return DEFAULT_BAUTH_ARGS;
|
|
41
|
+
if (!Array.isArray(value) || value.length !== DEFAULT_BAUTH_ARGS.length || value.some((item, index) => String(item) !== DEFAULT_BAUTH_ARGS[index])) {
|
|
42
|
+
throw new Error(`args must be exactly ${JSON.stringify(DEFAULT_BAUTH_ARGS)}.`);
|
|
43
|
+
}
|
|
44
|
+
return DEFAULT_BAUTH_ARGS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assertStructuredHitlApproval(input, context) {
|
|
48
|
+
const approval = input.hitlApproval;
|
|
49
|
+
if (!approval || typeof approval !== "object") {
|
|
50
|
+
throw new Error(`${context} requires structured HITL approval with approved, approvedBy, action, target, reason, expectedImpact, and risk.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const missing = ["approvedBy", "action", "target", "reason", "expectedImpact", "risk"]
|
|
54
|
+
.filter((key) => !normalizeText(approval[key]));
|
|
55
|
+
if (approval.approved !== true || missing.length > 0) {
|
|
56
|
+
throw new Error(`${context} requires approved=true and complete HITL details: ${missing.join(", ") || "approved"}.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const extractPasswordFromKeychainSecret = extractTokenFromKeychainSecret;
|
|
61
|
+
|
|
62
|
+
async function readPasswordFromKeychain(execFileImpl) {
|
|
63
|
+
for (const candidate of workdayProdCredentialCandidates()) {
|
|
64
|
+
try {
|
|
65
|
+
const result = await execFileImpl("/usr/bin/security", [
|
|
66
|
+
"find-generic-password",
|
|
67
|
+
"-s",
|
|
68
|
+
candidate.service,
|
|
69
|
+
"-a",
|
|
70
|
+
candidate.account,
|
|
71
|
+
"-w"
|
|
72
|
+
], {
|
|
73
|
+
maxBuffer: 1024 * 1024,
|
|
74
|
+
timeout: 10_000
|
|
75
|
+
});
|
|
76
|
+
const password = extractPasswordFromKeychainSecret(result?.stdout);
|
|
77
|
+
if (password) return { password, keychainService: candidate.service, keychainAccount: candidate.account };
|
|
78
|
+
} catch (_error) {
|
|
79
|
+
// Try the legacy Workday PROD credential source next.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { password: "", keychainService: DEFAULT_KEYCHAIN_SERVICE, keychainAccount: "eldwin.workday_prod_ldap" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseBoundaryAuthStatus(stdout = "", stderr = "") {
|
|
86
|
+
const raw = normalizeText(stdout);
|
|
87
|
+
if (raw) {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
if (Number(parsed?.status_code) === 200 && parsed?.item?.expiration_time) {
|
|
91
|
+
return {
|
|
92
|
+
ready: true,
|
|
93
|
+
reason: "",
|
|
94
|
+
expirationTime: String(parsed.item.expiration_time)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
} catch (_error) {
|
|
98
|
+
// Fall through to a text reason.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
ready: false,
|
|
103
|
+
reason: normalizeText(stderr || raw || "Boundary auth token is unavailable."),
|
|
104
|
+
expirationTime: ""
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readBoundaryAuthStatus(execFileImpl, input) {
|
|
109
|
+
try {
|
|
110
|
+
const result = await execFileImpl(input.boundaryBinary, [
|
|
111
|
+
"auth-tokens",
|
|
112
|
+
"read",
|
|
113
|
+
"-id",
|
|
114
|
+
"self",
|
|
115
|
+
"-format",
|
|
116
|
+
"json"
|
|
117
|
+
], {
|
|
118
|
+
env: buildBoundaryEnv(input),
|
|
119
|
+
maxBuffer: 1024 * 1024,
|
|
120
|
+
timeout: 10_000
|
|
121
|
+
});
|
|
122
|
+
return parseBoundaryAuthStatus(result.stdout, result.stderr);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return parseBoundaryAuthStatus(error?.stdout, error?.stderr || error?.message);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function readSshAgentStatus(execFileImpl) {
|
|
129
|
+
try {
|
|
130
|
+
const result = await execFileImpl("/usr/bin/ssh-add", ["-l"], {
|
|
131
|
+
env: buildBoundaryEnv({ boundaryAddr: DEFAULT_BOUNDARY_ADDR }),
|
|
132
|
+
maxBuffer: 1024 * 1024,
|
|
133
|
+
timeout: 10_000
|
|
134
|
+
});
|
|
135
|
+
const output = normalizeText(result.stdout || result.stderr);
|
|
136
|
+
return {
|
|
137
|
+
ready: /cert|ssh-rsa|ecdsa|ed25519/iu.test(output),
|
|
138
|
+
reason: "",
|
|
139
|
+
identities: output.split("\n").filter(Boolean).slice(0, 10)
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
ready: false,
|
|
144
|
+
reason: normalizeText(error?.stderr || error?.stdout || error?.message || "ssh-agent identity status is unavailable."),
|
|
145
|
+
identities: []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function findOpenPort() {
|
|
151
|
+
return await new Promise((resolve, reject) => {
|
|
152
|
+
const server = net.createServer();
|
|
153
|
+
server.listen(0, "127.0.0.1", () => {
|
|
154
|
+
const port = server.address().port;
|
|
155
|
+
server.close(() => resolve(port));
|
|
156
|
+
});
|
|
157
|
+
server.on("error", reject);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildBoundaryEnv(input, extra = {}) {
|
|
162
|
+
return {
|
|
163
|
+
...process.env,
|
|
164
|
+
...extra,
|
|
165
|
+
BOUNDARY_ADDR: input.boundaryAddr,
|
|
166
|
+
BOUNDARY_SKIP_CACHE_DAEMON: "true",
|
|
167
|
+
PATH: [
|
|
168
|
+
"/opt/homebrew/bin",
|
|
169
|
+
"/Users/boqiang.liang/bin",
|
|
170
|
+
"/usr/local/bin",
|
|
171
|
+
process.env.PATH || ""
|
|
172
|
+
].filter(Boolean).join(":")
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function redactSecret(text, password) {
|
|
177
|
+
const raw = String(text || "");
|
|
178
|
+
if (!password) return raw;
|
|
179
|
+
return raw.split(password).join("[redacted]");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function tclListValue(value) {
|
|
183
|
+
return `{${String(value).replaceAll("\\", "\\\\").replaceAll("{", "\\{").replaceAll("}", "\\}")}}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildExpectScript(command, args) {
|
|
187
|
+
const commandList = [command, ...args].map(tclListValue).join(" ");
|
|
188
|
+
return [
|
|
189
|
+
"set timeout $env(DEVOPS_LOGIN_TIMEOUT_SECONDS)",
|
|
190
|
+
"set password $env(DEVOPS_LDAP_PASSWORD)",
|
|
191
|
+
"set role_index $env(DEVOPS_ROLE_INDEX)",
|
|
192
|
+
`set command [list ${commandList}]`,
|
|
193
|
+
"spawn -noecho {*}$command",
|
|
194
|
+
"expect {",
|
|
195
|
+
" -re {Please enter your Okta/AD password:|Please enter your CORP LDAP password:|Password:} { send -- \"$password\\r\"; exp_continue }",
|
|
196
|
+
" -re {Please Select a Role:} { send -- \"$role_index\\r\"; exp_continue }",
|
|
197
|
+
" eof { catch wait result; exit [lindex $result 3] }",
|
|
198
|
+
" timeout { puts \"Workday boundary login timed out.\"; exit 124 }",
|
|
199
|
+
"}"
|
|
200
|
+
].join("\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function runBauthLogin(execFileImpl, input, password) {
|
|
204
|
+
const timeoutSeconds = Math.max(1, Math.ceil(input.timeoutMs / 1000));
|
|
205
|
+
let result;
|
|
206
|
+
try {
|
|
207
|
+
result = await execFileImpl(input.expectBinary, [
|
|
208
|
+
"-c",
|
|
209
|
+
buildExpectScript(input.command, input.args)
|
|
210
|
+
], {
|
|
211
|
+
env: buildBoundaryEnv(input, {
|
|
212
|
+
DEVOPS_LDAP_PASSWORD: password,
|
|
213
|
+
DEVOPS_LOGIN_TIMEOUT_SECONDS: String(timeoutSeconds),
|
|
214
|
+
DEVOPS_ROLE_INDEX: String(input.roleIndex)
|
|
215
|
+
}),
|
|
216
|
+
maxBuffer: 1024 * 1024,
|
|
217
|
+
timeout: input.timeoutMs + 5_000
|
|
218
|
+
});
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const stdout = redactSecret(error?.stdout, password);
|
|
221
|
+
const stderr = redactSecret(error?.stderr || error?.message, password);
|
|
222
|
+
throw new Error(`bauth init failed with exit ${error?.code ?? "unknown"}: ${[stdout, stderr].filter(Boolean).join("\n").slice(-2000)}`);
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
stdout: redactSecret(result.stdout, password),
|
|
226
|
+
stderr: redactSecret(result.stderr, password),
|
|
227
|
+
exitCode: 0
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizeInput(input = {}) {
|
|
232
|
+
return {
|
|
233
|
+
action: normalizeText(input.action) || "status",
|
|
234
|
+
keychainService: DEFAULT_KEYCHAIN_SERVICE,
|
|
235
|
+
keychainAccount: "eldwin.workday_prod_ldap",
|
|
236
|
+
boundaryAddr: normalizeText(input.boundaryAddr) || process.env.BOUNDARY_ADDR || DEFAULT_BOUNDARY_ADDR,
|
|
237
|
+
boundaryBinary: normalizePinnedText(input.boundaryBinary, DEFAULT_BOUNDARY_BINARY, "boundaryBinary"),
|
|
238
|
+
expectBinary: normalizePinnedText(input.expectBinary, DEFAULT_EXPECT_BINARY, "expectBinary"),
|
|
239
|
+
command: normalizePinnedText(input.command, DEFAULT_BAUTH_COMMAND, "command"),
|
|
240
|
+
args: normalizePinnedArgs(input.args),
|
|
241
|
+
roleIndex: Number.isInteger(input.roleIndex) ? input.roleIndex : 0,
|
|
242
|
+
humanApprovedLocalWrite: input.humanApprovedLocalWrite === true,
|
|
243
|
+
hitlApproval: input.hitlApproval,
|
|
244
|
+
timeoutMs: Number.isInteger(input.timeoutMs) ? Math.max(1000, input.timeoutMs) : DEFAULT_TIMEOUT_MS
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function waitForVault(execFileImpl, url, timeoutMs) {
|
|
249
|
+
const deadline = Date.now() + timeoutMs;
|
|
250
|
+
while (Date.now() < deadline) {
|
|
251
|
+
try {
|
|
252
|
+
await execFileImpl("/usr/bin/curl", ["-sk", "--connect-timeout", "2", "-o", "/dev/null", url], {
|
|
253
|
+
maxBuffer: 1024 * 1024,
|
|
254
|
+
timeout: 5_000
|
|
255
|
+
});
|
|
256
|
+
return true;
|
|
257
|
+
} catch (_error) {
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function parseVaultLoginDiagnostic(raw = "") {
|
|
265
|
+
const text = String(raw || "");
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(text);
|
|
268
|
+
const policies = Array.isArray(parsed?.auth?.token_policies) ? parsed.auth.token_policies : [];
|
|
269
|
+
const roles = policies
|
|
270
|
+
.filter((policy) => policy && policy !== "default")
|
|
271
|
+
.map((policy) => String(policy).replace(/_basic$/u, ""))
|
|
272
|
+
.sort();
|
|
273
|
+
const errors = Array.isArray(parsed?.errors) ? parsed.errors.map(String) : [];
|
|
274
|
+
return {
|
|
275
|
+
loginOk: Boolean(parsed?.auth?.client_token),
|
|
276
|
+
policyCount: policies.length,
|
|
277
|
+
roles,
|
|
278
|
+
errors
|
|
279
|
+
};
|
|
280
|
+
} catch (_error) {
|
|
281
|
+
return {
|
|
282
|
+
loginOk: false,
|
|
283
|
+
policyCount: 0,
|
|
284
|
+
roles: [],
|
|
285
|
+
errors: [text.slice(0, 500)]
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function diagnoseVaultRoles(execFileImpl, spawnImpl, input, password) {
|
|
291
|
+
const port = await findOpenPort();
|
|
292
|
+
const localUrl = `https://localhost:${port}`;
|
|
293
|
+
const child = spawnImpl(input.boundaryBinary, ["connect", DEFAULT_CUST_VAULT_HOST, "-listen-port", String(port)], {
|
|
294
|
+
env: buildBoundaryEnv(input),
|
|
295
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
296
|
+
});
|
|
297
|
+
let stderr = "";
|
|
298
|
+
child.stderr?.setEncoding?.("utf8");
|
|
299
|
+
child.stderr?.on?.("data", (chunk) => {
|
|
300
|
+
stderr += chunk;
|
|
301
|
+
});
|
|
302
|
+
try {
|
|
303
|
+
const reachable = await waitForVault(execFileImpl, `${localUrl}/v1/sys/health`, Math.min(input.timeoutMs, 30_000));
|
|
304
|
+
if (!reachable) {
|
|
305
|
+
return {
|
|
306
|
+
vaultHost: DEFAULT_CUST_VAULT_HOST,
|
|
307
|
+
reachable: false,
|
|
308
|
+
loginOk: false,
|
|
309
|
+
policyCount: 0,
|
|
310
|
+
roles: [],
|
|
311
|
+
errors: [normalizeText(stderr || "Vault tunnel did not become reachable.")]
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const result = await execFileImpl("/usr/bin/curl", [
|
|
315
|
+
"-sk",
|
|
316
|
+
`${localUrl}/v1/auth/ldap/login/${process.env.USER || ""}`,
|
|
317
|
+
"--request",
|
|
318
|
+
"POST",
|
|
319
|
+
"--data",
|
|
320
|
+
JSON.stringify({ password })
|
|
321
|
+
], {
|
|
322
|
+
maxBuffer: 1024 * 1024,
|
|
323
|
+
timeout: 15_000
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
vaultHost: DEFAULT_CUST_VAULT_HOST,
|
|
327
|
+
reachable: true,
|
|
328
|
+
...parseVaultLoginDiagnostic(result.stdout)
|
|
329
|
+
};
|
|
330
|
+
} finally {
|
|
331
|
+
child.kill("SIGTERM");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function workdayBoundaryLogin(input, options = {}) {
|
|
336
|
+
const execFileImpl = options.execFileImpl || execFileAsync;
|
|
337
|
+
const spawnImpl = options.spawnImpl || spawn;
|
|
338
|
+
const normalized = normalizeInput(input);
|
|
339
|
+
const before = await readBoundaryAuthStatus(execFileImpl, normalized);
|
|
340
|
+
const beforeSshAgent = await readSshAgentStatus(execFileImpl);
|
|
341
|
+
if (normalized.action === "status") {
|
|
342
|
+
return JSON.stringify({
|
|
343
|
+
action: "status",
|
|
344
|
+
boundaryAddr: normalized.boundaryAddr,
|
|
345
|
+
boundary: before,
|
|
346
|
+
sshAgent: beforeSshAgent,
|
|
347
|
+
ready: before.ready && beforeSshAgent.ready
|
|
348
|
+
}, null, 2);
|
|
349
|
+
}
|
|
350
|
+
if (normalized.action === "diagnose-vault") {
|
|
351
|
+
const keychain = await readPasswordFromKeychain(execFileImpl);
|
|
352
|
+
if (!keychain.password) {
|
|
353
|
+
throw new Error(`Missing LDAP PROD password in macOS keychain service ${normalized.keychainService}, account ${normalized.keychainAccount}. Store it in eldwin/eldwin.workday_prod_ldap first; wallee/wallee.workday_prod_ldap is supported only as a fallback source.`);
|
|
354
|
+
}
|
|
355
|
+
const diagnostic = await diagnoseVaultRoles(execFileImpl, spawnImpl, normalized, keychain.password);
|
|
356
|
+
return JSON.stringify({ action: "diagnose-vault", boundaryAddr: normalized.boundaryAddr, keychainService: keychain.keychainService, keychainAccount: keychain.keychainAccount, diagnostic }, null, 2);
|
|
357
|
+
}
|
|
358
|
+
if (normalized.action !== "login") {
|
|
359
|
+
throw new Error(`Unsupported workday_boundary_login action: ${normalized.action}`);
|
|
360
|
+
}
|
|
361
|
+
if (!normalized.humanApprovedLocalWrite) {
|
|
362
|
+
throw new Error("workday_boundary_login action=login may update local Boundary/SSH auth state and requires humanApprovedLocalWrite=true.");
|
|
363
|
+
}
|
|
364
|
+
assertStructuredHitlApproval(normalized, "workday_boundary_login action=login");
|
|
365
|
+
const keychain = await readPasswordFromKeychain(execFileImpl);
|
|
366
|
+
if (!keychain.password) {
|
|
367
|
+
throw new Error(`Missing LDAP PROD password in macOS keychain service ${normalized.keychainService}, account ${normalized.keychainAccount}. Store it in eldwin/eldwin.workday_prod_ldap first; wallee/wallee.workday_prod_ldap is supported only as a fallback source.`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const login = await runBauthLogin(execFileImpl, normalized, keychain.password);
|
|
371
|
+
const after = await readBoundaryAuthStatus(execFileImpl, normalized);
|
|
372
|
+
const afterSshAgent = await readSshAgentStatus(execFileImpl);
|
|
373
|
+
return JSON.stringify({
|
|
374
|
+
action: "login",
|
|
375
|
+
boundaryAddr: normalized.boundaryAddr,
|
|
376
|
+
keychainService: keychain.keychainService,
|
|
377
|
+
keychainAccount: keychain.keychainAccount,
|
|
378
|
+
command: normalized.command,
|
|
379
|
+
args: normalized.args,
|
|
380
|
+
roleIndex: normalized.roleIndex,
|
|
381
|
+
loginExitCode: login.exitCode,
|
|
382
|
+
boundary: after,
|
|
383
|
+
sshAgent: afterSshAgent,
|
|
384
|
+
ready: after.ready && afterSshAgent.ready,
|
|
385
|
+
transcript: [login.stdout, login.stderr].filter(Boolean).join("\n").slice(-1000)
|
|
386
|
+
}, null, 2);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export class WorkdayBoundaryLoginService {
|
|
390
|
+
constructor(options = {}) {
|
|
391
|
+
this.options = options;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async invoke(input) {
|
|
395
|
+
return workdayBoundaryLogin(input, this.options);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const defaultService = new WorkdayBoundaryLoginService();
|
|
400
|
+
|
|
401
|
+
export const __test = {
|
|
402
|
+
WorkdayBoundaryLoginService,
|
|
403
|
+
extractPasswordFromKeychainSecret,
|
|
404
|
+
parseBoundaryAuthStatus,
|
|
405
|
+
readSshAgentStatus,
|
|
406
|
+
parseVaultLoginDiagnostic,
|
|
407
|
+
diagnoseVaultRoles,
|
|
408
|
+
normalizeInput,
|
|
409
|
+
normalizePinnedText,
|
|
410
|
+
workdayBoundaryLogin,
|
|
411
|
+
runBauthLogin,
|
|
412
|
+
redactSecret,
|
|
413
|
+
tclListValue
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const sharedTool = defineReadOnlyTool({
|
|
417
|
+
name: "workday_boundary_login",
|
|
418
|
+
description: "Check or prepare Workday datacenter Boundary authentication using the saved macOS keychain LDAP password. The tool consumes the password in-place for bauth init and never returns the secret.",
|
|
419
|
+
meta: {
|
|
420
|
+
permissions: { rule: "ask" },
|
|
421
|
+
secret_provider: "keychain"
|
|
422
|
+
},
|
|
423
|
+
inputSchema: {
|
|
424
|
+
action: z.enum(["status", "login", "diagnose-vault"]).default("status"),
|
|
425
|
+
boundaryAddr: z.string().trim().url().max(2048).default(DEFAULT_BOUNDARY_ADDR),
|
|
426
|
+
boundaryBinary: z.literal(DEFAULT_BOUNDARY_BINARY).default(DEFAULT_BOUNDARY_BINARY),
|
|
427
|
+
expectBinary: z.literal(DEFAULT_EXPECT_BINARY).default(DEFAULT_EXPECT_BINARY),
|
|
428
|
+
command: z.literal(DEFAULT_BAUTH_COMMAND).default(DEFAULT_BAUTH_COMMAND),
|
|
429
|
+
args: z.tuple([z.literal("init")]).default(DEFAULT_BAUTH_ARGS),
|
|
430
|
+
roleIndex: z.number().int().min(0).max(99).default(0),
|
|
431
|
+
humanApprovedLocalWrite: z.boolean().default(false).describe("Required for action=login because bauth may update local Boundary/SSH auth state."),
|
|
432
|
+
hitlApproval: z.object({
|
|
433
|
+
approved: z.literal(true),
|
|
434
|
+
approvedBy: z.string().trim().min(1).max(256),
|
|
435
|
+
action: z.string().trim().min(1).max(256),
|
|
436
|
+
target: z.string().trim().min(1).max(4096),
|
|
437
|
+
reason: z.string().trim().min(1).max(2000),
|
|
438
|
+
expectedImpact: z.string().trim().min(1).max(2000),
|
|
439
|
+
risk: z.string().trim().min(1).max(2000)
|
|
440
|
+
}).optional().describe("Required for action=login because local auth state may be updated."),
|
|
441
|
+
timeoutMs: z.number().int().positive().max(600_000).default(DEFAULT_TIMEOUT_MS)
|
|
442
|
+
},
|
|
443
|
+
async handler(input) {
|
|
444
|
+
try {
|
|
445
|
+
return ok(await defaultService.invoke(input));
|
|
446
|
+
} catch (error) {
|
|
447
|
+
return fail(error);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
export default {
|
|
453
|
+
name: sharedTool.name,
|
|
454
|
+
description,
|
|
455
|
+
inputSchema: sharedTool.inputSchema,
|
|
456
|
+
meta: sharedTool.meta,
|
|
457
|
+
annotations: sharedTool.annotations,
|
|
458
|
+
invoke: sharedTool.invoke,
|
|
459
|
+
handler: sharedTool.handler
|
|
460
|
+
};
|