api.go 45 KB

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