fix: rerun args
This commit is contained in:
parent
e61c8b036d
commit
42003da384
|
|
@ -1,4 +1,4 @@
|
|||
// @generated by protoc-gen-es v2.12.0
|
||||
// @generated by protoc-gen-es v2.12.1
|
||||
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
|
|
@ -751,6 +751,11 @@ export declare type LogEntry = Message<"olivetin.api.v1.LogEntry"> & {
|
|||
* @generated from field: string justification = 23;
|
||||
*/
|
||||
justification: string;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated olivetin.api.v1.StartActionArgument arguments = 24;
|
||||
*/
|
||||
arguments: StartActionArgument[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,59 @@
|
|||
import { needsArgumentForm } from './needsArgumentForm.js'
|
||||
|
||||
export function logEntryArgumentsToStartActionArgs (logEntry) {
|
||||
return (logEntry?.arguments ?? []).map((arg) => ({
|
||||
name: arg.name,
|
||||
value: arg.value
|
||||
}))
|
||||
}
|
||||
|
||||
export function rerunNeedsArgumentForm (action, logEntry) {
|
||||
if (action?.justification && !logEntry?.justification) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!needsArgumentForm(action)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return hasMissingRerunArguments(action, logEntry?.arguments ?? [])
|
||||
}
|
||||
|
||||
export function hasMissingRerunArguments (action, storedArgs) {
|
||||
const stored = new Map(storedArgs.map((arg) => [arg.name, arg.value]))
|
||||
|
||||
for (const arg of action?.arguments ?? []) {
|
||||
if (arg.type === 'password') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (arg.required && !stored.has(arg.name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function buildArgumentFormQuery (logEntry) {
|
||||
const query = {}
|
||||
|
||||
for (const arg of logEntry?.arguments ?? []) {
|
||||
query[arg.name] = arg.value
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
export function buildRerunStartActionArgs (bindingId, logEntry, action) {
|
||||
const startActionArgs = {
|
||||
bindingId,
|
||||
arguments: logEntryArgumentsToStartActionArgs(logEntry)
|
||||
}
|
||||
|
||||
if (action?.justification && logEntry?.justification) {
|
||||
startActionArgs.justification = logEntry.justification
|
||||
}
|
||||
|
||||
return startActionArgs
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
buildArgumentFormQuery,
|
||||
buildRerunStartActionArgs,
|
||||
hasMissingRerunArguments,
|
||||
logEntryArgumentsToStartActionArgs,
|
||||
rerunNeedsArgumentForm
|
||||
} from '../utils/rerunArguments.js'
|
||||
|
||||
test('logEntryArgumentsToStartActionArgs maps proto arguments for StartAction', () => {
|
||||
assert.deepEqual(
|
||||
logEntryArgumentsToStartActionArgs({
|
||||
arguments: [
|
||||
{ name: 'host', value: 'example.com' },
|
||||
{ name: 'port', value: '443' }
|
||||
]
|
||||
}),
|
||||
[
|
||||
{ name: 'host', value: 'example.com' },
|
||||
{ name: 'port', value: '443' }
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('hasMissingRerunArguments requires password fields to be re-entered', () => {
|
||||
const action = {
|
||||
arguments: [
|
||||
{ name: 'user', type: 'ascii_identifier', required: true },
|
||||
{ name: 'pass', type: 'password', required: true }
|
||||
]
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
hasMissingRerunArguments(action, [{ name: 'user', value: 'alice' }]),
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
test('hasMissingRerunArguments detects missing required stored arguments', () => {
|
||||
const action = {
|
||||
arguments: [{ name: 'host', type: 'ascii_identifier', required: true }]
|
||||
}
|
||||
|
||||
assert.equal(hasMissingRerunArguments(action, []), true)
|
||||
assert.equal(
|
||||
hasMissingRerunArguments(action, [{ name: 'host', value: 'db-1' }]),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
test('rerunNeedsArgumentForm can start directly when stored args are complete', () => {
|
||||
const action = {
|
||||
arguments: [{ name: 'host', type: 'ascii_identifier', required: true }]
|
||||
}
|
||||
const logEntry = {
|
||||
arguments: [{ name: 'host', value: 'db-1' }]
|
||||
}
|
||||
|
||||
assert.equal(rerunNeedsArgumentForm(action, logEntry), false)
|
||||
})
|
||||
|
||||
test('rerunNeedsArgumentForm opens the form when justification is missing', () => {
|
||||
const action = { justification: true, arguments: [] }
|
||||
|
||||
assert.equal(rerunNeedsArgumentForm(action, {}), true)
|
||||
assert.equal(
|
||||
rerunNeedsArgumentForm(action, { justification: 'approved change' }),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
test('buildRerunStartActionArgs includes stored justification', () => {
|
||||
assert.deepEqual(
|
||||
buildRerunStartActionArgs('binding-1', {
|
||||
arguments: [{ name: 'host', value: 'db-1' }],
|
||||
justification: 'maintenance window'
|
||||
}, {
|
||||
justification: true,
|
||||
arguments: [{ name: 'host', type: 'ascii_identifier' }]
|
||||
}),
|
||||
{
|
||||
bindingId: 'binding-1',
|
||||
arguments: [{ name: 'host', value: 'db-1' }],
|
||||
justification: 'maintenance window'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('buildArgumentFormQuery prefills non-password stored arguments', () => {
|
||||
assert.deepEqual(
|
||||
buildArgumentFormQuery({
|
||||
arguments: [
|
||||
{ name: 'host', value: 'db-1' },
|
||||
{ name: 'port', value: '5432' }
|
||||
]
|
||||
}),
|
||||
{
|
||||
host: 'db-1',
|
||||
port: '5432'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
@ -174,6 +174,7 @@ message LogEntry {
|
|||
bool queued = 21;
|
||||
string queued_for_group = 22;
|
||||
string justification = 23;
|
||||
repeated StartActionArgument arguments = 24;
|
||||
}
|
||||
|
||||
message GetLogsResponse {
|
||||
|
|
|
|||
|
|
@ -1424,6 +1424,7 @@ type LogEntry struct {
|
|||
Queued bool `protobuf:"varint,21,opt,name=queued,proto3" json:"queued,omitempty"`
|
||||
QueuedForGroup string `protobuf:"bytes,22,opt,name=queued_for_group,json=queuedForGroup,proto3" json:"queued_for_group,omitempty"`
|
||||
Justification string `protobuf:"bytes,23,opt,name=justification,proto3" json:"justification,omitempty"`
|
||||
Arguments []*StartActionArgument `protobuf:"bytes,24,rep,name=arguments,proto3" json:"arguments,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
|
@ -1605,6 +1606,13 @@ func (x *LogEntry) GetJustification() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (x *LogEntry) GetArguments() []*StartActionArgument {
|
||||
if x != nil {
|
||||
return x.Arguments
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetLogsResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Logs []*LogEntry `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"`
|
||||
|
|
@ -4691,7 +4699,7 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
|||
"\vdate_filter\x18\x02 \x01(\tR\n" +
|
||||
"dateFilter\x12\x1b\n" +
|
||||
"\tpage_size\x18\x03 \x01(\x03R\bpageSize\x12\x16\n" +
|
||||
"\x06filter\x18\x04 \x01(\tR\x06filter\"\xf1\x05\n" +
|
||||
"\x06filter\x18\x04 \x01(\tR\x06filter\"\xb5\x06\n" +
|
||||
"\bLogEntry\x12)\n" +
|
||||
"\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" +
|
||||
"\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" +
|
||||
|
|
@ -4717,7 +4725,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
|||
"binding_id\x18\x14 \x01(\tR\tbindingId\x12\x16\n" +
|
||||
"\x06queued\x18\x15 \x01(\bR\x06queued\x12(\n" +
|
||||
"\x10queued_for_group\x18\x16 \x01(\tR\x0equeuedForGroup\x12$\n" +
|
||||
"\rjustification\x18\x17 \x01(\tR\rjustification\"\xca\x01\n" +
|
||||
"\rjustification\x18\x17 \x01(\tR\rjustification\x12B\n" +
|
||||
"\targuments\x18\x18 \x03(\v2$.olivetin.api.v1.StartActionArgumentR\targuments\"\xca\x01\n" +
|
||||
"\x0fGetLogsResponse\x12-\n" +
|
||||
"\x04logs\x18\x01 \x03(\v2\x19.olivetin.api.v1.LogEntryR\x04logs\x12'\n" +
|
||||
"\x0fcount_remaining\x18\x02 \x01(\x03R\x0ecountRemaining\x12\x1b\n" +
|
||||
|
|
@ -5055,88 +5064,89 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
|
|||
12, // 13: olivetin.api.v1.StartActionAndWaitRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument
|
||||
21, // 14: olivetin.api.v1.StartActionAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||
21, // 15: olivetin.api.v1.StartActionByGetAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||
21, // 16: olivetin.api.v1.GetLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry
|
||||
21, // 17: olivetin.api.v1.GetActionLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry
|
||||
21, // 18: olivetin.api.v1.ExecutionQueueAction.entries:type_name -> olivetin.api.v1.LogEntry
|
||||
26, // 19: olivetin.api.v1.ExecutionQueueGroup.actions:type_name -> olivetin.api.v1.ExecutionQueueAction
|
||||
27, // 20: olivetin.api.v1.GetExecutionQueueResponse.groups:type_name -> olivetin.api.v1.ExecutionQueueGroup
|
||||
21, // 21: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||
34, // 22: olivetin.api.v1.ExecutionStatusResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
|
||||
80, // 23: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry
|
||||
81, // 24: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
|
||||
50, // 25: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged
|
||||
51, // 26: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged
|
||||
53, // 27: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished
|
||||
54, // 28: olivetin.api.v1.EventStreamResponse.execution_started:type_name -> olivetin.api.v1.EventExecutionStarted
|
||||
49, // 29: olivetin.api.v1.EventStreamResponse.output_chunk:type_name -> olivetin.api.v1.EventOutputChunk
|
||||
52, // 30: olivetin.api.v1.EventStreamResponse.heartbeat:type_name -> olivetin.api.v1.EventHeartbeat
|
||||
21, // 31: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||
21, // 32: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||
68, // 33: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider
|
||||
67, // 34: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink
|
||||
7, // 35: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy
|
||||
0, // 36: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
|
||||
34, // 37: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
|
||||
73, // 38: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
|
||||
5, // 39: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
|
||||
42, // 40: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding
|
||||
8, // 41: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
|
||||
11, // 42: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
|
||||
14, // 43: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
|
||||
16, // 44: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest
|
||||
18, // 45: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest
|
||||
75, // 46: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest
|
||||
55, // 47: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest
|
||||
33, // 48: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest
|
||||
20, // 49: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest
|
||||
23, // 50: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest
|
||||
25, // 51: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest
|
||||
29, // 52: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest
|
||||
36, // 53: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest
|
||||
38, // 54: olivetin.api.v1.OliveTinApiService.SosReport:input_type -> olivetin.api.v1.SosReportRequest
|
||||
40, // 55: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest
|
||||
43, // 56: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest
|
||||
45, // 57: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest
|
||||
57, // 58: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest
|
||||
59, // 59: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest
|
||||
61, // 60: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest
|
||||
47, // 61: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest
|
||||
63, // 62: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest
|
||||
65, // 63: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest
|
||||
69, // 64: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest
|
||||
71, // 65: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest
|
||||
74, // 66: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest
|
||||
6, // 67: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse
|
||||
13, // 68: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse
|
||||
15, // 69: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse
|
||||
17, // 70: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse
|
||||
19, // 71: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse
|
||||
13, // 72: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse
|
||||
56, // 73: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse
|
||||
35, // 74: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse
|
||||
22, // 75: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse
|
||||
24, // 76: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse
|
||||
28, // 77: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse
|
||||
30, // 78: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse
|
||||
37, // 79: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse
|
||||
39, // 80: olivetin.api.v1.OliveTinApiService.SosReport:output_type -> olivetin.api.v1.SosReportResponse
|
||||
41, // 81: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse
|
||||
44, // 82: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse
|
||||
46, // 83: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse
|
||||
58, // 84: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse
|
||||
60, // 85: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse
|
||||
62, // 86: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse
|
||||
48, // 87: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse
|
||||
64, // 88: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse
|
||||
66, // 89: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse
|
||||
70, // 90: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse
|
||||
72, // 91: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse
|
||||
5, // 92: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity
|
||||
67, // [67:93] is the sub-list for method output_type
|
||||
41, // [41:67] is the sub-list for method input_type
|
||||
41, // [41:41] is the sub-list for extension type_name
|
||||
41, // [41:41] is the sub-list for extension extendee
|
||||
0, // [0:41] is the sub-list for field type_name
|
||||
12, // 16: olivetin.api.v1.LogEntry.arguments:type_name -> olivetin.api.v1.StartActionArgument
|
||||
21, // 17: olivetin.api.v1.GetLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry
|
||||
21, // 18: olivetin.api.v1.GetActionLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry
|
||||
21, // 19: olivetin.api.v1.ExecutionQueueAction.entries:type_name -> olivetin.api.v1.LogEntry
|
||||
26, // 20: olivetin.api.v1.ExecutionQueueGroup.actions:type_name -> olivetin.api.v1.ExecutionQueueAction
|
||||
27, // 21: olivetin.api.v1.GetExecutionQueueResponse.groups:type_name -> olivetin.api.v1.ExecutionQueueGroup
|
||||
21, // 22: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||
34, // 23: olivetin.api.v1.ExecutionStatusResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
|
||||
80, // 24: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry
|
||||
81, // 25: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
|
||||
50, // 26: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged
|
||||
51, // 27: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged
|
||||
53, // 28: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished
|
||||
54, // 29: olivetin.api.v1.EventStreamResponse.execution_started:type_name -> olivetin.api.v1.EventExecutionStarted
|
||||
49, // 30: olivetin.api.v1.EventStreamResponse.output_chunk:type_name -> olivetin.api.v1.EventOutputChunk
|
||||
52, // 31: olivetin.api.v1.EventStreamResponse.heartbeat:type_name -> olivetin.api.v1.EventHeartbeat
|
||||
21, // 32: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||
21, // 33: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||
68, // 34: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider
|
||||
67, // 35: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink
|
||||
7, // 36: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy
|
||||
0, // 37: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
|
||||
34, // 38: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
|
||||
73, // 39: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
|
||||
5, // 40: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
|
||||
42, // 41: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding
|
||||
8, // 42: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
|
||||
11, // 43: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
|
||||
14, // 44: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
|
||||
16, // 45: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest
|
||||
18, // 46: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest
|
||||
75, // 47: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest
|
||||
55, // 48: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest
|
||||
33, // 49: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest
|
||||
20, // 50: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest
|
||||
23, // 51: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest
|
||||
25, // 52: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest
|
||||
29, // 53: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest
|
||||
36, // 54: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest
|
||||
38, // 55: olivetin.api.v1.OliveTinApiService.SosReport:input_type -> olivetin.api.v1.SosReportRequest
|
||||
40, // 56: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest
|
||||
43, // 57: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest
|
||||
45, // 58: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest
|
||||
57, // 59: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest
|
||||
59, // 60: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest
|
||||
61, // 61: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest
|
||||
47, // 62: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest
|
||||
63, // 63: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest
|
||||
65, // 64: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest
|
||||
69, // 65: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest
|
||||
71, // 66: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest
|
||||
74, // 67: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest
|
||||
6, // 68: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse
|
||||
13, // 69: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse
|
||||
15, // 70: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse
|
||||
17, // 71: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse
|
||||
19, // 72: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse
|
||||
13, // 73: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse
|
||||
56, // 74: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse
|
||||
35, // 75: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse
|
||||
22, // 76: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse
|
||||
24, // 77: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse
|
||||
28, // 78: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse
|
||||
30, // 79: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse
|
||||
37, // 80: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse
|
||||
39, // 81: olivetin.api.v1.OliveTinApiService.SosReport:output_type -> olivetin.api.v1.SosReportResponse
|
||||
41, // 82: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse
|
||||
44, // 83: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse
|
||||
46, // 84: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse
|
||||
58, // 85: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse
|
||||
60, // 86: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse
|
||||
62, // 87: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse
|
||||
48, // 88: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse
|
||||
64, // 89: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse
|
||||
66, // 90: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse
|
||||
70, // 91: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse
|
||||
72, // 92: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse
|
||||
5, // 93: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity
|
||||
68, // [68:94] is the sub-list for method output_type
|
||||
42, // [42:68] is the sub-list for method input_type
|
||||
42, // [42:42] is the sub-list for extension type_name
|
||||
42, // [42:42] is the sub-list for extension extendee
|
||||
0, // [0:42] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_olivetin_api_v1_olivetin_proto_init() }
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
|
@ -388,6 +389,7 @@ func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry
|
|||
BindingId: logEntry.GetBindingId(),
|
||||
DatetimeRateLimitExpires: calculateRateLimitExpires(api, logEntry),
|
||||
Justification: logEntry.Justification,
|
||||
Arguments: logEntryArgumentsToProto(logEntry.Arguments),
|
||||
}
|
||||
|
||||
if !pble.ExecutionFinished && logEntry.Binding != nil && logEntry.Binding.Action != nil {
|
||||
|
|
@ -1548,14 +1550,15 @@ func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if execReqLogEntry.Binding.Action.Justification {
|
||||
if execReqLogEntry.Binding.Action.Justification && strings.TrimSpace(execReqLogEntry.Justification) == "" {
|
||||
return nil, restartRequiresJustificationError()
|
||||
}
|
||||
|
||||
authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
execReq := executor.ExecutionRequest{
|
||||
Binding: execReqLogEntry.Binding,
|
||||
Arguments: make(map[string]string),
|
||||
Arguments: copyStringMap(execReqLogEntry.Arguments),
|
||||
Justification: execReqLogEntry.Justification,
|
||||
AuthenticatedUser: authenticatedUser,
|
||||
Cfg: api.cfg,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||
)
|
||||
|
||||
func logEntryArgumentsToProto(args map[string]string) []*apiv1.StartActionArgument {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(args))
|
||||
for name := range args {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
out := make([]*apiv1.StartActionArgument, 0, len(names))
|
||||
for _, name := range names {
|
||||
out = append(out, &apiv1.StartActionArgument{
|
||||
Name: name,
|
||||
Value: args[name],
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func copyStringMap(source map[string]string) map[string]string {
|
||||
copied := make(map[string]string, len(source))
|
||||
for key, value := range source {
|
||||
copied[key] = value
|
||||
}
|
||||
|
||||
return copied
|
||||
}
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
)
|
||||
|
||||
func argumentAction(title, shell string, args []config.ActionArgument) *config.Action {
|
||||
return &config.Action{
|
||||
Title: title,
|
||||
Shell: shell,
|
||||
MaxConcurrent: 1,
|
||||
Arguments: args,
|
||||
}
|
||||
}
|
||||
|
||||
func waitForLogArguments(t *testing.T, ex *executor.Executor, trackingID string) map[string]string {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
entry, ok := ex.GetLog(trackingID)
|
||||
if ok && entry.Arguments != nil {
|
||||
return entry.Arguments
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
|
||||
t.Fatalf("timed out waiting for arguments on log %s", trackingID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestExecutionStatusIncludesStoredArguments(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Actions = []*config.Action{
|
||||
argumentAction("Ping host", "echo {{ host }}", []config.ActionArgument{
|
||||
{Name: "host", Type: "ascii_identifier"},
|
||||
}),
|
||||
}
|
||||
|
||||
ex := executor.DefaultExecutor(cfg)
|
||||
ex.RebuildActionMap()
|
||||
binding := ex.FindBindingWithNoEntity(cfg.Actions[0])
|
||||
require.NotNil(t, binding)
|
||||
|
||||
ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
|
||||
defer ts.Close()
|
||||
|
||||
startResp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
|
||||
BindingId: binding.ID,
|
||||
Arguments: []*apiv1.StartActionArgument{
|
||||
{Name: "host", Value: "example.com"},
|
||||
},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
|
||||
statusResp, err := client.ExecutionStatus(context.Background(), connect.NewRequest(&apiv1.ExecutionStatusRequest{
|
||||
ExecutionTrackingId: startResp.Msg.ExecutionTrackingId,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, statusResp.Msg.LogEntry)
|
||||
require.Len(t, statusResp.Msg.LogEntry.Arguments, 1)
|
||||
assert.Equal(t, "host", statusResp.Msg.LogEntry.Arguments[0].Name)
|
||||
assert.Equal(t, "example.com", statusResp.Msg.LogEntry.Arguments[0].Value)
|
||||
}
|
||||
|
||||
func TestExecutionStatusOmitsPasswordArguments(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Actions = []*config.Action{
|
||||
{
|
||||
Title: "Connect",
|
||||
Exec: []string{"echo", "{{ user }}"},
|
||||
MaxConcurrent: 1,
|
||||
Arguments: []config.ActionArgument{
|
||||
{Name: "user", Type: "ascii_identifier"},
|
||||
{Name: "pass", Type: "password"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ex := executor.DefaultExecutor(cfg)
|
||||
ex.RebuildActionMap()
|
||||
binding := ex.FindBindingWithNoEntity(cfg.Actions[0])
|
||||
require.NotNil(t, binding)
|
||||
|
||||
ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
|
||||
defer ts.Close()
|
||||
|
||||
startResp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
|
||||
BindingId: binding.ID,
|
||||
Arguments: []*apiv1.StartActionArgument{
|
||||
{Name: "user", Value: "alice"},
|
||||
{Name: "pass", Value: "secret"},
|
||||
},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
|
||||
statusResp, err := client.ExecutionStatus(context.Background(), connect.NewRequest(&apiv1.ExecutionStatusRequest{
|
||||
ExecutionTrackingId: startResp.Msg.ExecutionTrackingId,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, statusResp.Msg.LogEntry)
|
||||
|
||||
for _, arg := range statusResp.Msg.LogEntry.Arguments {
|
||||
assert.NotEqual(t, "pass", arg.Name)
|
||||
}
|
||||
|
||||
require.Len(t, statusResp.Msg.LogEntry.Arguments, 1)
|
||||
assert.Equal(t, "user", statusResp.Msg.LogEntry.Arguments[0].Name)
|
||||
assert.Equal(t, "alice", statusResp.Msg.LogEntry.Arguments[0].Value)
|
||||
}
|
||||
|
||||
func TestRestartActionReusesStoredArguments(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Actions = []*config.Action{
|
||||
argumentAction("Ping host", "echo {{ host }}", []config.ActionArgument{
|
||||
{Name: "host", Type: "ascii_identifier"},
|
||||
}),
|
||||
}
|
||||
|
||||
ex := executor.DefaultExecutor(cfg)
|
||||
ex.RebuildActionMap()
|
||||
binding := ex.FindBindingWithNoEntity(cfg.Actions[0])
|
||||
require.NotNil(t, binding)
|
||||
|
||||
ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
|
||||
defer ts.Close()
|
||||
|
||||
startResp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
|
||||
BindingId: binding.ID,
|
||||
Arguments: []*apiv1.StartActionArgument{
|
||||
{Name: "host", Value: "server-a"},
|
||||
},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
|
||||
originalArgs := waitForLogArguments(t, ex, startResp.Msg.ExecutionTrackingId)
|
||||
assert.Equal(t, "server-a", originalArgs["host"])
|
||||
|
||||
restartResp, err := client.RestartAction(context.Background(), connect.NewRequest(&apiv1.RestartActionRequest{
|
||||
ExecutionTrackingId: startResp.Msg.ExecutionTrackingId,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, restartResp.Msg.ExecutionTrackingId)
|
||||
assert.NotEqual(t, startResp.Msg.ExecutionTrackingId, restartResp.Msg.ExecutionTrackingId)
|
||||
|
||||
restartedArgs := waitForLogArguments(t, ex, restartResp.Msg.ExecutionTrackingId)
|
||||
assert.Equal(t, "server-a", restartedArgs["host"])
|
||||
}
|
||||
|
||||
func TestLogEntryArgumentsToProto(t *testing.T) {
|
||||
assert.Nil(t, logEntryArgumentsToProto(nil))
|
||||
assert.Nil(t, logEntryArgumentsToProto(map[string]string{}))
|
||||
|
||||
out := logEntryArgumentsToProto(map[string]string{
|
||||
"host": "example.com",
|
||||
"port": "443",
|
||||
})
|
||||
require.Len(t, out, 2)
|
||||
|
||||
values := map[string]string{}
|
||||
for _, arg := range out {
|
||||
values[arg.Name] = arg.Value
|
||||
}
|
||||
|
||||
assert.Equal(t, "example.com", values["host"])
|
||||
assert.Equal(t, "443", values["port"])
|
||||
}
|
||||
|
||||
func TestCopyStringMap(t *testing.T) {
|
||||
source := map[string]string{"host": "example.com"}
|
||||
copied := copyStringMap(source)
|
||||
|
||||
assert.Equal(t, source, copied)
|
||||
source["host"] = "changed"
|
||||
assert.Equal(t, "example.com", copied["host"])
|
||||
|
||||
empty := copyStringMap(nil)
|
||||
assert.NotNil(t, empty)
|
||||
assert.Empty(t, empty)
|
||||
}
|
||||
|
||||
func TestRestartActionRequiresJustificationWhenMissingFromStoredLog(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Actions = []*config.Action{
|
||||
{
|
||||
Title: "Dangerous action",
|
||||
Shell: "echo ok",
|
||||
MaxConcurrent: 1,
|
||||
Justification: true,
|
||||
},
|
||||
}
|
||||
|
||||
ex := executor.DefaultExecutor(cfg)
|
||||
ex.RebuildActionMap()
|
||||
binding := ex.FindBindingWithNoEntity(cfg.Actions[0])
|
||||
require.NotNil(t, binding)
|
||||
|
||||
trackingID := "manual-log-without-justification"
|
||||
ex.SetLog(trackingID, &executor.InternalLogEntry{
|
||||
Binding: binding,
|
||||
ExecutionFinished: true,
|
||||
ExecutionTrackingID: trackingID,
|
||||
})
|
||||
|
||||
ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
|
||||
defer ts.Close()
|
||||
|
||||
_, err := client.RestartAction(context.Background(), connect.NewRequest(&apiv1.RestartActionRequest{
|
||||
ExecutionTrackingId: trackingID,
|
||||
}))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "justification")
|
||||
}
|
||||
|
||||
func TestRestartActionReusesStoredJustificationViaStartActionPath(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Actions = []*config.Action{
|
||||
{
|
||||
Title: "Dangerous action",
|
||||
Shell: "echo ok",
|
||||
MaxConcurrent: 1,
|
||||
Justification: true,
|
||||
},
|
||||
}
|
||||
|
||||
ex := executor.DefaultExecutor(cfg)
|
||||
ex.RebuildActionMap()
|
||||
binding := ex.FindBindingWithNoEntity(cfg.Actions[0])
|
||||
require.NotNil(t, binding)
|
||||
|
||||
ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
|
||||
defer ts.Close()
|
||||
|
||||
startResp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
|
||||
BindingId: binding.ID,
|
||||
Justification: "maintenance window",
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
|
||||
originalLog, ok := ex.GetLog(startResp.Msg.ExecutionTrackingId)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "maintenance window", originalLog.Justification)
|
||||
|
||||
restartResp, err := client.RestartAction(context.Background(), connect.NewRequest(&apiv1.RestartActionRequest{
|
||||
ExecutionTrackingId: startResp.Msg.ExecutionTrackingId,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
|
||||
restartedLog, ok := ex.GetLog(restartResp.Msg.ExecutionTrackingId)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "maintenance window", restartedLog.Justification)
|
||||
}
|
||||
|
||||
func TestGetLogsIncludesStoredArguments(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Actions = []*config.Action{
|
||||
argumentAction("Ping host", "echo {{ host }}", []config.ActionArgument{
|
||||
{Name: "host", Type: "ascii_identifier"},
|
||||
}),
|
||||
}
|
||||
|
||||
ex := executor.DefaultExecutor(cfg)
|
||||
ex.RebuildActionMap()
|
||||
binding := ex.FindBindingWithNoEntity(cfg.Actions[0])
|
||||
require.NotNil(t, binding)
|
||||
|
||||
ts, client := getNewTestServerAndClientWithExecutor(cfg, ex)
|
||||
defer ts.Close()
|
||||
|
||||
startResp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{
|
||||
BindingId: binding.ID,
|
||||
Arguments: []*apiv1.StartActionArgument{
|
||||
{Name: "host", Value: "db-1"},
|
||||
},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, startResp.Msg.ExecutionTrackingId)
|
||||
|
||||
logsResp, err := client.GetLogs(context.Background(), connect.NewRequest(&apiv1.GetLogsRequest{}))
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, logsResp.Msg.Logs)
|
||||
|
||||
var matched bool
|
||||
for _, entry := range logsResp.Msg.Logs {
|
||||
if entry.ExecutionTrackingId != startResp.Msg.ExecutionTrackingId {
|
||||
continue
|
||||
}
|
||||
|
||||
matched = true
|
||||
require.Len(t, entry.Arguments, 1)
|
||||
assert.Equal(t, "host", entry.Arguments[0].Name)
|
||||
assert.Equal(t, "db-1", entry.Arguments[0].Value)
|
||||
}
|
||||
|
||||
assert.True(t, matched, "expected log entry with stored arguments")
|
||||
}
|
||||
|
|
@ -171,6 +171,7 @@ type InternalLogEntry struct {
|
|||
ActionTitle string
|
||||
ActionIcon string
|
||||
Justification string
|
||||
Arguments map[string]string
|
||||
}
|
||||
|
||||
// .Binding can be nil, so we need to handle that.
|
||||
|
|
@ -863,6 +864,7 @@ func stepParseArgs(req *ExecutionRequest) bool {
|
|||
return fail(req, err)
|
||||
}
|
||||
mangleInvalidArgumentValues(req)
|
||||
copyStorableArgumentsToLogEntry(req)
|
||||
|
||||
if hasExec(req) {
|
||||
return handleExecBranch(req)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package executor
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
)
|
||||
|
||||
func argumentTypeStorableInLog(argType string) bool {
|
||||
switch argType {
|
||||
case "password", "very_dangerous_raw_string":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func storableArgumentNames(action *config.Action) map[string]struct{} {
|
||||
if action == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := make(map[string]struct{}, len(action.Arguments))
|
||||
for i := range action.Arguments {
|
||||
arg := &action.Arguments[i]
|
||||
if !argumentTypeStorableInLog(arg.Type) {
|
||||
continue
|
||||
}
|
||||
|
||||
names[arg.Name] = struct{}{}
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func storableArgumentNamesFromRequest(req *ExecutionRequest) map[string]struct{} {
|
||||
if req == nil || req.Binding == nil || req.Binding.Action == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return storableArgumentNames(req.Binding.Action)
|
||||
}
|
||||
|
||||
func isStorableArgumentName(name string, allowedNames map[string]struct{}) bool {
|
||||
if strings.HasPrefix(name, config.ReservedArgumentNamePrefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := allowedNames[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func collectStorableArguments(args map[string]string, allowedNames map[string]struct{}) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for name, value := range args {
|
||||
if isStorableArgumentName(name, allowedNames) {
|
||||
result[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func filterStorableArguments(args map[string]string, allowedNames map[string]struct{}) map[string]string {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := collectStorableArguments(args, allowedNames)
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func storableArgumentsFromRequest(req *ExecutionRequest) map[string]string {
|
||||
allowedNames := storableArgumentNamesFromRequest(req)
|
||||
if len(allowedNames) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return filterStorableArguments(req.Arguments, allowedNames)
|
||||
}
|
||||
|
||||
func copyStorableArgumentsToLogEntry(req *ExecutionRequest) {
|
||||
args := storableArgumentsFromRequest(req)
|
||||
if args == nil || req.logEntry == nil {
|
||||
return
|
||||
}
|
||||
|
||||
req.mutateLogEntry(func(entry *InternalLogEntry) {
|
||||
entry.Arguments = args
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package executor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
auth "github.com/OliveTin/OliveTin/internal/auth"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
)
|
||||
|
||||
func TestArgumentTypeStorableInLog(t *testing.T) {
|
||||
assert.True(t, argumentTypeStorableInLog("ascii"))
|
||||
assert.True(t, argumentTypeStorableInLog("shell_safe_identifier"))
|
||||
assert.False(t, argumentTypeStorableInLog("password"))
|
||||
assert.False(t, argumentTypeStorableInLog("very_dangerous_raw_string"))
|
||||
}
|
||||
|
||||
func TestStorableArgumentsFromRequestExcludesSensitiveAndSystemArgs(t *testing.T) {
|
||||
req := newExecRequest()
|
||||
req.Binding.Action.Arguments = []config.ActionArgument{
|
||||
{Name: "host", Type: "ascii_identifier"},
|
||||
{Name: "secret", Type: "password"},
|
||||
{Name: "payload", Type: "very_dangerous_raw_string"},
|
||||
}
|
||||
req.Arguments = map[string]string{
|
||||
"host": "example.com",
|
||||
"secret": "hunter2",
|
||||
"payload": "rm -rf /",
|
||||
"ot_executionTrackingId": "track-123",
|
||||
"ot_username": "alice",
|
||||
"extra_undefined": "drop-me",
|
||||
}
|
||||
|
||||
args := storableArgumentsFromRequest(req)
|
||||
|
||||
require.Len(t, args, 1)
|
||||
assert.Equal(t, "example.com", args["host"])
|
||||
assert.NotContains(t, args, "secret")
|
||||
assert.NotContains(t, args, "payload")
|
||||
assert.NotContains(t, args, "ot_executionTrackingId")
|
||||
assert.NotContains(t, args, "ot_username")
|
||||
assert.NotContains(t, args, "extra_undefined")
|
||||
}
|
||||
|
||||
func TestStorableArgumentsFromRequestReturnsNilWhenEmpty(t *testing.T) {
|
||||
req := newExecRequest()
|
||||
req.Binding.Action.Arguments = []config.ActionArgument{
|
||||
{Name: "secret", Type: "password"},
|
||||
}
|
||||
req.Arguments = map[string]string{
|
||||
"secret": "hunter2",
|
||||
}
|
||||
|
||||
assert.Nil(t, storableArgumentsFromRequest(req))
|
||||
}
|
||||
|
||||
func TestStorableArgumentsFromRequestStoresMangledCheckboxValue(t *testing.T) {
|
||||
req := newExecRequest()
|
||||
req.Binding.Action.Arguments = []config.ActionArgument{
|
||||
{
|
||||
Name: "mode",
|
||||
Type: "checkbox",
|
||||
Choices: []config.ActionArgumentChoice{
|
||||
{Title: "Enabled", Value: "1"},
|
||||
{Title: "Disabled", Value: "0"},
|
||||
},
|
||||
},
|
||||
}
|
||||
req.Arguments = map[string]string{
|
||||
"mode": "Enabled",
|
||||
}
|
||||
|
||||
mangleInvalidArgumentValues(req)
|
||||
args := storableArgumentsFromRequest(req)
|
||||
|
||||
require.Len(t, args, 1)
|
||||
assert.Equal(t, "1", args["mode"])
|
||||
}
|
||||
|
||||
func TestCopyStorableArgumentsToLogEntry(t *testing.T) {
|
||||
req := newExecRequest()
|
||||
req.logEntry = &InternalLogEntry{}
|
||||
req.Binding.Action.Arguments = []config.ActionArgument{
|
||||
{Name: "target", Type: "ascii_identifier"},
|
||||
}
|
||||
req.Arguments = map[string]string{
|
||||
"target": "server-a",
|
||||
}
|
||||
|
||||
copyStorableArgumentsToLogEntry(req)
|
||||
|
||||
require.NotNil(t, req.logEntry.Arguments)
|
||||
assert.Equal(t, "server-a", req.logEntry.Arguments["target"])
|
||||
}
|
||||
|
||||
func TestExecRequestStoresArgumentsOnLogEntry(t *testing.T) {
|
||||
e, cfg := testingExecutor()
|
||||
|
||||
e.RebuildActionMap()
|
||||
binding := e.FindBindingWithNoEntity(cfg.Actions[0])
|
||||
require.NotNil(t, binding)
|
||||
|
||||
req := ExecutionRequest{
|
||||
Binding: binding,
|
||||
Cfg: cfg,
|
||||
AuthenticatedUser: auth.UserGuest(cfg),
|
||||
Arguments: map[string]string{
|
||||
"person": "yourself",
|
||||
},
|
||||
}
|
||||
|
||||
wg, trackingID := e.ExecRequest(&req)
|
||||
wg.Wait()
|
||||
|
||||
logEntry, ok := e.GetLog(trackingID)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, logEntry.Arguments)
|
||||
assert.Equal(t, "yourself", logEntry.Arguments["person"])
|
||||
}
|
||||
Loading…
Reference in New Issue