Skip to content

Commit d9470e0

Browse files
committed
samples(http): publish proxmox-ve-http ticket-cookie/CSRF sample with full README
1 parent 9a34426 commit d9470e0

5 files changed

Lines changed: 392 additions & 1 deletion

File tree

docs/agent-reference/samples-index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ CI runs the same script with `-CheckOnly` and fails the build if the committed c
2727
| http || CheckSystem, CheckPassword, ChangePassword || [`samples/http/forgerock-openam/Forgerock_OpenAM.json`](../../samples/http/forgerock-openam/Forgerock_OpenAM.json) | [README](../../samples/http/forgerock-openam/README.md) |
2828
| http || CheckSystem, CheckPassword, ChangePassword, EnableAccount, DisableAccount, DiscoverAccounts || [`samples/http/okta-discovery/Okta_WithDiscoveryAndGroupMembershipRestore.json`](../../samples/http/okta-discovery/Okta_WithDiscoveryAndGroupMembershipRestore.json) | [README](../../samples/http/okta-discovery/README.md) |
2929
| http | Basic+Bearer | CheckSystem, ChangePassword, EnableAccount, DisableAccount, ElevateAccount, DemoteAccount || [`samples/http/onelogin-jit/OneLogin_GRC_JIT_addon.json`](../../samples/http/onelogin-jit/OneLogin_GRC_JIT_addon.json) | [README](../../samples/http/onelogin-jit/README.md) |
30+
| http || CheckSystem, CheckPassword, ChangePassword || [`samples/http/proxmox-ve-http/ProxmoxVE-HTTP.json`](../../samples/http/proxmox-ve-http/ProxmoxVE-HTTP.json) | [README](../../samples/http/proxmox-ve-http/README.md) |
3031
| http | Form | CheckPassword, ChangePassword || [`samples/http/twitter/CustomTwitter.json`](../../samples/http/twitter/CustomTwitter.json) | [README](../../samples/http/twitter/README.md) |
3132
| http | Basic | CheckSystem, CheckPassword, ChangePassword || [`samples/http/wordpress/WordPressHttp.json`](../../samples/http/wordpress/WordPressHttp.json) | [README](../../samples/http/wordpress/README.md) |
3233
| ssh | Interactive | CheckSystem, CheckPassword, ChangePassword, DiscoverSshHostKey, ChangeSshKey, CheckSshKey, DiscoverAuthorizedKeys || [`samples/ssh/generic-linux-ssh-keys/GenericLinuxWithSSHKeySupport.json`](../../samples/ssh/generic-linux-ssh-keys/GenericLinuxWithSSHKeySupport.json) | [README](../../samples/ssh/generic-linux-ssh-keys/README.md) |

