api.go 32 KB

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