api.go 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194
  1. package api
  2. import (
  3. ctx "context"
  4. "encoding/json"
  5. "sort"
  6. "connectrpc.com/connect"
  7. "google.golang.org/protobuf/encoding/protojson"
  8. apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
  9. apiv1connect "github.com/OliveTin/OliveTin/gen/olivetin/api/v1/apiv1connect"
  10. "github.com/google/uuid"
  11. log "github.com/sirupsen/logrus"
  12. "fmt"
  13. "net/http"
  14. "sync"
  15. acl "github.com/OliveTin/OliveTin/internal/acl"
  16. auth "github.com/OliveTin/OliveTin/internal/auth"
  17. authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
  18. config "github.com/OliveTin/OliveTin/internal/config"
  19. entities "github.com/OliveTin/OliveTin/internal/entities"
  20. executor "github.com/OliveTin/OliveTin/internal/executor"
  21. installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
  22. connectproto "go.akshayshah.org/connectproto"
  23. )
  24. type oliveTinAPI struct {
  25. executor *executor.Executor
  26. cfg *config.Config
  27. // streamingClients is a set of currently connected clients.
  28. // The empty struct value models set semantics (keys only) and keeps add/remove O(1).
  29. // We use a map for efficient membership and deletion; ordering is not required.
  30. streamingClients map[*streamingClient]struct{}
  31. streamingClientsMutex sync.RWMutex
  32. }
  33. // This is used to avoid race conditions when iterating over the connectedClients map.
  34. // and holds the lock for as minimal time as possible to avoid blocking the API for too long.
  35. func (api *oliveTinAPI) copyOfStreamingClients() []*streamingClient {
  36. api.streamingClientsMutex.RLock()
  37. defer api.streamingClientsMutex.RUnlock()
  38. clients := make([]*streamingClient, 0, len(api.streamingClients))
  39. for client := range api.streamingClients {
  40. clients = append(clients, client)
  41. }
  42. return clients
  43. }
  44. type streamingClient struct {
  45. channel chan *apiv1.EventStreamResponse
  46. AuthenticatedUser *authpublic.AuthenticatedUser
  47. }
  48. func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.KillActionRequest]) (*connect.Response[apiv1.KillActionResponse], error) {
  49. ret := &apiv1.KillActionResponse{
  50. ExecutionTrackingId: req.Msg.ExecutionTrackingId,
  51. }
  52. var execReqLogEntry *executor.InternalLogEntry
  53. execReqLogEntry, ret.Found = api.executor.GetLog(req.Msg.ExecutionTrackingId)
  54. if !ret.Found {
  55. log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId)
  56. return connect.NewResponse(ret), nil
  57. }
  58. log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId)
  59. action := execReqLogEntry.Binding.Action
  60. if action == nil {
  61. log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle)
  62. ret.Killed = false
  63. return connect.NewResponse(ret), nil
  64. }
  65. user := auth.UserFromApiCall(ctx, req, api.cfg)
  66. api.killActionByTrackingId(user, action, execReqLogEntry, ret)
  67. return connect.NewResponse(ret), nil
  68. }
  69. func (api *oliveTinAPI) killActionByTrackingId(user *authpublic.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
  70. if !acl.IsAllowedKill(api.cfg, user, action) {
  71. log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
  72. ret.Killed = false
  73. return
  74. }
  75. err := api.executor.Kill(execReqLogEntry)
  76. if err != nil {
  77. log.Warnf("Killing execution request err: %v", err)
  78. ret.AlreadyCompleted = true
  79. ret.Killed = false
  80. } else {
  81. ret.Killed = true
  82. }
  83. }
  84. func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *connect.Request[apiv1.StartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {
  85. args := make(map[string]string)
  86. for _, arg := range req.Msg.Arguments {
  87. args[arg.Name] = arg.Value
  88. }
  89. pair := api.executor.FindBindingByID(req.Msg.BindingId)
  90. if pair == nil || pair.Action == nil {
  91. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.BindingId))
  92. }
  93. authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg)
  94. execReq := executor.ExecutionRequest{
  95. Binding: pair,
  96. TrackingID: req.Msg.UniqueTrackingId,
  97. Arguments: args,
  98. AuthenticatedUser: authenticatedUser,
  99. Cfg: api.cfg,
  100. }
  101. api.executor.ExecRequest(&execReq)
  102. ret := &apiv1.StartActionResponse{
  103. ExecutionTrackingId: execReq.TrackingID,
  104. }
  105. return connect.NewResponse(ret), nil
  106. }
  107. func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1.PasswordHashRequest]) (*connect.Response[apiv1.PasswordHashResponse], error) {
  108. hash, err := createHash(req.Msg.Password)
  109. if err != nil {
  110. return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("error creating hash: %w", err))
  111. }
  112. ret := &apiv1.PasswordHashResponse{
  113. Hash: hash,
  114. }
  115. return connect.NewResponse(ret), nil
  116. }
  117. func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) {
  118. // Check if local user authentication is enabled
  119. if !api.cfg.AuthLocalUsers.Enabled {
  120. return connect.NewResponse(&apiv1.LocalUserLoginResponse{
  121. Success: false,
  122. }), nil
  123. }
  124. match := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password)
  125. response := connect.NewResponse(&apiv1.LocalUserLoginResponse{
  126. Success: match,
  127. })
  128. if match {
  129. // Set authentication cookie for successful login
  130. user := api.cfg.FindUserByUsername(req.Msg.Username)
  131. if user != nil {
  132. sid := uuid.NewString()
  133. // Register the session in the session storage
  134. auth.RegisterUserSession(api.cfg, "local", sid, user.Username)
  135. log.WithFields(log.Fields{
  136. "username": user.Username,
  137. }).Info("LocalUserLogin: Session created and registered")
  138. // Set the authentication cookie in the response headers
  139. cookie := &http.Cookie{
  140. Name: "olivetin-sid-local",
  141. Value: sid,
  142. MaxAge: 31556952, // 1 year
  143. HttpOnly: true,
  144. Path: "/",
  145. }
  146. response.Header().Set("Set-Cookie", cookie.String())
  147. }
  148. log.WithFields(log.Fields{
  149. "username": req.Msg.Username,
  150. }).Info("LocalUserLogin: User logged in successfully.")
  151. } else {
  152. log.WithFields(log.Fields{
  153. "username": req.Msg.Username,
  154. }).Warn("LocalUserLogin: User login failed.")
  155. }
  156. return response, nil
  157. }
  158. func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) {
  159. args := make(map[string]string)
  160. for _, arg := range req.Msg.Arguments {
  161. args[arg.Name] = arg.Value
  162. }
  163. user := auth.UserFromApiCall(ctx, req, api.cfg)
  164. execReq := executor.ExecutionRequest{
  165. Binding: api.executor.FindBindingByID(req.Msg.ActionId),
  166. TrackingID: uuid.NewString(),
  167. Arguments: args,
  168. AuthenticatedUser: user,
  169. Cfg: api.cfg,
  170. }
  171. wg, _ := api.executor.ExecRequest(&execReq)
  172. wg.Wait()
  173. internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
  174. if ok {
  175. return connect.NewResponse(&apiv1.StartActionAndWaitResponse{
  176. LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
  177. }), nil
  178. } else {
  179. return nil, fmt.Errorf("execution not found")
  180. }
  181. }
  182. func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetRequest]) (*connect.Response[apiv1.StartActionByGetResponse], error) {
  183. args := make(map[string]string)
  184. execReq := executor.ExecutionRequest{
  185. Binding: api.executor.FindBindingByID(req.Msg.ActionId),
  186. TrackingID: uuid.NewString(),
  187. Arguments: args,
  188. AuthenticatedUser: auth.UserFromApiCall(ctx, req, api.cfg),
  189. Cfg: api.cfg,
  190. }
  191. _, uniqueTrackingId := api.executor.ExecRequest(&execReq)
  192. return connect.NewResponse(&apiv1.StartActionByGetResponse{
  193. ExecutionTrackingId: uniqueTrackingId,
  194. }), nil
  195. }
  196. func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
  197. args := make(map[string]string)
  198. user := auth.UserFromApiCall(ctx, req, api.cfg)
  199. execReq := executor.ExecutionRequest{
  200. Binding: api.executor.FindBindingByID(req.Msg.ActionId),
  201. TrackingID: uuid.NewString(),
  202. Arguments: args,
  203. AuthenticatedUser: user,
  204. Cfg: api.cfg,
  205. }
  206. wg, _ := api.executor.ExecRequest(&execReq)
  207. wg.Wait()
  208. internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
  209. if ok {
  210. return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{
  211. LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
  212. }), nil
  213. } else {
  214. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
  215. }
  216. }
  217. func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *authpublic.AuthenticatedUser) *apiv1.LogEntry {
  218. pble := &apiv1.LogEntry{
  219. ActionTitle: logEntry.ActionTitle,
  220. ActionIcon: logEntry.ActionIcon,
  221. ActionId: logEntry.ActionId,
  222. DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
  223. DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
  224. DatetimeIndex: logEntry.Index,
  225. Output: logEntry.Output,
  226. TimedOut: logEntry.TimedOut,
  227. Blocked: logEntry.Blocked,
  228. ExitCode: logEntry.ExitCode,
  229. Tags: logEntry.Tags,
  230. ExecutionTrackingId: logEntry.ExecutionTrackingID,
  231. ExecutionStarted: logEntry.ExecutionStarted,
  232. ExecutionFinished: logEntry.ExecutionFinished,
  233. User: logEntry.Username,
  234. }
  235. if !pble.ExecutionFinished {
  236. pble.CanKill = acl.IsAllowedKill(api.cfg, authenticatedUser, logEntry.Binding.Action)
  237. }
  238. return pble
  239. }
  240. func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
  241. logEntry, ok := api.executor.GetLog(executionTrackingId)
  242. if !ok {
  243. return nil
  244. }
  245. return logEntry
  246. }
  247. func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
  248. var ile *executor.InternalLogEntry
  249. logs := api.executor.GetLogsByActionId(actionId)
  250. if len(logs) == 0 {
  251. return nil
  252. } else {
  253. // Get last log entry
  254. ile = logs[len(logs)-1]
  255. }
  256. return ile
  257. }
  258. func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) {
  259. res := &apiv1.ExecutionStatusResponse{}
  260. user := auth.UserFromApiCall(ctx, req, api.cfg)
  261. if err := api.checkDashboardAccess(user); err != nil {
  262. return nil, err
  263. }
  264. var ile *executor.InternalLogEntry
  265. if req.Msg.ExecutionTrackingId != "" {
  266. ile = getExecutionStatusByTrackingID(api, req.Msg.ExecutionTrackingId)
  267. } else {
  268. ile = getMostRecentExecutionStatusById(api, req.Msg.ActionId)
  269. }
  270. if ile == nil {
  271. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s or action ID %s", req.Msg.ExecutionTrackingId, req.Msg.ActionId))
  272. } else {
  273. res.LogEntry = api.internalLogEntryToPb(ile, user)
  274. }
  275. return connect.NewResponse(res), nil
  276. }
  277. func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
  278. user := auth.UserFromApiCall(ctx, req, api.cfg)
  279. log.WithFields(log.Fields{
  280. "username": user.Username,
  281. "provider": user.Provider,
  282. }).Info("Logout: User logged out")
  283. response := connect.NewResponse(&apiv1.LogoutResponse{})
  284. // Clear the local authentication cookie by setting it to expire
  285. localCookie := &http.Cookie{
  286. Name: "olivetin-sid-local",
  287. Value: "",
  288. MaxAge: -1, // This tells the browser to delete the cookie
  289. HttpOnly: true,
  290. Path: "/",
  291. }
  292. response.Header().Set("Set-Cookie", localCookie.String())
  293. // Clear the OAuth2 authentication cookie by setting it to expire
  294. oauth2Cookie := &http.Cookie{
  295. Name: "olivetin-sid-oauth",
  296. Value: "",
  297. MaxAge: -1, // This tells the browser to delete the cookie
  298. HttpOnly: true,
  299. Path: "/",
  300. }
  301. response.Header().Add("Set-Cookie", oauth2Cookie.String())
  302. return response, nil
  303. }
  304. func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[apiv1.GetActionBindingRequest]) (*connect.Response[apiv1.GetActionBindingResponse], error) {
  305. user := auth.UserFromApiCall(ctx, req, api.cfg)
  306. if err := api.checkDashboardAccess(user); err != nil {
  307. return nil, err
  308. }
  309. binding := api.executor.FindBindingByID(req.Msg.BindingId)
  310. if binding == nil {
  311. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.BindingId))
  312. }
  313. return connect.NewResponse(&apiv1.GetActionBindingResponse{
  314. Action: buildAction(binding, &DashboardRenderRequest{
  315. cfg: api.cfg,
  316. AuthenticatedUser: user,
  317. ex: api.executor,
  318. }),
  319. }), nil
  320. }
  321. func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardRequest]) (*connect.Response[apiv1.GetDashboardResponse], error) {
  322. user := auth.UserFromApiCall(ctx, req, api.cfg)
  323. if err := api.checkDashboardAccess(user); err != nil {
  324. return nil, err
  325. }
  326. entityType := ""
  327. entityKey := ""
  328. if req.Msg != nil {
  329. entityType = req.Msg.EntityType
  330. entityKey = req.Msg.EntityKey
  331. }
  332. dashboardRenderRequest := api.createDashboardRenderRequest(user, entityType, entityKey)
  333. if api.isDefaultDashboard(req.Msg.Title) {
  334. return api.buildDefaultDashboardResponse(dashboardRenderRequest)
  335. }
  336. return api.buildCustomDashboardResponse(dashboardRenderRequest, req.Msg.Title)
  337. }
  338. func (api *oliveTinAPI) checkDashboardAccess(user *authpublic.AuthenticatedUser) error {
  339. if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin {
  340. return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard"))
  341. }
  342. return nil
  343. }
  344. func (api *oliveTinAPI) createDashboardRenderRequest(user *authpublic.AuthenticatedUser, entityType, entityKey string) *DashboardRenderRequest {
  345. return &DashboardRenderRequest{
  346. AuthenticatedUser: user,
  347. cfg: api.cfg,
  348. ex: api.executor,
  349. EntityType: entityType,
  350. EntityKey: entityKey,
  351. }
  352. }
  353. func (api *oliveTinAPI) isDefaultDashboard(title string) bool {
  354. return title == "default" || title == "" || title == "Actions"
  355. }
  356. func (api *oliveTinAPI) buildDefaultDashboardResponse(rr *DashboardRenderRequest) (*connect.Response[apiv1.GetDashboardResponse], error) {
  357. db := buildDefaultDashboard(rr)
  358. res := &apiv1.GetDashboardResponse{
  359. Dashboard: db,
  360. }
  361. return connect.NewResponse(res), nil
  362. }
  363. func (api *oliveTinAPI) buildCustomDashboardResponse(rr *DashboardRenderRequest, title string) (*connect.Response[apiv1.GetDashboardResponse], error) {
  364. res := &apiv1.GetDashboardResponse{
  365. Dashboard: renderDashboard(rr, title),
  366. }
  367. return connect.NewResponse(res), nil
  368. }
  369. func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
  370. user := auth.UserFromApiCall(ctx, req, api.cfg)
  371. if err := api.checkDashboardAccess(user); err != nil {
  372. return nil, err
  373. }
  374. ret := &apiv1.GetLogsResponse{}
  375. logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, api.cfg.LogHistoryPageSize)
  376. for _, le := range logEntries {
  377. ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
  378. }
  379. ret.CountRemaining = paging.CountRemaining
  380. ret.PageSize = paging.PageSize
  381. ret.TotalCount = paging.TotalCount
  382. ret.StartOffset = paging.StartOffset
  383. return connect.NewResponse(ret), nil
  384. }
  385. // isValidLogEntry checks if a log entry has all required fields populated.
  386. func isValidLogEntry(e *executor.InternalLogEntry) bool {
  387. return e != nil && e.Binding != nil && e.Binding.Action != nil
  388. }
  389. // isLogEntryAllowed checks if a log entry is allowed to be viewed by the user.
  390. func (api *oliveTinAPI) isLogEntryAllowed(e *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) bool {
  391. return acl.IsAllowedLogs(api.cfg, user, e.Binding.Action)
  392. }
  393. // buildEmptyPageResponse creates a response for an empty page.
  394. func buildEmptyPageResponse(page pageInfo) *apiv1.GetActionLogsResponse {
  395. return &apiv1.GetActionLogsResponse{
  396. CountRemaining: 0,
  397. PageSize: page.size,
  398. TotalCount: page.total,
  399. StartOffset: page.start,
  400. }
  401. }
  402. // calculateReversedIndices computes the reversed indices for newest-first pagination.
  403. func calculateReversedIndices(page pageInfo, filteredLen int) (int64, int64) {
  404. startIdx := page.total - page.end
  405. endIdx := page.total - page.start
  406. if startIdx < 0 {
  407. startIdx = 0
  408. }
  409. if endIdx > int64(filteredLen) {
  410. endIdx = int64(filteredLen)
  411. }
  412. return startIdx, endIdx
  413. }
  414. // buildActionLogsResponse builds the response with paginated log entries.
  415. func (api *oliveTinAPI) buildActionLogsResponse(filtered []*executor.InternalLogEntry, page pageInfo, user *authpublic.AuthenticatedUser) *apiv1.GetActionLogsResponse {
  416. startIdx, endIdx := calculateReversedIndices(page, len(filtered))
  417. ret := &apiv1.GetActionLogsResponse{}
  418. for _, le := range filtered[startIdx:endIdx] {
  419. ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
  420. }
  421. ret.CountRemaining = page.start
  422. ret.PageSize = page.size
  423. ret.TotalCount = page.total
  424. ret.StartOffset = page.start
  425. return ret
  426. }
  427. func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv1.GetActionLogsRequest]) (*connect.Response[apiv1.GetActionLogsResponse], error) {
  428. user := auth.UserFromApiCall(ctx, req, api.cfg)
  429. if err := api.checkDashboardAccess(user); err != nil {
  430. return nil, err
  431. }
  432. filtered := api.filterLogsByACL(api.executor.GetLogsByActionId(req.Msg.ActionId), user)
  433. page := paginate(int64(len(filtered)), api.cfg.LogHistoryPageSize, req.Msg.StartOffset)
  434. if page.empty {
  435. return connect.NewResponse(buildEmptyPageResponse(page)), nil
  436. }
  437. return connect.NewResponse(api.buildActionLogsResponse(filtered, page, user)), nil
  438. }
  439. func (api *oliveTinAPI) filterLogsByACL(entries []*executor.InternalLogEntry, user *authpublic.AuthenticatedUser) []*executor.InternalLogEntry {
  440. filtered := make([]*executor.InternalLogEntry, 0, len(entries))
  441. for _, e := range entries {
  442. if !isValidLogEntry(e) {
  443. continue
  444. }
  445. if api.isLogEntryAllowed(e, user) {
  446. filtered = append(filtered, e)
  447. }
  448. }
  449. return filtered
  450. }
  451. type pageInfo struct {
  452. total int64
  453. size int64
  454. start int64
  455. end int64
  456. empty bool
  457. }
  458. func paginate(total int64, size int64, start int64) pageInfo {
  459. if start < 0 {
  460. start = 0
  461. }
  462. if start >= total {
  463. return pageInfo{total: total, size: size, start: start, end: start, empty: true}
  464. }
  465. end := start + size
  466. if end > total {
  467. end = total
  468. }
  469. return pageInfo{total: total, size: size, start: start, end: end, empty: false}
  470. }
  471. /*
  472. This function is ONLY a helper for the UI - the arguments are validated properly
  473. on the StartAction -> Executor chain. This is here basically to provide helpful
  474. error messages more quickly before starting the action.
  475. It uses the same validation logic as the executor, including mangling argument
  476. values (e.g., datetime formatting, checkbox title-to-value conversion).
  477. */
  478. func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) {
  479. err := api.validateArgumentTypeInternal(req.Msg)
  480. desc := ""
  481. if err != nil {
  482. desc = err.Error()
  483. }
  484. return connect.NewResponse(&apiv1.ValidateArgumentTypeResponse{
  485. Valid: err == nil,
  486. Description: desc,
  487. }), nil
  488. }
  489. func (api *oliveTinAPI) validateArgumentTypeInternal(msg *apiv1.ValidateArgumentTypeRequest) error {
  490. if msg.BindingId == "" || msg.ArgumentName == "" {
  491. return executor.TypeSafetyCheck("", msg.Value, msg.Type)
  492. }
  493. arg, action := api.findArgumentForValidation(msg.BindingId, msg.ArgumentName)
  494. if arg == nil {
  495. return fmt.Errorf("argument not found")
  496. }
  497. return executor.ValidateArgument(arg, msg.Value, action)
  498. }
  499. func (api *oliveTinAPI) findArgumentForValidation(bindingId string, argumentName string) (*config.ActionArgument, *config.Action) {
  500. binding := api.executor.FindBindingByID(bindingId)
  501. if binding == nil || binding.Action == nil {
  502. return nil, nil
  503. }
  504. arg := api.findArgumentByName(binding.Action, argumentName)
  505. return arg, binding.Action
  506. }
  507. func (api *oliveTinAPI) findArgumentByName(action *config.Action, name string) *config.ActionArgument {
  508. for i := range action.Arguments {
  509. if action.Arguments[i].Name == name {
  510. return &action.Arguments[i]
  511. }
  512. }
  513. return nil
  514. }
  515. func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAmIRequest]) (*connect.Response[apiv1.WhoAmIResponse], error) {
  516. user := auth.UserFromApiCall(ctx, req, api.cfg)
  517. if err := api.checkDashboardAccess(user); err != nil {
  518. return nil, err
  519. }
  520. res := &apiv1.WhoAmIResponse{
  521. AuthenticatedUser: user.Username,
  522. Usergroup: user.UsergroupLine,
  523. Provider: user.Provider,
  524. Sid: user.SID,
  525. Acls: user.Acls,
  526. }
  527. return connect.NewResponse(res), nil
  528. }
  529. func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *connect.Request[apiv1.SosReportRequest]) (*connect.Response[apiv1.SosReportResponse], error) {
  530. sos := installationinfo.GetSosReport()
  531. if !api.cfg.InsecureAllowDumpSos {
  532. log.Info(sos)
  533. sos = "Your SOS Report has been logged to OliveTin logs.\n\nIf you are in a safe network, you can temporarily set `insecureAllowDumpSos: true` in your config.yaml, restart OliveTin, and refresh this page - it will put the output directly in the browser."
  534. }
  535. ret := &apiv1.SosReportResponse{
  536. Alert: sos,
  537. }
  538. return connect.NewResponse(ret), nil
  539. }
  540. func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.DumpVarsRequest]) (*connect.Response[apiv1.DumpVarsResponse], error) {
  541. res := &apiv1.DumpVarsResponse{}
  542. if !api.cfg.InsecureAllowDumpVars {
  543. res.Alert = "Dumping variables is not allowed by default because it is insecure."
  544. return connect.NewResponse(res), nil
  545. }
  546. jsonstring, _ := json.MarshalIndent(entities.GetAll(), "", " ")
  547. fmt.Printf("%s", &jsonstring)
  548. res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
  549. return connect.NewResponse(res), nil
  550. }
  551. func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) {
  552. res := &apiv1.DumpPublicIdActionMapResponse{}
  553. res.Contents = make(map[string]*apiv1.ActionEntityPair)
  554. if !api.cfg.InsecureAllowDumpActionMap {
  555. res.Alert = "Dumping Public IDs is disallowed."
  556. return connect.NewResponse(res), nil
  557. }
  558. api.executor.MapActionIdToBindingLock.RLock()
  559. for k, v := range api.executor.MapActionIdToBinding {
  560. res.Contents[k] = &apiv1.ActionEntityPair{
  561. ActionTitle: v.Action.Title,
  562. EntityPrefix: "?",
  563. }
  564. }
  565. api.executor.MapActionIdToBindingLock.RUnlock()
  566. res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
  567. return connect.NewResponse(res), nil
  568. }
  569. func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *connect.Request[apiv1.GetReadyzRequest]) (*connect.Response[apiv1.GetReadyzResponse], error) {
  570. res := &apiv1.GetReadyzResponse{
  571. Status: "OK",
  572. }
  573. return connect.NewResponse(res), nil
  574. }
  575. func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.EventStreamRequest], srv *connect.ServerStream[apiv1.EventStreamResponse]) error {
  576. log.Debugf("EventStream: %v", req.Msg)
  577. user := auth.UserFromApiCall(ctx, req, api.cfg)
  578. if err := api.checkDashboardAccess(user); err != nil {
  579. return err
  580. }
  581. client := &streamingClient{
  582. channel: make(chan *apiv1.EventStreamResponse, 10), // Buffered channel to hold Events
  583. AuthenticatedUser: user,
  584. }
  585. log.WithFields(log.Fields{
  586. "authenticatedUser": user.Username,
  587. }).Debugf("EventStream: client connected")
  588. api.streamingClientsMutex.Lock()
  589. api.streamingClients[client] = struct{}{}
  590. api.streamingClientsMutex.Unlock()
  591. // loop over client channel and send events to connectedClient
  592. for msg := range client.channel {
  593. log.Debugf("Sending event to client: %v", msg)
  594. if err := srv.Send(msg); err != nil {
  595. log.Errorf("Error sending event to client: %v", err)
  596. // Remove disconnected client from the list
  597. api.removeClient(client)
  598. break
  599. }
  600. }
  601. log.Infof("EventStream: client disconnected")
  602. return nil
  603. }
  604. func (api *oliveTinAPI) removeClient(clientToRemove *streamingClient) {
  605. api.streamingClientsMutex.Lock()
  606. delete(api.streamingClients, clientToRemove)
  607. api.streamingClientsMutex.Unlock()
  608. close(clientToRemove.channel)
  609. }
  610. func (api *oliveTinAPI) OnActionMapRebuilt() {
  611. toRemove := []*streamingClient{}
  612. for _, client := range api.copyOfStreamingClients() {
  613. select {
  614. case client.channel <- &apiv1.EventStreamResponse{
  615. Event: &apiv1.EventStreamResponse_ConfigChanged{
  616. ConfigChanged: &apiv1.EventConfigChanged{},
  617. },
  618. }:
  619. default:
  620. log.Warnf("EventStream: client channel is full, removing client")
  621. toRemove = append(toRemove, client)
  622. }
  623. }
  624. for _, client := range toRemove {
  625. api.removeClient(client)
  626. }
  627. }
  628. func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
  629. toRemove := []*streamingClient{}
  630. for _, client := range api.copyOfStreamingClients() {
  631. select {
  632. case client.channel <- &apiv1.EventStreamResponse{
  633. Event: &apiv1.EventStreamResponse_ExecutionStarted{
  634. ExecutionStarted: &apiv1.EventExecutionStarted{
  635. LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
  636. },
  637. },
  638. }:
  639. default:
  640. log.Warnf("EventStream: client channel is full, removing client")
  641. toRemove = append(toRemove, client)
  642. }
  643. }
  644. for _, client := range toRemove {
  645. api.removeClient(client)
  646. }
  647. }
  648. func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
  649. toRemove := []*streamingClient{}
  650. for _, client := range api.copyOfStreamingClients() {
  651. select {
  652. case client.channel <- &apiv1.EventStreamResponse{
  653. Event: &apiv1.EventStreamResponse_ExecutionFinished{
  654. ExecutionFinished: &apiv1.EventExecutionFinished{
  655. LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
  656. },
  657. },
  658. }:
  659. default:
  660. log.Warnf("EventStream: client channel is full, removing client")
  661. toRemove = append(toRemove, client)
  662. }
  663. }
  664. for _, client := range toRemove {
  665. api.removeClient(client)
  666. }
  667. }
  668. func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[apiv1.GetDiagnosticsRequest]) (*connect.Response[apiv1.GetDiagnosticsResponse], error) {
  669. res := &apiv1.GetDiagnosticsResponse{
  670. SshFoundKey: installationinfo.Runtime.SshFoundKey,
  671. SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
  672. }
  673. return connect.NewResponse(res), nil
  674. }
  675. func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitRequest]) (*connect.Response[apiv1.InitResponse], error) {
  676. user := auth.UserFromApiCall(ctx, req, api.cfg)
  677. loginRequired := user.IsGuest() && api.cfg.AuthRequireGuestsToLogin
  678. res := &apiv1.InitResponse{
  679. ShowFooter: api.cfg.ShowFooter,
  680. ShowNavigation: api.cfg.ShowNavigation,
  681. ShowNewVersions: api.cfg.ShowNewVersions,
  682. AvailableVersion: installationinfo.Runtime.AvailableVersion,
  683. CurrentVersion: installationinfo.Build.Version,
  684. PageTitle: api.cfg.PageTitle,
  685. SectionNavigationStyle: api.cfg.SectionNavigationStyle,
  686. DefaultIconForBack: api.cfg.DefaultIconForBack,
  687. EnableCustomJs: api.cfg.EnableCustomJs,
  688. AuthLoginUrl: api.cfg.AuthLoginUrl,
  689. AuthLocalLogin: api.cfg.AuthLocalUsers.Enabled,
  690. OAuth2Providers: buildPublicOAuth2ProvidersList(api.cfg),
  691. AdditionalLinks: buildAdditionalLinks(api.cfg.AdditionalNavigationLinks),
  692. StyleMods: api.cfg.StyleMods,
  693. RootDashboards: api.buildRootDashboards(user, api.cfg.Dashboards),
  694. AuthenticatedUser: user.Username,
  695. AuthenticatedUserProvider: user.Provider,
  696. EffectivePolicy: buildEffectivePolicy(user.EffectivePolicy),
  697. BannerMessage: api.cfg.BannerMessage,
  698. BannerCss: api.cfg.BannerCSS,
  699. ShowDiagnostics: user.EffectivePolicy.ShowDiagnostics,
  700. ShowLogList: user.EffectivePolicy.ShowLogList,
  701. LoginRequired: loginRequired,
  702. }
  703. return connect.NewResponse(res), nil
  704. }
  705. func (api *oliveTinAPI) buildRootDashboards(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
  706. var rootDashboards []string
  707. dashboardRenderRequest := api.createDashboardRenderRequest(user, "", "")
  708. api.addDefaultDashboardIfNeeded(&rootDashboards, dashboardRenderRequest)
  709. api.addCustomDashboards(&rootDashboards, dashboards, dashboardRenderRequest)
  710. return rootDashboards
  711. }
  712. func (api *oliveTinAPI) addDefaultDashboardIfNeeded(rootDashboards *[]string, rr *DashboardRenderRequest) {
  713. defaultDashboard := buildDefaultDashboard(rr)
  714. if defaultDashboard != nil && len(defaultDashboard.Contents) > 0 {
  715. log.Tracef("defaultDashboard: %+v", defaultDashboard.Contents)
  716. *rootDashboards = append(*rootDashboards, "Actions")
  717. }
  718. }
  719. func (api *oliveTinAPI) addCustomDashboards(rootDashboards *[]string, dashboards []*config.DashboardComponent, rr *DashboardRenderRequest) {
  720. for _, dashboard := range dashboards {
  721. // We have to build the dashboard response instead of just looping over config.dashboards,
  722. // because we need to check if the user has access to the dashboard
  723. db := renderDashboard(rr, dashboard.Title)
  724. if db != nil {
  725. *rootDashboards = append(*rootDashboards, dashboard.Title)
  726. }
  727. }
  728. }
  729. func buildPublicOAuth2ProvidersList(cfg *config.Config) []*apiv1.OAuth2Provider {
  730. var publicProviders []*apiv1.OAuth2Provider
  731. for providerKey, provider := range cfg.AuthOAuth2Providers {
  732. publicProviders = append(publicProviders, &apiv1.OAuth2Provider{
  733. Title: provider.Title,
  734. Icon: provider.Icon,
  735. Key: providerKey,
  736. })
  737. }
  738. sort.Slice(publicProviders, func(i, j int) bool {
  739. return publicProviders[i].Key < publicProviders[j].Key
  740. })
  741. return publicProviders
  742. }
  743. func buildAdditionalLinks(links []*config.NavigationLink) []*apiv1.AdditionalLink {
  744. var additionalLinks []*apiv1.AdditionalLink
  745. for _, link := range links {
  746. additionalLinks = append(additionalLinks, &apiv1.AdditionalLink{
  747. Title: link.Title,
  748. Url: link.Url,
  749. })
  750. }
  751. return additionalLinks
  752. }
  753. func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string) {
  754. toRemove := []*streamingClient{}
  755. for _, client := range api.copyOfStreamingClients() {
  756. select {
  757. case client.channel <- &apiv1.EventStreamResponse{
  758. Event: &apiv1.EventStreamResponse_OutputChunk{
  759. OutputChunk: &apiv1.EventOutputChunk{
  760. Output: string(content),
  761. ExecutionTrackingId: executionTrackingId,
  762. },
  763. },
  764. }:
  765. default:
  766. log.Warnf("EventStream: client channel is full, removing client")
  767. toRemove = append(toRemove, client)
  768. }
  769. }
  770. for _, client := range toRemove {
  771. api.removeClient(client)
  772. }
  773. }
  774. func (api *oliveTinAPI) GetEntities(ctx ctx.Context, req *connect.Request[apiv1.GetEntitiesRequest]) (*connect.Response[apiv1.GetEntitiesResponse], error) {
  775. user := auth.UserFromApiCall(ctx, req, api.cfg)
  776. if err := api.checkDashboardAccess(user); err != nil {
  777. return nil, err
  778. }
  779. entityMap := entities.GetEntities()
  780. entityNames := make([]string, 0, len(entityMap))
  781. for name := range entityMap {
  782. entityNames = append(entityNames, name)
  783. }
  784. sort.Strings(entityNames)
  785. entityDefinitions := make([]*apiv1.EntityDefinition, 0, len(entityNames))
  786. for _, name := range entityNames {
  787. def := &apiv1.EntityDefinition{
  788. Title: name,
  789. UsedOnDashboards: findDashboardsForEntity(name, api.cfg.Dashboards),
  790. Instances: buildSortedEntityInstances(name, entityMap[name]),
  791. }
  792. entityDefinitions = append(entityDefinitions, def)
  793. }
  794. res := &apiv1.GetEntitiesResponse{
  795. EntityDefinitions: entityDefinitions,
  796. }
  797. return connect.NewResponse(res), nil
  798. }
  799. func buildSortedEntityInstances(entityType string, entityInstances map[string]*entities.Entity) []*apiv1.Entity {
  800. instanceKeys := make([]string, 0, len(entityInstances))
  801. for key := range entityInstances {
  802. instanceKeys = append(instanceKeys, key)
  803. }
  804. sort.Strings(instanceKeys)
  805. instances := make([]*apiv1.Entity, 0, len(instanceKeys))
  806. for _, key := range instanceKeys {
  807. e := entityInstances[key]
  808. instances = append(instances, &apiv1.Entity{
  809. Title: e.Title,
  810. UniqueKey: e.UniqueKey,
  811. Type: entityType,
  812. })
  813. }
  814. return instances
  815. }
  816. func findDashboardsForEntity(entityTitle string, dashboards []*config.DashboardComponent) []string {
  817. var foundDashboards []string
  818. seen := make(map[string]bool)
  819. findEntityInComponents(entityTitle, "", dashboards, &foundDashboards, seen)
  820. return foundDashboards
  821. }
  822. func findEntityInComponents(entityTitle string, parentTitle string, components []*config.DashboardComponent, foundDashboards *[]string, seen map[string]bool) {
  823. for _, component := range components {
  824. if component.Entity == entityTitle {
  825. addEntityDashboard(component, parentTitle, foundDashboards, seen)
  826. }
  827. if len(component.Contents) > 0 {
  828. findEntityInComponents(entityTitle, component.Title, component.Contents, foundDashboards, seen)
  829. }
  830. }
  831. }
  832. func addEntityDashboard(component *config.DashboardComponent, parentTitle string, foundDashboards *[]string, seen map[string]bool) {
  833. if component.Type == "directory" {
  834. addEntityDirectory(component, foundDashboards, seen)
  835. } else {
  836. addParentDashboard(parentTitle, foundDashboards, seen)
  837. }
  838. }
  839. func addEntityDirectory(component *config.DashboardComponent, foundDashboards *[]string, seen map[string]bool) {
  840. dashboardTitle := component.Title + " [Entity Directory]"
  841. if !seen[dashboardTitle] {
  842. *foundDashboards = append(*foundDashboards, dashboardTitle)
  843. seen[dashboardTitle] = true
  844. seen[component.Title] = true
  845. }
  846. }
  847. func addParentDashboard(parentTitle string, foundDashboards *[]string, seen map[string]bool) {
  848. if parentTitle != "" && !seen[parentTitle] {
  849. *foundDashboards = append(*foundDashboards, parentTitle)
  850. seen[parentTitle] = true
  851. }
  852. }
  853. func findDirectoriesInEntityFieldsets(entityType string, dashboards []*config.DashboardComponent) []string {
  854. var directories []string
  855. for _, dashboard := range dashboards {
  856. findDirectoriesInEntityFieldsetsRecursive(entityType, dashboard, &directories)
  857. }
  858. return directories
  859. }
  860. func findDirectoriesInEntityFieldsetsRecursive(entityType string, component *config.DashboardComponent, directories *[]string) {
  861. if component.Entity == entityType {
  862. collectDirectoriesFromComponent(component, directories)
  863. }
  864. if len(component.Contents) > 0 {
  865. searchSubcomponentsForDirectories(entityType, component.Contents, directories)
  866. }
  867. }
  868. func collectDirectoriesFromComponent(component *config.DashboardComponent, directories *[]string) {
  869. for _, subitem := range component.Contents {
  870. if subitem.Type == "directory" {
  871. *directories = append(*directories, subitem.Title)
  872. }
  873. }
  874. }
  875. func searchSubcomponentsForDirectories(entityType string, contents []*config.DashboardComponent, directories *[]string) {
  876. for _, subitem := range contents {
  877. findDirectoriesInEntityFieldsetsRecursive(entityType, subitem, directories)
  878. }
  879. }
  880. func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.GetEntityRequest]) (*connect.Response[apiv1.Entity], error) {
  881. user := auth.UserFromApiCall(ctx, req, api.cfg)
  882. if err := api.checkDashboardAccess(user); err != nil {
  883. return nil, err
  884. }
  885. instances := entities.GetEntityInstances(req.Msg.Type)
  886. if len(instances) == 0 {
  887. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity type %s not found", req.Msg.Type))
  888. }
  889. entity, ok := instances[req.Msg.UniqueKey]
  890. if !ok {
  891. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity with unique key %s not found in type %s", req.Msg.UniqueKey, req.Msg.Type))
  892. }
  893. res := buildEntityResponse(entity, req.Msg.Type, api.cfg.Dashboards)
  894. return connect.NewResponse(res), nil
  895. }
  896. func buildEntityResponse(entity *entities.Entity, entityType string, dashboards []*config.DashboardComponent) *apiv1.Entity {
  897. res := &apiv1.Entity{
  898. Title: entity.Title,
  899. UniqueKey: entity.UniqueKey,
  900. Type: entityType,
  901. Directories: findDirectoriesInEntityFieldsets(entityType, dashboards),
  902. Fields: serializeEntityFields(entity.Data),
  903. }
  904. return res
  905. }
  906. func serializeEntityFields(data any) map[string]string {
  907. if data == nil {
  908. return nil
  909. }
  910. dataMap, ok := data.(map[string]any)
  911. if !ok {
  912. return nil
  913. }
  914. fields := make(map[string]string)
  915. for k, v := range dataMap {
  916. fields[k] = fmt.Sprintf("%v", v)
  917. }
  918. return fields
  919. }
  920. func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv1.RestartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {
  921. ret := &apiv1.StartActionResponse{
  922. ExecutionTrackingId: req.Msg.ExecutionTrackingId,
  923. }
  924. var execReqLogEntry *executor.InternalLogEntry
  925. execReqLogEntry, found := api.executor.GetLog(req.Msg.ExecutionTrackingId)
  926. if !found {
  927. log.Warnf("Restarting execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId)
  928. return connect.NewResponse(ret), nil
  929. }
  930. log.Warnf("Restarting execution request by tracking ID: %v", req.Msg.ExecutionTrackingId)
  931. action := execReqLogEntry.Binding.Action
  932. if action == nil {
  933. log.Warnf("Restarting execution request not possible - action not found: %v", execReqLogEntry.ActionTitle)
  934. return connect.NewResponse(ret), nil
  935. }
  936. return api.StartAction(ctx, &connect.Request[apiv1.StartActionRequest]{
  937. Msg: &apiv1.StartActionRequest{
  938. // FIXME
  939. UniqueTrackingId: req.Msg.ExecutionTrackingId,
  940. },
  941. })
  942. }
  943. func newServer(ex *executor.Executor) *oliveTinAPI {
  944. server := oliveTinAPI{}
  945. server.cfg = ex.Cfg
  946. server.executor = ex
  947. server.streamingClients = make(map[*streamingClient]struct{})
  948. ex.AddListener(&server)
  949. return &server
  950. }
  951. func GetNewHandler(ex *executor.Executor) (string, http.Handler) {
  952. server := newServer(ex)
  953. jsonOpt := connectproto.WithJSON(
  954. protojson.MarshalOptions{
  955. EmitUnpopulated: true, // https://github.com/OliveTin/OliveTin/issues/674
  956. },
  957. protojson.UnmarshalOptions{
  958. DiscardUnknown: true,
  959. },
  960. )
  961. return apiv1connect.NewOliveTinApiServiceHandler(server, jsonOpt)
  962. }