docs/agent-reference/strategy-decision-tree.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Pick a bucket, then a specific scheme. The bucket determines whether the script
7777
| Vendor docs or probe evidence show `Authorization: Bearer …` on operation calls. | Script-managed header | `Bearer` | `Headers.AddHeaders` with `Authorization: Bearer %Token%`. See `samples/http/onelogin-jit/`. |
7878
| Vendor docs show a custom `Authorization` scheme such as `PVEAPIToken=…` (Proxmox token-auth path), `Token …`, or other vendor-specific prefix. | Script-managed header | Custom `Authorization` scheme | Same as Bearer but with the vendor's scheme name. Treat as Bearer-shaped for authoring purposes. |
7979
| Vendor docs describe a static API key passed in a custom header (e.g., `X-API-Key`, `X-Vault-Token`, `X-Auth-Token`). | Script-managed header | Custom-header API key | `Headers.AddHeaders` with the named header. Pair with [`docs/guides/api-key-management.md`](../guides/api-key-management.md) when the same script must rotate the key. |
80-
| Vendor docs describe a session-cookie auth flow: a login endpoint returns an opaque session token that is then sent back as a `Cookie:` header on every subsequent call (often paired with a secondary CSRF/XSRF header on write verbs). The Proxmox VE **ticket-cookie** path is canonical — `POST /access/ticket` returns `data.ticket` and `data.CSRFPreventionToken`; subsequent calls send `Cookie: PVEAuthCookie=<ticket>` and write calls additionally send `CSRFPreventionToken: <token>`. | Script-managed header | Session cookie (+ CSRF header on writes) | Two-step by definition (the cookie is fetched, not pre-held). `Headers.AddHeaders` with `Cookie: <name>=%Token%` on every call; add the CSRF header on writes only. Form bodies for the login post **must** use `SetFormValue` + `Request.Content.ContentObjectName` — pre-built body strings via `Request.Content.Value` re-encode unpredictably (see `failure-patterns.md`). Beware native-cookie-jar interactions: if you also set `PersistCookies: true` (default), the engine may inject a session cookie from a prior call alongside the script-set `Cookie` header — pick one mechanism and stick to it. |
80+
| Vendor docs describe a session-cookie auth flow: a login endpoint returns an opaque session token that is then sent back as a `Cookie:` header on every subsequent call (often paired with a secondary CSRF/XSRF header on write verbs). The Proxmox VE **ticket-cookie** path is canonical — `POST /access/ticket` returns `data.ticket` and `data.CSRFPreventionToken`; subsequent calls send `Cookie: PVEAuthCookie=<ticket>` and write calls additionally send `CSRFPreventionToken: <token>`. See [`samples/http/proxmox-ve-http/`](../../samples/http/proxmox-ve-http/). | Script-managed header | Session cookie (+ CSRF header on writes) | Two-step by definition (the cookie is fetched, not pre-held). `Headers.AddHeaders` with `Cookie: <name>=%Token%` on every call; add the CSRF header on writes only. Form bodies for the login post **must** use `SetFormValue` + `Request.Content.ContentObjectName` — pre-built body strings via `Request.Content.Value` re-encode unpredictably (see `failure-patterns.md`). Beware native-cookie-jar interactions: if you also set `PersistCookies: true` (default), the engine may inject a session cookie from a prior call alongside the script-set `Cookie` header — pick one mechanism and stick to it. |
8181

8282
### `http-api` one-step vs two-step
8383

samples/http/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Tested custom platform scripts for managing systems over HTTP/REST APIs. These s
1111
| [forgerock-openam](forgerock-openam/) | ⭐⭐ | ForgeRock AM 7.5 REST API |
1212
| [okta-discovery](okta-discovery/) | ⭐⭐⭐ | Okta with account discovery and group membership |
1313
| [onelogin-jit](onelogin-jit/) | ⭐⭐⭐ | OneLogin JIT elevation and account activation |
14+
| [proxmox-ve-http](proxmox-ve-http/) | ⭐⭐⭐ | Proxmox VE 7.x/8.x REST API (ticket-cookie + CSRF) |
1415
| [wordpress](wordpress/) | ⭐⭐ | WordPress REST API with Basic Auth |
1516

1617
## Choosing a Sample
@@ -20,6 +21,7 @@ Tested custom platform scripts for managing systems over HTTP/REST APIs. These s
2021
- **Need account discovery?** See [okta-discovery](okta-discovery/).
2122
- **JIT elevation workflow?** Try [onelogin-jit](onelogin-jit/).
2223
- **Browser-form login (not a REST API)?** See [facebook](facebook/) or [twitter](twitter/).
24+
- **Cookie-based session auth with CSRF tokens?** See [proxmox-ve-http](proxmox-ve-http/).
2325

