api.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. package api
  2. import (
  3. ctx "context"
  4. "encoding/json"
  5. "connectrpc.com/connect"
  6. apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
  7. apiv1connect "github.com/OliveTin/OliveTin/gen/olivetin/api/v1/apiv1connect"
  8. "github.com/google/uuid"
  9. log "github.com/sirupsen/logrus"
  10. "fmt"
  11. "net/http"
  12. acl "github.com/OliveTin/OliveTin/internal/acl"
  13. config "github.com/OliveTin/OliveTin/internal/config"
  14. entities "github.com/OliveTin/OliveTin/internal/entities"
  15. executor "github.com/OliveTin/OliveTin/internal/executor"
  16. installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
  17. )
  18. type oliveTinAPI struct {
  19. executor *executor.Executor
  20. cfg *config.Config
  21. connectedClients []*connectedClients
  22. }
  23. type connectedClients struct {
  24. channel chan *apiv1.EventStreamResponse
  25. AuthenticatedUser *acl.AuthenticatedUser
  26. }
  27. func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.KillActionRequest]) (*connect.Response[apiv1.KillActionResponse], error) {
  28. ret := &apiv1.KillActionResponse{
  29. ExecutionTrackingId: req.Msg.ExecutionTrackingId,
  30. }
  31. var execReqLogEntry *executor.InternalLogEntry
  32. execReqLogEntry, ret.Found = api.executor.GetLog(req.Msg.ExecutionTrackingId)
  33. if !ret.Found {
  34. log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId)
  35. return connect.NewResponse(ret), nil
  36. }
  37. log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId)
  38. action := execReqLogEntry.Binding.Action
  39. if action == nil {
  40. log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle)
  41. ret.Killed = false
  42. return connect.NewResponse(ret), nil
  43. }
  44. user := acl.UserFromContext(ctx, api.cfg)
  45. api.killActionByTrackingId(user, action, execReqLogEntry, ret)
  46. return connect.NewResponse(ret), nil
  47. }
  48. func (api *oliveTinAPI) killActionByTrackingId(user *acl.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
  49. if !acl.IsAllowedKill(api.cfg, user, action) {
  50. log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
  51. ret.Killed = false
  52. }
  53. err := api.executor.Kill(execReqLogEntry)
  54. if err != nil {
  55. log.Warnf("Killing execution request err: %v", err)
  56. ret.AlreadyCompleted = true
  57. ret.Killed = false
  58. } else {
  59. ret.Killed = true
  60. }
  61. }
  62. func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *connect.Request[apiv1.StartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {
  63. args := make(map[string]string)
  64. for _, arg := range req.Msg.Arguments {
  65. args[arg.Name] = arg.Value
  66. }
  67. pair := api.executor.FindBindingByID(req.Msg.BindingId)
  68. if pair == nil || pair.Action == nil {
  69. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.BindingId))
  70. }
  71. authenticatedUser := acl.UserFromContext(ctx, api.cfg)
  72. execReq := executor.ExecutionRequest{
  73. Binding: pair,
  74. TrackingID: req.Msg.UniqueTrackingId,
  75. Arguments: args,
  76. AuthenticatedUser: authenticatedUser,
  77. Cfg: api.cfg,
  78. }
  79. api.executor.ExecRequest(&execReq)
  80. ret := &apiv1.StartActionResponse{
  81. ExecutionTrackingId: execReq.TrackingID,
  82. }
  83. return connect.NewResponse(ret), nil
  84. }
  85. func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1.PasswordHashRequest]) (*connect.Response[apiv1.PasswordHashResponse], error) {
  86. hash, err := createHash(req.Msg.Password)
  87. if err != nil {
  88. return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("error creating hash: %w", err))
  89. }
  90. ret := &apiv1.PasswordHashResponse{
  91. Hash: hash,
  92. }
  93. return connect.NewResponse(ret), nil
  94. }
  95. func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) {
  96. match := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password)
  97. if match {
  98. // grpc.SendHeader(ctx, metadata.Pairs("set-username", req.Username))
  99. log.WithFields(log.Fields{
  100. "username": req.Msg.Username,
  101. }).Info("LocalUserLogin: User logged in successfully.")
  102. } else {
  103. log.WithFields(log.Fields{
  104. "username": req.Msg.Username,
  105. }).Warn("LocalUserLogin: User login failed.")
  106. }
  107. return connect.NewResponse(&apiv1.LocalUserLoginResponse{
  108. Success: match,
  109. }), nil
  110. }
  111. func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) {
  112. args := make(map[string]string)
  113. for _, arg := range req.Msg.Arguments {
  114. args[arg.Name] = arg.Value
  115. }
  116. user := acl.UserFromContext(ctx, api.cfg)
  117. execReq := executor.ExecutionRequest{
  118. Binding: api.executor.FindBindingByID(req.Msg.ActionId),
  119. TrackingID: uuid.NewString(),
  120. Arguments: args,
  121. AuthenticatedUser: user,
  122. Cfg: api.cfg,
  123. }
  124. wg, _ := api.executor.ExecRequest(&execReq)
  125. wg.Wait()
  126. internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
  127. if ok {
  128. return connect.NewResponse(&apiv1.StartActionAndWaitResponse{
  129. LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
  130. }), nil
  131. } else {
  132. return nil, fmt.Errorf("execution not found")
  133. }
  134. }
  135. func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetRequest]) (*connect.Response[apiv1.StartActionByGetResponse], error) {
  136. args := make(map[string]string)
  137. execReq := executor.ExecutionRequest{
  138. Binding: api.executor.FindBindingByID(req.Msg.ActionId),
  139. TrackingID: uuid.NewString(),
  140. Arguments: args,
  141. AuthenticatedUser: acl.UserFromContext(ctx, api.cfg),
  142. Cfg: api.cfg,
  143. }
  144. _, uniqueTrackingId := api.executor.ExecRequest(&execReq)
  145. return connect.NewResponse(&apiv1.StartActionByGetResponse{
  146. ExecutionTrackingId: uniqueTrackingId,
  147. }), nil
  148. }
  149. func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
  150. args := make(map[string]string)
  151. user := acl.UserFromContext(ctx, api.cfg)
  152. execReq := executor.ExecutionRequest{
  153. Binding: api.executor.FindBindingByID(req.Msg.ActionId),
  154. TrackingID: uuid.NewString(),
  155. Arguments: args,
  156. AuthenticatedUser: user,
  157. Cfg: api.cfg,
  158. }
  159. wg, _ := api.executor.ExecRequest(&execReq)
  160. wg.Wait()
  161. internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
  162. if ok {
  163. return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{
  164. LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
  165. }), nil
  166. } else {
  167. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
  168. }
  169. }
  170. func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *acl.AuthenticatedUser) *apiv1.LogEntry {
  171. pble := &apiv1.LogEntry{
  172. ActionTitle: logEntry.ActionTitle,
  173. ActionIcon: logEntry.ActionIcon,
  174. ActionId: logEntry.ActionId,
  175. DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
  176. DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
  177. DatetimeIndex: logEntry.Index,
  178. Output: logEntry.Output,
  179. TimedOut: logEntry.TimedOut,
  180. Blocked: logEntry.Blocked,
  181. ExitCode: logEntry.ExitCode,
  182. Tags: logEntry.Tags,
  183. ExecutionTrackingId: logEntry.ExecutionTrackingID,
  184. ExecutionStarted: logEntry.ExecutionStarted,
  185. ExecutionFinished: logEntry.ExecutionFinished,
  186. User: logEntry.Username,
  187. }
  188. if !pble.ExecutionFinished {
  189. pble.CanKill = acl.IsAllowedKill(api.cfg, authenticatedUser, logEntry.Binding.Action)
  190. }
  191. return pble
  192. }
  193. func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
  194. logEntry, ok := api.executor.GetLog(executionTrackingId)
  195. if !ok {
  196. return nil
  197. }
  198. return logEntry
  199. }
  200. func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
  201. var ile *executor.InternalLogEntry
  202. logs := api.executor.GetLogsByActionId(actionId)
  203. if len(logs) == 0 {
  204. return nil
  205. } else {
  206. // Get last log entry
  207. ile = logs[len(logs)-1]
  208. }
  209. return ile
  210. }
  211. func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) {
  212. res := &apiv1.ExecutionStatusResponse{}
  213. user := acl.UserFromContext(ctx, api.cfg)
  214. var ile *executor.InternalLogEntry
  215. if req.Msg.ExecutionTrackingId != "" {
  216. ile = getExecutionStatusByTrackingID(api, req.Msg.ExecutionTrackingId)
  217. } else {
  218. ile = getMostRecentExecutionStatusById(api, req.Msg.ActionId)
  219. }
  220. if ile == nil {
  221. 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))
  222. } else {
  223. res.LogEntry = api.internalLogEntryToPb(ile, user)
  224. }
  225. return connect.NewResponse(res), nil
  226. }
  227. func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
  228. // user := acl.UserFromContext(ctx, cfg)
  229. // grpc.SendHeader(ctx, metadata.Pairs("logout-provider", user.Provider))
  230. // grpc.SendHeader(ctx, metadata.Pairs("logout-sid", user.SID))
  231. return nil, nil
  232. }
  233. func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[apiv1.GetActionBindingRequest]) (*connect.Response[apiv1.GetActionBindingResponse], error) {
  234. binding := api.executor.FindBindingByID(req.Msg.BindingId)
  235. return connect.NewResponse(&apiv1.GetActionBindingResponse{
  236. Action: buildAction(binding, &DashboardRenderRequest{
  237. cfg: api.cfg,
  238. AuthenticatedUser: acl.UserFromContext(ctx, api.cfg),
  239. ex: api.executor,
  240. }),
  241. }), nil
  242. }
  243. func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardRequest]) (*connect.Response[apiv1.GetDashboardResponse], error) {
  244. user := acl.UserFromContext(ctx, api.cfg)
  245. if err := api.checkDashboardAccess(user); err != nil {
  246. return nil, err
  247. }
  248. dashboardRenderRequest := api.createDashboardRenderRequest(user)
  249. if api.isDefaultDashboard(req.Msg.Title) {
  250. return api.buildDefaultDashboardResponse(dashboardRenderRequest)
  251. }
  252. return api.buildCustomDashboardResponse(dashboardRenderRequest, req.Msg.Title)
  253. }
  254. func (api *oliveTinAPI) checkDashboardAccess(user *acl.AuthenticatedUser) error {
  255. if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin {
  256. return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard"))
  257. }
  258. return nil
  259. }
  260. func (api *oliveTinAPI) createDashboardRenderRequest(user *acl.AuthenticatedUser) *DashboardRenderRequest {
  261. return &DashboardRenderRequest{
  262. AuthenticatedUser: user,
  263. cfg: api.cfg,
  264. ex: api.executor,
  265. }
  266. }
  267. func (api *oliveTinAPI) isDefaultDashboard(title string) bool {
  268. return title == "default" || title == "" || title == "Actions"
  269. }
  270. func (api *oliveTinAPI) buildDefaultDashboardResponse(rr *DashboardRenderRequest) (*connect.Response[apiv1.GetDashboardResponse], error) {
  271. db := buildDefaultDashboard(rr)
  272. res := &apiv1.GetDashboardResponse{
  273. Dashboard: db,
  274. }
  275. return connect.NewResponse(res), nil
  276. }
  277. func (api *oliveTinAPI) buildCustomDashboardResponse(rr *DashboardRenderRequest, title string) (*connect.Response[apiv1.GetDashboardResponse], error) {
  278. res := &apiv1.GetDashboardResponse{
  279. Dashboard: renderDashboard(rr, title),
  280. }
  281. return connect.NewResponse(res), nil
  282. }
  283. func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
  284. user := acl.UserFromContext(ctx, api.cfg)
  285. ret := &apiv1.GetLogsResponse{}
  286. logEntries, pagingResult := api.executor.GetLogTrackingIds(req.Msg.StartOffset, api.cfg.LogHistoryPageSize)
  287. for _, logEntry := range logEntries {
  288. action := logEntry.Binding.Action
  289. if action == nil || acl.IsAllowedLogs(api.cfg, user, action) {
  290. pbLogEntry := api.internalLogEntryToPb(logEntry, user)
  291. ret.Logs = append(ret.Logs, pbLogEntry)
  292. }
  293. }
  294. ret.CountRemaining = pagingResult.CountRemaining
  295. ret.PageSize = pagingResult.PageSize
  296. ret.TotalCount = pagingResult.TotalCount
  297. ret.StartOffset = pagingResult.StartOffset
  298. return connect.NewResponse(ret), nil
  299. }
  300. /*
  301. This function is ONLY a helper for the UI - the arguments are validated properly
  302. on the StartAction -> Executor chain. This is here basically to provide helpful
  303. error messages more quickly before starting the action.
  304. */
  305. func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) {
  306. err := executor.TypeSafetyCheck("", req.Msg.Value, req.Msg.Type)
  307. desc := ""
  308. if err != nil {
  309. desc = err.Error()
  310. }
  311. return connect.NewResponse(&apiv1.ValidateArgumentTypeResponse{
  312. Valid: err == nil,
  313. Description: desc,
  314. }), nil
  315. }
  316. func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAmIRequest]) (*connect.Response[apiv1.WhoAmIResponse], error) {
  317. user := acl.UserFromContext(ctx, api.cfg)
  318. res := &apiv1.WhoAmIResponse{
  319. AuthenticatedUser: user.Username,
  320. Usergroup: user.UsergroupLine,
  321. Provider: user.Provider,
  322. Sid: user.SID,
  323. Acls: user.Acls,
  324. }
  325. return connect.NewResponse(res), nil
  326. }
  327. func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *connect.Request[apiv1.SosReportRequest]) (*connect.Response[apiv1.SosReportResponse], error) {
  328. sos := installationinfo.GetSosReport()
  329. if !api.cfg.InsecureAllowDumpSos {
  330. log.Info(sos)
  331. 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."
  332. }
  333. ret := &apiv1.SosReportResponse{
  334. Alert: sos,
  335. }
  336. return connect.NewResponse(ret), nil
  337. }
  338. func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.DumpVarsRequest]) (*connect.Response[apiv1.DumpVarsResponse], error) {
  339. res := &apiv1.DumpVarsResponse{}
  340. if !api.cfg.InsecureAllowDumpVars {
  341. res.Alert = "Dumping variables is not allowed by default because it is insecure."
  342. return connect.NewResponse(res), nil
  343. }
  344. jsonstring, _ := json.MarshalIndent(entities.GetAll(), "", " ")
  345. fmt.Printf("%s", &jsonstring)
  346. res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
  347. return connect.NewResponse(res), nil
  348. }
  349. func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) {
  350. res := &apiv1.DumpPublicIdActionMapResponse{}
  351. res.Contents = make(map[string]*apiv1.ActionEntityPair)
  352. if !api.cfg.InsecureAllowDumpActionMap {
  353. res.Alert = "Dumping Public IDs is disallowed."
  354. return connect.NewResponse(res), nil
  355. }
  356. api.executor.MapActionIdToBindingLock.RLock()
  357. for k, v := range api.executor.MapActionIdToBinding {
  358. res.Contents[k] = &apiv1.ActionEntityPair{
  359. ActionTitle: v.Action.Title,
  360. EntityPrefix: "?",
  361. }
  362. }
  363. api.executor.MapActionIdToBindingLock.RUnlock()
  364. res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
  365. return connect.NewResponse(res), nil
  366. }
  367. func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *connect.Request[apiv1.GetReadyzRequest]) (*connect.Response[apiv1.GetReadyzResponse], error) {
  368. res := &apiv1.GetReadyzResponse{
  369. Status: "OK",
  370. }
  371. return connect.NewResponse(res), nil
  372. }
  373. func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.EventStreamRequest], srv *connect.ServerStream[apiv1.EventStreamResponse]) error {
  374. log.Debugf("EventStream: %v", req.Msg)
  375. client := &connectedClients{
  376. channel: make(chan *apiv1.EventStreamResponse, 10), // Buffered channel to hold Events
  377. AuthenticatedUser: acl.UserFromContext(ctx, api.cfg),
  378. }
  379. log.Infof("EventStream: client connected: %v", client.AuthenticatedUser.Username)
  380. api.connectedClients = append(api.connectedClients, client)
  381. // loop over client channel and send events to connectedClient
  382. for msg := range client.channel {
  383. log.Debugf("Sending event to client: %v", msg)
  384. if err := srv.Send(msg); err != nil {
  385. log.Errorf("Error sending event to client: %v", err)
  386. }
  387. }
  388. log.Infof("EventStream: client disconnected")
  389. return nil
  390. }
  391. func (api *oliveTinAPI) OnActionMapRebuilt() {
  392. for _, client := range api.connectedClients {
  393. select {
  394. case client.channel <- &apiv1.EventStreamResponse{
  395. Event: &apiv1.EventStreamResponse_ConfigChanged{
  396. ConfigChanged: &apiv1.EventConfigChanged{},
  397. },
  398. }:
  399. default:
  400. log.Warnf("EventStream: client channel is full, dropping message")
  401. }
  402. }
  403. }
  404. func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
  405. for _, client := range api.connectedClients {
  406. select {
  407. case client.channel <- &apiv1.EventStreamResponse{
  408. Event: &apiv1.EventStreamResponse_ExecutionStarted{
  409. ExecutionStarted: &apiv1.EventExecutionStarted{
  410. LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
  411. },
  412. },
  413. }:
  414. default:
  415. log.Warnf("EventStream: client channel is full, dropping message")
  416. }
  417. }
  418. }
  419. func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
  420. for _, client := range api.connectedClients {
  421. select {
  422. case client.channel <- &apiv1.EventStreamResponse{
  423. Event: &apiv1.EventStreamResponse_ExecutionFinished{
  424. ExecutionFinished: &apiv1.EventExecutionFinished{
  425. LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
  426. },
  427. },
  428. }:
  429. default:
  430. log.Warnf("EventStream: client channel is full, dropping message")
  431. }
  432. }
  433. }
  434. func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[apiv1.GetDiagnosticsRequest]) (*connect.Response[apiv1.GetDiagnosticsResponse], error) {
  435. res := &apiv1.GetDiagnosticsResponse{
  436. SshFoundKey: installationinfo.Runtime.SshFoundKey,
  437. SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
  438. }
  439. return connect.NewResponse(res), nil
  440. }
  441. func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitRequest]) (*connect.Response[apiv1.InitResponse], error) {
  442. user := acl.UserFromContext(ctx, api.cfg)
  443. res := &apiv1.InitResponse{
  444. ShowFooter: api.cfg.ShowFooter,
  445. ShowNavigation: api.cfg.ShowNavigation,
  446. ShowNewVersions: api.cfg.ShowNewVersions,
  447. AvailableVersion: installationinfo.Runtime.AvailableVersion,
  448. CurrentVersion: installationinfo.Build.Version,
  449. PageTitle: api.cfg.PageTitle,
  450. SectionNavigationStyle: api.cfg.SectionNavigationStyle,
  451. DefaultIconForBack: api.cfg.DefaultIconForBack,
  452. EnableCustomJs: api.cfg.EnableCustomJs,
  453. AuthLoginUrl: api.cfg.AuthLoginUrl,
  454. AuthLocalLogin: api.cfg.AuthLocalUsers.Enabled,
  455. OAuth2Providers: buildPublicOAuth2ProvidersList(api.cfg),
  456. AdditionalLinks: buildAdditionalLinks(api.cfg.AdditionalNavigationLinks),
  457. StyleMods: api.cfg.StyleMods,
  458. RootDashboards: api.buildRootDashboards(user, api.cfg.Dashboards),
  459. AuthenticatedUser: user.Username,
  460. AuthenticatedUserProvider: user.Provider,
  461. EffectivePolicy: buildEffectivePolicy(user.EffectivePolicy),
  462. BannerMessage: api.cfg.BannerMessage,
  463. BannerCss: api.cfg.BannerCSS,
  464. ShowDiagnostics: user.EffectivePolicy.ShowDiagnostics,
  465. ShowLogList: user.EffectivePolicy.ShowLogList,
  466. }
  467. return connect.NewResponse(res), nil
  468. }
  469. func (api *oliveTinAPI) buildRootDashboards(user *acl.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
  470. var rootDashboards []string
  471. dashboardRenderRequest := api.createDashboardRenderRequest(user)
  472. api.addDefaultDashboardIfNeeded(&rootDashboards, dashboardRenderRequest)
  473. api.addCustomDashboards(&rootDashboards, dashboards, dashboardRenderRequest)
  474. return rootDashboards
  475. }
  476. func (api *oliveTinAPI) addDefaultDashboardIfNeeded(rootDashboards *[]string, rr *DashboardRenderRequest) {
  477. defaultDashboard := buildDefaultDashboard(rr)
  478. if defaultDashboard != nil && len(defaultDashboard.Contents) > 0 {
  479. log.Infof("defaultDashboard: %+v", defaultDashboard.Contents)
  480. *rootDashboards = append(*rootDashboards, "Actions")
  481. }
  482. }
  483. func (api *oliveTinAPI) addCustomDashboards(rootDashboards *[]string, dashboards []*config.DashboardComponent, rr *DashboardRenderRequest) {
  484. for _, dashboard := range dashboards {
  485. // We have to build the dashboard response instead of just looping over config.dashboards,
  486. // because we need to check if the user has access to the dashboard
  487. db := renderDashboard(rr, dashboard.Title)
  488. if db != nil {
  489. *rootDashboards = append(*rootDashboards, dashboard.Title)
  490. }
  491. }
  492. }
  493. func buildPublicOAuth2ProvidersList(cfg *config.Config) []*apiv1.OAuth2Provider {
  494. var publicProviders []*apiv1.OAuth2Provider
  495. for _, provider := range cfg.AuthOAuth2Providers {
  496. publicProviders = append(publicProviders, &apiv1.OAuth2Provider{
  497. Title: provider.Title,
  498. Url: provider.AuthUrl,
  499. Icon: provider.Icon,
  500. })
  501. }
  502. return publicProviders
  503. }
  504. func buildAdditionalLinks(links []*config.NavigationLink) []*apiv1.AdditionalLink {
  505. var additionalLinks []*apiv1.AdditionalLink
  506. for _, link := range links {
  507. additionalLinks = append(additionalLinks, &apiv1.AdditionalLink{
  508. Title: link.Title,
  509. Url: link.Url,
  510. })
  511. }
  512. return additionalLinks
  513. }
  514. func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string) {
  515. for _, client := range api.connectedClients {
  516. select {
  517. case client.channel <- &apiv1.EventStreamResponse{
  518. Event: &apiv1.EventStreamResponse_OutputChunk{
  519. OutputChunk: &apiv1.EventOutputChunk{
  520. Output: string(content),
  521. ExecutionTrackingId: executionTrackingId,
  522. },
  523. },
  524. }:
  525. default:
  526. log.Warnf("EventStream: client channel is full, dropping message")
  527. }
  528. }
  529. }
  530. func (api *oliveTinAPI) GetEntities(ctx ctx.Context, req *connect.Request[apiv1.GetEntitiesRequest]) (*connect.Response[apiv1.GetEntitiesResponse], error) {
  531. res := &apiv1.GetEntitiesResponse{
  532. EntityDefinitions: make([]*apiv1.EntityDefinition, 0),
  533. }
  534. for name, entityInstances := range entities.GetEntities() {
  535. def := &apiv1.EntityDefinition{
  536. Title: name,
  537. UsedOnDashboards: findDashboardsForEntity(name, api.cfg.Dashboards),
  538. }
  539. for _, e := range entityInstances {
  540. entity := &apiv1.Entity{
  541. Title: e.Title,
  542. UniqueKey: e.UniqueKey,
  543. Type: name,
  544. }
  545. def.Instances = append(def.Instances, entity)
  546. }
  547. res.EntityDefinitions = append(res.EntityDefinitions, def)
  548. }
  549. return connect.NewResponse(res), nil
  550. }
  551. func findDashboardsForEntity(entityTitle string, dashboards []*config.DashboardComponent) []string {
  552. var foundDashboards []string
  553. findEntityInComponents(entityTitle, "", dashboards, &foundDashboards)
  554. return foundDashboards
  555. }
  556. func findEntityInComponents(entityTitle string, parentTitle string, components []*config.DashboardComponent, foundDashboards *[]string) {
  557. for _, component := range components {
  558. if component.Entity == entityTitle {
  559. *foundDashboards = append(*foundDashboards, parentTitle)
  560. }
  561. if len(component.Contents) > 0 {
  562. findEntityInComponents(entityTitle, component.Title, component.Contents, foundDashboards)
  563. }
  564. }
  565. }
  566. func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.GetEntityRequest]) (*connect.Response[apiv1.Entity], error) {
  567. res := &apiv1.Entity{}
  568. instances := entities.GetEntityInstances(req.Msg.Type)
  569. log.Infof("msg: %+v", req.Msg)
  570. if len(instances) == 0 {
  571. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity type %s not found", req.Msg.Type))
  572. }
  573. if entity, ok := instances[req.Msg.UniqueKey]; !ok {
  574. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity with unique key %s not found in type %s", req.Msg.UniqueKey, req.Msg.Type))
  575. } else {
  576. res.Title = entity.Title
  577. return connect.NewResponse(res), nil
  578. }
  579. }
  580. func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv1.RestartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {
  581. ret := &apiv1.StartActionResponse{
  582. ExecutionTrackingId: req.Msg.ExecutionTrackingId,
  583. }
  584. var execReqLogEntry *executor.InternalLogEntry
  585. execReqLogEntry, found := api.executor.GetLog(req.Msg.ExecutionTrackingId)
  586. if !found {
  587. log.Warnf("Restarting execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId)
  588. return connect.NewResponse(ret), nil
  589. }
  590. log.Warnf("Restarting execution request by tracking ID: %v", req.Msg.ExecutionTrackingId)
  591. action := execReqLogEntry.Binding.Action
  592. if action == nil {
  593. log.Warnf("Restarting execution request not possible - action not found: %v", execReqLogEntry.ActionTitle)
  594. return connect.NewResponse(ret), nil
  595. }
  596. return api.StartAction(ctx, &connect.Request[apiv1.StartActionRequest]{
  597. Msg: &apiv1.StartActionRequest{
  598. // FIXME
  599. UniqueTrackingId: req.Msg.ExecutionTrackingId,
  600. },
  601. })
  602. }
  603. func newServer(ex *executor.Executor) *oliveTinAPI {
  604. server := oliveTinAPI{}
  605. server.cfg = ex.Cfg
  606. server.executor = ex
  607. ex.AddListener(&server)
  608. return &server
  609. }
  610. func GetNewHandler(ex *executor.Executor) (string, http.Handler) {
  611. server := newServer(ex)
  612. return apiv1connect.NewOliveTinApiServiceHandler(server)
  613. }