api.go 43 KB

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