2426
## Related Docs
2527

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
{
2+
"Id": "ProxmoxVE",
3+
"BackEnd": "Scriptable",
4+
"Meta": {
5+
"Description": "Proxmox VE password rotation via the ticket-cookie REST API (HTTP variant). Manages users in the @pve realm only — @pam-realm users are OS-PAM-backed and require root-on-host (use the SSH variant instead). Service-account model: a privileged @pve user (FuncUserName) rotates the managed @pve user's (AccountUserName) password via PUT /access/password. Two-step token fetch — POST /access/ticket returns a session ticket plus a CSRFPreventionToken; subsequent calls send Cookie: PVEAuthCookie=<ticket> and (on writes) CSRFPreventionToken: <token>. Cross-user password changes additionally require the caller's own password as the confirmation-password form field."
6+
},
7+
"CheckSystem": {
8+
"Parameters": [
9+
{ "Address": { "Type": "String", "Required": true } },
10+
{ "Port": { "Type": "Integer", "Required": false, "DefaultValue": 8006 } },
11+
{ "Timeout": { "Type": "Integer", "Required": false, "DefaultValue": 30 } },
12+
{ "SkipServerCertValidation": { "Type": "Boolean", "Required": false, "DefaultValue": false } },
13+
{ "FuncUserName": { "Type": "String", "Required": true } },
14+
{ "FuncPassword": { "Type": "Secret", "Required": true } }
15+
],
16+
"Do": [
17+
{
18+
"Try": {
19+
"Do": [
20+
{
21+
"Function": {
22+
"Name": "AuthenticateClient",
23+
"Parameters": [
24+
"%Address%",
25+
"%{Port}%",
26+
"%{Timeout}%",
27+
"%{SkipServerCertValidation}%",
28+
"%FuncUserName%",
29+
"%FuncPassword%"
30+
],
31+
"ResultVariable": "AuthenticateClientResult"
32+
}
33+
},
34+
{ "NewHttpRequest": { "ObjectName": "SystemRequest" } },
35+
{
36+
"Headers": {
37+
"RequestObjectName": "SystemRequest",
38+
"AddHeaders": {
39+
"Accept": "application/json",
40+
"Cookie": "PVEAuthCookie=%Ticket%"
41+
}
42+
}
43+
},
44+
{
45+
"Request": {
46+
"RequestObjectName": "SystemRequest",
47+
"ResponseObjectName": "SystemResponse",
48+
"Verb": "GET",
49+
"Url": "/api2/json/version",
50+
"IgnoreServerCertAuthentication": "%{SkipServerCertValidation}%"
51+
}
52+
},
53+
{
54+
"Condition": {
55+
"If": "SystemResponse.StatusCode == 200",
56+
"Then": { "Do": [ { "Return": { "Value": true } } ] },
57+
"Else": { "Do": [ { "Throw": { "Value": "CheckSystem failed with HTTP %{SystemResponse.StatusCode}%" } } ] }
58+
}
59+
}
60+
],
61+
"Catch": [ { "Return": { "Value": false } } ]
62+
}
63+
}
64+
]
65+
},
66+
"CheckPassword": {
67+
"Parameters": [
68+
{ "Address": { "Type": "String", "Required": true } },
69+
{ "Port": { "Type": "Integer", "Required": false, "DefaultValue": 8006 } },
70+
{ "Timeout": { "Type": "Integer", "Required": false, "DefaultValue": 30 } },
71+
{ "SkipServerCertValidation": { "Type": "Boolean", "Required": false, "DefaultValue": false } },
72+
{ "FuncUserName": { "Type": "String", "Required": true } },
73+
{ "FuncPassword": { "Type": "Secret", "Required": true } },
74+
{ "AccountUserName": { "Type": "String", "Required": true } },
75+
{ "AccountPassword": { "Type": "Secret", "Required": true } }
76+
],
77+
"Do": [
78+
{
79+
"Try": {
80+
"Do": [
81+
{
82+
"Function": {
83+
"Name": "SetBaseAddress",
84+
"Parameters": [ "%Address%", "%{Port}%" ],
85+
"ResultVariable": "SetBaseAddressResult"
86+
}
87+
},
88+
{ "SetFormValue": { "FormObjectName": "CheckPasswordForm", "InputName": "username", "Value": "%AccountUserName%" } },
89+
{ "SetFormValue": { "FormObjectName": "CheckPasswordForm", "InputName": "password", "Value": "%AccountPassword%", "IsSecret": true } },
90+
{ "NewHttpRequest": { "ObjectName": "CheckPasswordRequest" } },
91+
{
92+
"Headers": {
93+
"RequestObjectName": "CheckPasswordRequest",
94+
"AddHeaders": { "Accept": "application/json" }
95+
}
96+
},
97+
{
98+
"Request": {
99+
"RequestObjectName": "CheckPasswordRequest",
100+
"ResponseObjectName": "CheckPasswordResponse",
101+
"Verb": "POST",
102+
"Url": "/api2/json/access/ticket",
103+
"Content": {
104+
"ContentObjectName": "CheckPasswordForm",
105+
"ContentType": "application/x-www-form-urlencoded"
106+
},
107+
"IgnoreServerCertAuthentication": "%{SkipServerCertValidation}%"
108+
}
109+
},
110+
{
111+
"Condition": {
112+
"If": "CheckPasswordResponse.StatusCode == 200",
113+
"Then": { "Do": [ { "Return": { "Value": true } } ] },
114+
"Else": {
115+
"Do": [
116+
{
117+
"Condition": {
118+
"If": "CheckPasswordResponse.StatusCode == 401",
119+
"Then": { "Do": [ { "Return": { "Value": false } } ] },
120+
"Else": { "Do": [ { "Throw": { "Value": "CheckPassword returned HTTP %{CheckPasswordResponse.StatusCode}%" } } ] }
121+
}
122+
}
123+
]
124+
}
125+
}
126+
}
127+
],
128+
"Catch": [ { "Return": { "Value": false } } ]
129+
}
130+
}
131+
]
132+
},
133+
"ChangePassword": {
134+
"Parameters": [
135+
{ "Address": { "Type": "String", "Required": true } },
136+
{ "Port": { "Type": "Integer", "Required": false, "DefaultValue": 8006 } },
137+
{ "Timeout": { "Type": "Integer", "Required": false, "DefaultValue": 30 } },
138+
{ "SkipServerCertValidation": { "Type": "Boolean", "Required": false, "DefaultValue": false } },
139+
{ "FuncUserName": { "Type": "String", "Required": true } },
140+
{ "FuncPassword": { "Type": "Secret", "Required": true } },
141+
{ "AccountUserName": { "Type": "String", "Required": true } },
142+
{ "AccountPassword": { "Type": "Secret", "Required": true } },
143+
{ "NewPassword": { "Type": "Secret", "Required": true } }
144+
],
145+
"Do": [
146+
{
147+
"Try": {
148+
"Do": [
149+
{
150+
"Condition": {
151+
"If": "AccountPassword.Equals(NewPassword)",
152+
"Then": { "Do": [ { "Return": { "Value": false } } ] }
153+
}
154+
},
155+
{
156+
"Function": {
157+
"Name": "AuthenticateClient",
158+
"Parameters": [
159+
"%Address%",
160+
"%{Port}%",
161+
"%{Timeout}%",
162+
"%{SkipServerCertValidation}%",
163+
"%FuncUserName%",
164+
"%FuncPassword%"
165+
],
166+
"ResultVariable": "AuthenticateClientResult"
167+
}
168+
},
169+
{ "SetFormValue": { "FormObjectName": "ChangePasswordForm", "InputName": "userid", "Value": "%AccountUserName%" } },
170+
{ "SetFormValue": { "FormObjectName": "ChangePasswordForm", "InputName": "password", "Value": "%NewPassword%", "IsSecret": true } },
171+
{ "SetFormValue": { "FormObjectName": "ChangePasswordForm", "InputName": "confirmation-password", "Value": "%FuncPassword%", "IsSecret": true } },
172+
{ "NewHttpRequest": { "ObjectName": "ChangePasswordRequest" } },
173+
{
174+
"Headers": {
175+
"RequestObjectName": "ChangePasswordRequest",
176+
"AddHeaders": {
177+
"Accept": "application/json",
178+
"Cookie": "PVEAuthCookie=%Ticket%",
179+
"CSRFPreventionToken": "%CsrfToken%"
180+
}
181+
}
182+
},
183+
{
184+
"Request": {
185+
"RequestObjectName": "ChangePasswordRequest",
186+
"ResponseObjectName": "ChangePasswordResponse",
187+
"Verb": "PUT",
188+
"Url": "/api2/json/access/password",
189+
"Content": {
190+
"ContentObjectName": "ChangePasswordForm",
191+
"ContentType": "application/x-www-form-urlencoded"
192+
},
193+
"IgnoreServerCertAuthentication": "%{SkipServerCertValidation}%"
194+
}
195+
},
196+
{
197+
"Condition": {
198+
"If": "ChangePasswordResponse.StatusCode == 200",
199+
"Then": { "Do": [ { "Return": { "Value": true } } ] },
200+
"Else": { "Do": [ { "Throw": { "Value": "ChangePassword failed with HTTP %{ChangePasswordResponse.StatusCode}%" } } ] }
201+
}
202+
}
203+
],
204+
"Catch": [ { "Throw": { "Value": "%Exception%" } } ]
205+
}
206+
}
207+
]
208+
},
209+
"Functions": [
210+
{
211+
"Name": "SetBaseAddress",
212+
"Parameters": [
213+
{ "Address": { "Type": "String", "Required": true } },
214+
{ "Port": { "Type": "Integer", "Required": false, "DefaultValue": 8006 } }
215+
],
216+
"Do": [
217+
{ "BaseAddress": { "Address": "https://%Address%:%{Port}%" } },
218+
{ "Return": { "Value": true } }
219+
]
220+
},
221+
{
222+
"Name": "AuthenticateClient",
223+
"Parameters": [
224+
{ "Address": { "Type": "String", "Required": true } },
225+
{ "Port": { "Type": "Integer", "Required": false, "DefaultValue": 8006 } },
226+
{ "Timeout": { "Type": "Integer", "Required": false, "DefaultValue": 30 } },
227+
{ "SkipServerCertValidation": { "Type": "Boolean", "Required": false, "DefaultValue": false } },
228+
{ "FuncUserName": { "Type": "String", "Required": true } },
229+
{ "FuncPassword": { "Type": "Secret", "Required": true } }
230+
],
231+
"Do": [
232+
{
233+
"Function": {
234+
"Name": "SetBaseAddress",
235+
"Parameters": [ "%Address%", "%{Port}%" ],
236+
"ResultVariable": "SetBaseAddressResult"
237+
}
238+
},
239+
{ "SetFormValue": { "FormObjectName": "TicketForm", "InputName": "username", "Value": "%FuncUserName%" } },
240+
{ "SetFormValue": { "FormObjectName": "TicketForm", "InputName": "password", "Value": "%FuncPassword%", "IsSecret": true } },
241+
{ "NewHttpRequest": { "ObjectName": "TicketRequest" } },
242+
{
243+
"Headers": {
244+
"RequestObjectName": "TicketRequest",
245+
"AddHeaders": { "Accept": "application/json" }
246+
}
247+
},
248+
{
249+
"Request": {
250+
"RequestObjectName": "TicketRequest",
251+
"ResponseObjectName": "TicketResponse",
252+
"Verb": "POST",
253+
"Url": "/api2/json/access/ticket",
254+
"Content": {
255+
"ContentObjectName": "TicketForm",
256+
"ContentType": "application/x-www-form-urlencoded"
257+
},
258+
"IgnoreServerCertAuthentication": "%{SkipServerCertValidation}%"
259+
}
260+
},
261+
{
262+
"Condition": {
263+
"If": "TicketResponse.StatusCode == 200",
264+
"Then": {
265+
"Do": [
266+
{
267+
"ExtractJsonObject": {
268+
"JsonObjectName": "TicketResponse",
269+
"Name": "TicketResponseJson",
270+
"ContainsSecret": true
271+
}
272+
},
273+
{
274+
"Condition": {
275+
"If": "TicketResponseJson.data != null && TicketResponseJson.data.ticket != null && !string.IsNullOrWhiteSpace(TicketResponseJson.data.ticket.ToString())",
276+
"Then": {
277+
"Do": [
278+
{
279+
"SetItem": {
280+
"Name": "Global:Ticket",
281+
"Value": "%{TicketResponseJson.data.ticket.ToString()}%",
282+
"IsSecret": true
283+
}
284+
},
285+
{
286+
"SetItem": {
287+
"Name": "Global:CsrfToken",
288+
"Value": "%{TicketResponseJson.data.CSRFPreventionToken.ToString()}%",
289+
"IsSecret": true
290+
}
291+
},
292+
{ "Return": { "Value": true } }
293+
]
294+
},
295+
"Else": { "Do": [ { "Throw": { "Value": "Ticket response did not include data.ticket." } } ] }
296+
}
297+
}
298+
]
299+
},
300+
"Else": { "Do": [ { "Throw": { "Value": "Service-account authentication failed with HTTP %{TicketResponse.StatusCode}%" } } ] }
301+
}
302+
}
303+
]
304+
}
305+
]
306+
}

0 commit comments

Comments
 (0)