api.go 27 KB

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