api.go 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883
  1. package api
  2. import (
  3. ctx "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. "os"
  9. "path"
  10. "sort"
  11. "strings"
  12. "sync"
  13. "time"
  14. "connectrpc.com/connect"
  15. "google.golang.org/protobuf/encoding/protojson"
  16. apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
  17. apiv1connect "github.com/OliveTin/OliveTin/gen/olivetin/api/v1/apiv1connect"
  18. "github.com/google/uuid"
  19. log "github.com/sirupsen/logrus"
  20. acl "github.com/OliveTin/OliveTin/internal/acl"
  21. auth "github.com/OliveTin/OliveTin/internal/auth"
  22. authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
  23. config "github.com/OliveTin/OliveTin/internal/config"
  24. entities "github.com/OliveTin/OliveTin/internal/entities"
  25. executor "github.com/OliveTin/OliveTin/internal/executor"
  26. installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
  27. "github.com/OliveTin/OliveTin/internal/tpl"
  28. connectproto "go.akshayshah.org/connectproto"
  29. )
  30. type oliveTinAPI struct {
  31. executor *executor.Executor
  32. cfg *config.Config
  33. // streamingClients is a set of currently connected clients.
  34. // The empty struct value models set semantics (keys only) and keeps add/remove O(1).
  35. // We use a map for efficient membership and deletion; ordering is not required.
  36. streamingClients map[*streamingClient]struct{}
  37. streamingClientsMutex sync.RWMutex
  38. }
  39. const maxEventStreamClients = 16
  40. var errEventStreamClientLimit = errors.New("too many concurrent event stream clients")
  41. // This is used to avoid race conditions when iterating over the connectedClients map.
  42. // and holds the lock for as minimal time as possible to avoid blocking the API for too long.
  43. func (api *oliveTinAPI) copyOfStreamingClients() []*streamingClient {
  44. api.streamingClientsMutex.RLock()
  45. defer api.streamingClientsMutex.RUnlock()
  46. clients := make([]*streamingClient, 0, len(api.streamingClients))
  47. for client := range api.streamingClients {
  48. clients = append(clients, client)
  49. }
  50. return clients
  51. }
  52. type streamingClient struct {
  53. channel chan *apiv1.EventStreamResponse
  54. AuthenticatedUser *authpublic.AuthenticatedUser
  55. heartbeatStop chan struct{}
  56. heartbeatDone chan struct{}
  57. heartbeatStopOnce sync.Once
  58. }
  59. func (c *streamingClient) stopHeartbeat() {
  60. if c.heartbeatStop == nil || c.heartbeatDone == nil {
  61. return
  62. }
  63. c.heartbeatStopOnce.Do(func() {
  64. close(c.heartbeatStop)
  65. })
  66. <-c.heartbeatDone
  67. }
  68. // trySendEventToClient sends msg to the client's channel. Returns false if the channel is full or closed.
  69. func (api *oliveTinAPI) trySendEventToClient(client *streamingClient, msg *apiv1.EventStreamResponse) bool {
  70. if client == nil || msg == nil {
  71. return false
  72. }
  73. sent := sendToStreamingClientChannel(client.channel, msg)
  74. if !sent {
  75. log.Warnf("EventStream: client channel is full or closed, removing client")
  76. }
  77. return sent
  78. }
  79. func sendToStreamingClientChannel(ch chan *apiv1.EventStreamResponse, msg *apiv1.EventStreamResponse) (sent bool) {
  80. defer func() {
  81. if recover() != nil {
  82. sent = false
  83. }
  84. }()
  85. select {
  86. case ch <- msg:
  87. return true
  88. default:
  89. return false
  90. }
  91. }
  92. func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.KillActionRequest]) (*connect.Response[apiv1.KillActionResponse], error) {
  93. ret := &apiv1.KillActionResponse{
  94. ExecutionTrackingId: req.Msg.ExecutionTrackingId,
  95. }
  96. var execReqLogEntry *executor.InternalLogEntry
  97. execReqLogEntry, ret.Found = api.executor.GetLog(req.Msg.ExecutionTrackingId)
  98. if !ret.Found {
  99. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s", req.Msg.ExecutionTrackingId))
  100. }
  101. if execReqLogEntry.Binding == nil {
  102. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("log entry has no binding for tracking ID %s", req.Msg.ExecutionTrackingId))
  103. }
  104. action := execReqLogEntry.Binding.Action
  105. if action == nil {
  106. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action not found for tracking ID %s", req.Msg.ExecutionTrackingId))
  107. }
  108. log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId)
  109. user := auth.UserFromApiCall(ctx, req, api.cfg)
  110. api.killActionByTrackingId(user, action, execReqLogEntry, ret)
  111. return connect.NewResponse(ret), nil
  112. }
  113. func (api *oliveTinAPI) killActionByTrackingId(user *authpublic.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
  114. if !acl.IsAllowedKill(api.cfg, user, action) {
  115. log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
  116. ret.Killed = false
  117. return
  118. }
  119. err := api.executor.Kill(execReqLogEntry)
  120. if err != nil {
  121. log.Warnf("Killing execution request err: %v", err)
  122. ret.AlreadyCompleted = true
  123. ret.Killed = false
  124. } else {
  125. ret.Killed = true
  126. }
  127. }
  128. func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *connect.Request[apiv1.StartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {
  129. pair, err := api.findBindingByIDOrNotFound(req.Msg.BindingId)
  130. if err != nil {
  131. return nil, err
  132. }
  133. authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg)
  134. args := startActionArgumentsFromProto(req.Msg.Arguments)
  135. if err := api.errUnlessStartEntityAccessAllowed(authenticatedUser, pair, args); err != nil {
  136. return nil, err
  137. }
  138. justification := resolveStartJustification(pair.Action, pair, req.Msg.Justification, args)
  139. if err := validateJustificationRequired(pair.Action, justification, authenticatedUser); err != nil {
  140. return nil, connectInvalidJustification(err)
  141. }
  142. execReq := executor.ExecutionRequest{
  143. Binding: pair,
  144. TrackingID: req.Msg.UniqueTrackingId,
  145. Arguments: args,
  146. Justification: justification,
  147. AuthenticatedUser: authenticatedUser,
  148. Cfg: api.cfg,
  149. }
  150. api.executor.ExecRequest(&execReq)
  151. return connect.NewResponse(&apiv1.StartActionResponse{
  152. ExecutionTrackingId: execReq.TrackingID,
  153. }), nil
  154. }
  155. func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1.PasswordHashRequest]) (*connect.Response[apiv1.PasswordHashResponse], error) {
  156. hash, err := createHash(req.Msg.Password)
  157. if err != nil {
  158. if errors.Is(err, ErrArgon2Busy) {
  159. return nil, connect.NewError(connect.CodeResourceExhausted, err)
  160. }
  161. return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("error creating hash: %w", err))
  162. }
  163. ret := &apiv1.PasswordHashResponse{
  164. Hash: hash,
  165. }
  166. return connect.NewResponse(ret), nil
  167. }
  168. func (api *oliveTinAPI) cookieSecure(header http.Header) bool {
  169. useTLS := header.Get("X-Forwarded-Proto") == "https"
  170. return useTLS || api.cfg.Security.ForceSecureCookies
  171. }
  172. func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest, response *connect.Response[apiv1.LocalUserLoginResponse], match bool, secure bool) {
  173. if match {
  174. user := api.cfg.FindUserByUsername(req.Username)
  175. if user != nil {
  176. sid := uuid.NewString()
  177. auth.RegisterUserSession(api.cfg, "local", sid, user.Username)
  178. log.WithFields(log.Fields{"username": user.Username}).Info("LocalUserLogin: Session created and registered")
  179. cookie := &http.Cookie{
  180. Name: "olivetin-sid-local",
  181. Value: sid,
  182. MaxAge: 31556952,
  183. HttpOnly: true,
  184. Path: "/",
  185. Secure: secure,
  186. SameSite: http.SameSiteLaxMode,
  187. }
  188. response.Header().Set("Set-Cookie", cookie.String())
  189. log.WithFields(log.Fields{"username": user.Username}).Info("LocalUserLogin: User logged in successfully.")
  190. } else {
  191. log.WithFields(log.Fields{"username": req.Username}).Warn("LocalUserLogin: Password matched but user lookup failed.")
  192. }
  193. } else {
  194. log.WithFields(log.Fields{"username": req.Username}).Warn("LocalUserLogin: User login failed.")
  195. }
  196. }
  197. func (api *oliveTinAPI) localUserLoginEarlyReject(req *connect.Request[apiv1.LocalUserLoginRequest]) *connect.Response[apiv1.LocalUserLoginResponse] {
  198. if !api.cfg.AuthLocalUsers.Enabled {
  199. return connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: false})
  200. }
  201. if isLocalInteractiveLoginDisabledForUser(api.cfg, req.Msg.Username) {
  202. log.WithFields(log.Fields{"username": req.Msg.Username}).Debug("LocalUserLogin: interactive login disabled (no password configured)")
  203. return connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: false})
  204. }
  205. return nil
  206. }
  207. func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) {
  208. if early := api.localUserLoginEarlyReject(req); early != nil {
  209. return early, nil
  210. }
  211. match, err := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password)
  212. if err != nil {
  213. if errors.Is(err, ErrArgon2Busy) {
  214. return nil, connect.NewError(connect.CodeResourceExhausted, err)
  215. }
  216. return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("checking password: %w", err))
  217. }
  218. response := connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: match})
  219. api.applyLocalLoginResult(req.Msg, response, match, api.cookieSecure(req.Header()))
  220. return response, nil
  221. }
  222. func (api *oliveTinAPI) startActionAndWaitRun(binding *executor.ActionBinding, args map[string]string, justification string, user *authpublic.AuthenticatedUser) (*executor.InternalLogEntry, bool) {
  223. execReq := executor.ExecutionRequest{
  224. Binding: binding,
  225. TrackingID: uuid.NewString(),
  226. Arguments: args,
  227. Justification: justification,
  228. AuthenticatedUser: user,
  229. Cfg: api.cfg,
  230. }
  231. wg, _ := api.executor.ExecRequest(&execReq)
  232. wg.Wait()
  233. return api.executor.GetLog(execReq.TrackingID)
  234. }
  235. func (api *oliveTinAPI) findBindingOrNotFound(actionId string) (*executor.ActionBinding, error) {
  236. binding := api.executor.FindBindingByID(actionId)
  237. if binding == nil || binding.Action == nil {
  238. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", actionId))
  239. }
  240. return binding, nil
  241. }
  242. func (api *oliveTinAPI) findBindingByIDOrNotFound(bindingId string) (*executor.ActionBinding, error) {
  243. return api.findBindingOrNotFound(bindingId)
  244. }
  245. func (api *oliveTinAPI) startActionAndWaitLogEntry(binding *executor.ActionBinding, args map[string]string, justification string, user *authpublic.AuthenticatedUser) (*apiv1.LogEntry, error) {
  246. internalLogEntry, ok := api.startActionAndWaitRun(binding, args, justification, user)
  247. if !ok {
  248. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
  249. }
  250. return api.logEntryForAllowedViewer(internalLogEntry, user)
  251. }
  252. func (api *oliveTinAPI) logEntryForAllowedViewer(internalLogEntry *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) (*apiv1.LogEntry, error) {
  253. if err := api.requireLogEntryAllowed(internalLogEntry, user); err != nil {
  254. return nil, err
  255. }
  256. return api.internalLogEntryToPb(internalLogEntry, user), nil
  257. }
  258. func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) {
  259. binding, err := api.findBindingOrNotFound(req.Msg.ActionId)
  260. if err != nil {
  261. return nil, err
  262. }
  263. user := auth.UserFromApiCall(ctx, req, api.cfg)
  264. args := startActionArgumentsFromProto(req.Msg.Arguments)
  265. if err = api.errUnlessStartEntityAccessAllowed(user, binding, args); err != nil {
  266. return nil, err
  267. }
  268. justification := resolveStartJustification(binding.Action, binding, req.Msg.Justification, args)
  269. if err = validateJustificationRequired(binding.Action, justification, user); err != nil {
  270. return nil, connectInvalidJustification(err)
  271. }
  272. logEntry, err := api.startActionAndWaitLogEntry(binding, args, justification, user)
  273. if err != nil {
  274. return nil, err
  275. }
  276. return connect.NewResponse(&apiv1.StartActionAndWaitResponse{
  277. LogEntry: logEntry,
  278. }), nil
  279. }
  280. func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetRequest]) (*connect.Response[apiv1.StartActionByGetResponse], error) {
  281. binding := api.executor.FindBindingByID(req.Msg.ActionId)
  282. if binding == nil || binding.Action == nil {
  283. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId))
  284. }
  285. user := auth.UserFromApiCall(ctx, req, api.cfg)
  286. if err := api.errUnlessStartEntityAccessAllowed(user, binding, map[string]string{}); err != nil {
  287. return nil, err
  288. }
  289. args := make(map[string]string)
  290. execReq := executor.ExecutionRequest{
  291. Binding: binding,
  292. TrackingID: uuid.NewString(),
  293. Arguments: args,
  294. AuthenticatedUser: user,
  295. Cfg: api.cfg,
  296. }
  297. _, uniqueTrackingId := api.executor.ExecRequest(&execReq)
  298. return connect.NewResponse(&apiv1.StartActionByGetResponse{
  299. ExecutionTrackingId: uniqueTrackingId,
  300. }), nil
  301. }
  302. func (api *oliveTinAPI) runBindingAndWait(binding *executor.ActionBinding, args map[string]string, user *authpublic.AuthenticatedUser) (*executor.InternalLogEntry, bool) {
  303. execReq := executor.ExecutionRequest{
  304. Binding: binding,
  305. TrackingID: uuid.NewString(),
  306. Arguments: args,
  307. AuthenticatedUser: user,
  308. Cfg: api.cfg,
  309. }
  310. wg, _ := api.executor.ExecRequest(&execReq)
  311. wg.Wait()
  312. return api.executor.GetLog(execReq.TrackingID)
  313. }
  314. func (api *oliveTinAPI) startActionByGetAndWaitLogEntry(binding *executor.ActionBinding, user *authpublic.AuthenticatedUser) (*apiv1.LogEntry, error) {
  315. internalLogEntry, ok := api.runBindingAndWait(binding, map[string]string{}, user)
  316. if !ok {
  317. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
  318. }
  319. return api.logEntryForAllowedViewer(internalLogEntry, user)
  320. }
  321. func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
  322. binding := api.executor.FindBindingByID(req.Msg.ActionId)
  323. if binding == nil || binding.Action == nil {
  324. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId))
  325. }
  326. user := auth.UserFromApiCall(ctx, req, api.cfg)
  327. if err := api.errUnlessStartEntityAccessAllowed(user, binding, map[string]string{}); err != nil {
  328. return nil, err
  329. }
  330. logEntry, err := api.startActionByGetAndWaitLogEntry(binding, user)
  331. if err != nil {
  332. return nil, err
  333. }
  334. return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{
  335. LogEntry: logEntry,
  336. }), nil
  337. }
  338. func calculateRateLimitExpires(api *oliveTinAPI, logEntry *executor.InternalLogEntry) string {
  339. if logEntry.Binding == nil || logEntry.Binding.Action == nil {
  340. return ""
  341. }
  342. expiryUnix := api.executor.GetTimeUntilAvailable(logEntry.Binding)
  343. if expiryUnix <= 0 {
  344. return ""
  345. }
  346. return time.Unix(expiryUnix, 0).Format("2006-01-02 15:04:05")
  347. }
  348. func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *authpublic.AuthenticatedUser) *apiv1.LogEntry {
  349. pble := &apiv1.LogEntry{
  350. ActionTitle: logEntry.ActionTitle,
  351. ActionIcon: logEntry.ActionIcon,
  352. DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
  353. DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
  354. DatetimeIndex: logEntry.Index,
  355. Output: logEntry.Output,
  356. TimedOut: logEntry.TimedOut,
  357. Blocked: logEntry.Blocked,
  358. Queued: logEntry.Queued,
  359. QueuedForGroup: logEntry.QueuedForGroup,
  360. ExitCode: logEntry.ExitCode,
  361. Tags: logEntry.Tags,
  362. ExecutionTrackingId: logEntry.ExecutionTrackingID,
  363. ExecutionStarted: logEntry.ExecutionStarted,
  364. ExecutionFinished: logEntry.ExecutionFinished,
  365. User: logEntry.Username,
  366. BindingId: logEntry.GetBindingId(),
  367. DatetimeRateLimitExpires: calculateRateLimitExpires(api, logEntry),
  368. Justification: logEntry.Justification,
  369. Arguments: logEntryArgumentsToProto(logEntry.Arguments),
  370. }
  371. if !pble.ExecutionFinished && logEntry.Binding != nil && logEntry.Binding.Action != nil {
  372. pble.CanKill = acl.IsAllowedKill(api.cfg, authenticatedUser, logEntry.Binding.Action)
  373. }
  374. return pble
  375. }
  376. func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
  377. logEntry, ok := api.executor.GetLog(executionTrackingId)
  378. if !ok {
  379. return nil
  380. }
  381. return logEntry
  382. }
  383. // This is the actual action ID, not the binding ID.
  384. func getMostRecentExecutionStatusByActionId(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
  385. var ile *executor.InternalLogEntry
  386. binding := api.executor.FindBindingByID(actionId)
  387. if binding == nil {
  388. return nil
  389. }
  390. logs := api.executor.GetLogsByBindingId(binding.ID)
  391. if len(logs) == 0 {
  392. return nil
  393. }
  394. if len(logs) == 0 {
  395. return nil
  396. } else {
  397. // Get last log entry
  398. ile = logs[len(logs)-1]
  399. }
  400. return ile
  401. }
  402. func (api *oliveTinAPI) resolveExecutionStatusForView(msg *apiv1.ExecutionStatusRequest, user *authpublic.AuthenticatedUser) (*executor.InternalLogEntry, error) {
  403. ile := api.getExecutionStatusByRequest(msg)
  404. if ile == nil {
  405. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s or action ID %s", msg.ExecutionTrackingId, msg.ActionId))
  406. }
  407. if !isValidLogEntry(ile) || !api.isLogEntryAllowed(ile, user) {
  408. return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied to view this execution"))
  409. }
  410. return ile, nil
  411. }
  412. func (api *oliveTinAPI) getExecutionStatusByRequest(msg *apiv1.ExecutionStatusRequest) *executor.InternalLogEntry {
  413. if msg.ExecutionTrackingId != "" {
  414. return getExecutionStatusByTrackingID(api, msg.ExecutionTrackingId)
  415. }
  416. return getMostRecentExecutionStatusByActionId(api, msg.ActionId)
  417. }
  418. func dashboardNavigationTargetsToPb(targets []executor.DashboardNavigationTarget) []*apiv1.DashboardNavigationTarget {
  419. if len(targets) == 0 {
  420. return nil
  421. }
  422. result := make([]*apiv1.DashboardNavigationTarget, 0, len(targets))
  423. for _, target := range targets {
  424. result = append(result, &apiv1.DashboardNavigationTarget{
  425. Title: target.Title,
  426. EntityType: target.EntityType,
  427. EntityKey: target.EntityKey,
  428. Path: target.Path,
  429. })
  430. }
  431. return result
  432. }
  433. func (api *oliveTinAPI) executionStatusBackToDashboards(ile *executor.InternalLogEntry) []*apiv1.DashboardNavigationTarget {
  434. if ile == nil || ile.Binding == nil {
  435. return nil
  436. }
  437. return dashboardNavigationTargetsToPb(ile.Binding.OnDashboards)
  438. }
  439. func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) {
  440. user := auth.UserFromApiCall(ctx, req, api.cfg)
  441. if err := api.checkDashboardAccess(user); err != nil {
  442. return nil, err
  443. }
  444. ile, err := api.resolveExecutionStatusForView(req.Msg, user)
  445. if err != nil {
  446. return nil, err
  447. }
  448. res := &apiv1.ExecutionStatusResponse{
  449. LogEntry: api.internalLogEntryToPb(ile, user),
  450. BackToDashboards: api.executionStatusBackToDashboards(ile),
  451. }
  452. return connect.NewResponse(res), nil
  453. }
  454. func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
  455. user := auth.UserFromApiCall(ctx, req, api.cfg)
  456. auth.RevokeSessionForProvider(api.cfg, user.Provider, user.SID)
  457. log.WithFields(log.Fields{
  458. "username": user.Username,
  459. "provider": user.Provider,
  460. }).Info("Logout: User logged out")
  461. response := connect.NewResponse(&apiv1.LogoutResponse{})
  462. secure := api.cookieSecure(req.Header())
  463. // Clear the local authentication cookie by setting it to expire
  464. localCookie := &http.Cookie{
  465. Name: "olivetin-sid-local",
  466. Value: "",
  467. MaxAge: -1, // This tells the browser to delete the cookie
  468. HttpOnly: true,
  469. Path: "/",
  470. Secure: secure,
  471. SameSite: http.SameSiteLaxMode,
  472. }
  473. response.Header().Set("Set-Cookie", localCookie.String())
  474. // Clear the OAuth2 authentication cookie by setting it to expire
  475. oauth2Cookie := &http.Cookie{
  476. Name: "olivetin-sid-oauth",
  477. Value: "",
  478. MaxAge: -1, // This tells the browser to delete the cookie
  479. HttpOnly: true,
  480. Path: "/",
  481. Secure: secure,
  482. SameSite: http.SameSiteLaxMode,
  483. }
  484. response.Header().Add("Set-Cookie", oauth2Cookie.String())
  485. return response, nil
  486. }
  487. func (api *oliveTinAPI) GetActionBinding(ctx ctx.Context, req *connect.Request[apiv1.GetActionBindingRequest]) (*connect.Response[apiv1.GetActionBindingResponse], error) {
  488. user := auth.UserFromApiCall(ctx, req, api.cfg)
  489. if err := api.checkDashboardAccess(user); err != nil {
  490. return nil, err
  491. }
  492. resp, err := api.getActionBindingResponse(user, req.Msg.BindingId)
  493. if err != nil {
  494. return nil, err
  495. }
  496. return connect.NewResponse(resp), nil
  497. }
  498. func (api *oliveTinAPI) getActionBindingResponse(user *authpublic.AuthenticatedUser, bindingId string) (*apiv1.GetActionBindingResponse, error) {
  499. binding := api.executor.FindBindingByID(bindingId)
  500. if binding == nil || binding.Action == nil {
  501. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", bindingId))
  502. }
  503. if !api.userCanViewBinding(user, binding) {
  504. return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
  505. }
  506. return &apiv1.GetActionBindingResponse{
  507. Action: buildAction(binding, api.createDashboardRenderRequest(user, "", "")),
  508. BackToDashboards: dashboardNavigationTargetsToPb(binding.OnDashboards),
  509. }, nil
  510. }
  511. func (api *oliveTinAPI) userCanViewAction(user *authpublic.AuthenticatedUser, action *config.Action) bool {
  512. if user == nil {
  513. return true
  514. }
  515. return acl.IsAllowedView(api.cfg, user, action)
  516. }
  517. func (api *oliveTinAPI) userCanViewEntityType(user *authpublic.AuthenticatedUser, entityType string) bool {
  518. return acl.IsAllowedViewEntityType(api.cfg, user, entityFileForType(api.cfg, entityType))
  519. }
  520. func (api *oliveTinAPI) userCanViewBinding(user *authpublic.AuthenticatedUser, binding *executor.ActionBinding) bool {
  521. if binding == nil || binding.Action == nil {
  522. return false
  523. }
  524. if !api.userCanViewAction(user, binding.Action) {
  525. return false
  526. }
  527. return api.bindingEntityTypeAllowed(user, binding)
  528. }
  529. func (api *oliveTinAPI) bindingEntityTypeAllowed(user *authpublic.AuthenticatedUser, binding *executor.ActionBinding) bool {
  530. if binding == nil || binding.Action == nil || binding.Action.Entity == "" {
  531. return true
  532. }
  533. return api.userCanViewEntityType(user, binding.Action.Entity)
  534. }
  535. func (api *oliveTinAPI) errUnlessBindingEntityTypeAllowed(user *authpublic.AuthenticatedUser, binding *executor.ActionBinding) error {
  536. if api.bindingEntityTypeAllowed(user, binding) {
  537. return nil
  538. }
  539. return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
  540. }
  541. func (api *oliveTinAPI) GetDashboard(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardRequest]) (*connect.Response[apiv1.GetDashboardResponse], error) {
  542. user := auth.UserFromApiCall(ctx, req, api.cfg)
  543. if err := api.checkDashboardAccess(user); err != nil {
  544. return nil, err
  545. }
  546. entityType := ""
  547. entityKey := ""
  548. if req.Msg != nil {
  549. entityType = req.Msg.EntityType
  550. entityKey = req.Msg.EntityKey
  551. }
  552. dashboardRenderRequest := api.createDashboardRenderRequest(user, entityType, entityKey)
  553. if api.isDefaultDashboard(req.Msg.Title) {
  554. return api.buildDefaultDashboardResponse(dashboardRenderRequest)
  555. }
  556. return api.buildCustomDashboardResponse(dashboardRenderRequest, req.Msg.Title)
  557. }
  558. func (api *oliveTinAPI) checkDashboardAccess(user *authpublic.AuthenticatedUser) error {
  559. if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin {
  560. return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard"))
  561. }
  562. return nil
  563. }
  564. func (api *oliveTinAPI) createDashboardRenderRequest(user *authpublic.AuthenticatedUser, entityType, entityKey string) *DashboardRenderRequest {
  565. rr := &DashboardRenderRequest{
  566. AuthenticatedUser: user,
  567. cfg: api.cfg,
  568. ex: api.executor,
  569. EntityType: entityType,
  570. EntityKey: entityKey,
  571. }
  572. populateActiveBindingStates(rr)
  573. return rr
  574. }
  575. func (api *oliveTinAPI) isDefaultDashboard(title string) bool {
  576. return title == "default" || title == "" || title == "Actions"
  577. }
  578. func (api *oliveTinAPI) buildDefaultDashboardResponse(rr *DashboardRenderRequest) (*connect.Response[apiv1.GetDashboardResponse], error) {
  579. db := buildDefaultDashboard(rr)
  580. res := &apiv1.GetDashboardResponse{
  581. Dashboard: db,
  582. }
  583. return connect.NewResponse(res), nil
  584. }
  585. func (api *oliveTinAPI) buildCustomDashboardResponse(rr *DashboardRenderRequest, title string) (*connect.Response[apiv1.GetDashboardResponse], error) {
  586. res := &apiv1.GetDashboardResponse{
  587. Dashboard: renderDashboard(rr, title),
  588. }
  589. return connect.NewResponse(res), nil
  590. }
  591. func resolveLogsPageSize(requestPageSize, defaultPageSize int64) int64 {
  592. if requestPageSize == 0 {
  593. return defaultPageSize
  594. }
  595. if requestPageSize < 10 {
  596. return 10
  597. }
  598. if requestPageSize > 100 {
  599. return 100
  600. }
  601. return requestPageSize
  602. }
  603. func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
  604. user := auth.UserFromApiCall(ctx, req, api.cfg)
  605. if err := api.checkDashboardAccess(user); err != nil {
  606. return nil, err
  607. }
  608. pageSize := resolveLogsPageSize(req.Msg.GetPageSize(), api.cfg.LogHistoryPageSize)
  609. logEntries, paging, err := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, pageSize, req.Msg.DateFilter, req.Msg.GetFilter())
  610. if err != nil {
  611. return nil, connect.NewError(connect.CodeInvalidArgument, err)
  612. }
  613. ret := &apiv1.GetLogsResponse{}
  614. for _, le := range logEntries {
  615. ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
  616. }
  617. ret.CountRemaining = paging.CountRemaining
  618. ret.PageSize = paging.PageSize
  619. ret.TotalCount = paging.TotalCount
  620. ret.StartOffset = paging.StartOffset
  621. return connect.NewResponse(ret), nil
  622. }
  623. // isValidLogEntry checks if a log entry has all required fields populated.
  624. func isValidLogEntry(e *executor.InternalLogEntry) bool {
  625. return e != nil && e.Binding != nil && e.Binding.Action != nil
  626. }
  627. // isLogEntryAllowed checks if a log entry is allowed to be viewed by the user.
  628. func (api *oliveTinAPI) isLogEntryAllowed(e *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) bool {
  629. if user == nil || !isValidLogEntry(e) {
  630. return false
  631. }
  632. return acl.IsAllowedLogs(api.cfg, user, e.Binding.Action)
  633. }
  634. func (api *oliveTinAPI) requireLogEntryAllowed(entry *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) error {
  635. if api.isLogEntryAllowed(entry, user) {
  636. return nil
  637. }
  638. return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied to view this execution"))
  639. }
  640. // mayViewExecutionEvent returns whether the user is allowed to receive this execution event (for EventStream ACL).
  641. func (api *oliveTinAPI) mayViewExecutionEvent(entry *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) bool {
  642. if user == nil {
  643. return false
  644. }
  645. return isValidLogEntry(entry) && api.isLogEntryAllowed(entry, user)
  646. }
  647. // buildEmptyPageResponse creates a response for an empty page.
  648. func buildEmptyPageResponse(page pageInfo) *apiv1.GetActionLogsResponse {
  649. return &apiv1.GetActionLogsResponse{
  650. CountRemaining: 0,
  651. PageSize: page.size,
  652. TotalCount: page.total,
  653. StartOffset: page.start,
  654. }
  655. }
  656. // calculateReversedIndices computes the reversed indices for newest-first pagination.
  657. func calculateReversedIndices(page pageInfo, filteredLen int) (int64, int64) {
  658. startIdx := page.total - page.end
  659. endIdx := page.total - page.start
  660. if startIdx < 0 {
  661. startIdx = 0
  662. }
  663. if endIdx > int64(filteredLen) {
  664. endIdx = int64(filteredLen)
  665. }
  666. return startIdx, endIdx
  667. }
  668. // buildActionLogsResponse builds the response with paginated log entries (newest first).
  669. func (api *oliveTinAPI) buildActionLogsResponse(filtered []*executor.InternalLogEntry, page pageInfo, user *authpublic.AuthenticatedUser) *apiv1.GetActionLogsResponse {
  670. startIdx, endIdx := calculateReversedIndices(page, len(filtered))
  671. ret := &apiv1.GetActionLogsResponse{}
  672. chunk := filtered[int(startIdx):int(endIdx)]
  673. for i := len(chunk) - 1; i >= 0; i-- {
  674. ret.Logs = append(ret.Logs, api.internalLogEntryToPb(chunk[i], user))
  675. }
  676. ret.CountRemaining = page.start
  677. ret.PageSize = page.size
  678. ret.TotalCount = page.total
  679. ret.StartOffset = page.start
  680. return ret
  681. }
  682. func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv1.GetActionLogsRequest]) (*connect.Response[apiv1.GetActionLogsResponse], error) {
  683. user := auth.UserFromApiCall(ctx, req, api.cfg)
  684. if err := api.checkDashboardAccess(user); err != nil {
  685. return nil, err
  686. }
  687. filtered := api.filterLogsByACL(api.executor.GetLogsByBindingId(req.Msg.ActionId), user)
  688. page := paginate(int64(len(filtered)), api.cfg.LogHistoryPageSize, req.Msg.StartOffset)
  689. if page.empty {
  690. return connect.NewResponse(buildEmptyPageResponse(page)), nil
  691. }
  692. return connect.NewResponse(api.buildActionLogsResponse(filtered, page, user)), nil
  693. }
  694. func (api *oliveTinAPI) filterLogsByACL(entries []*executor.InternalLogEntry, user *authpublic.AuthenticatedUser) []*executor.InternalLogEntry {
  695. filtered := make([]*executor.InternalLogEntry, 0, len(entries))
  696. for _, e := range entries {
  697. if !isValidLogEntry(e) {
  698. continue
  699. }
  700. if api.isLogEntryAllowed(e, user) {
  701. filtered = append(filtered, e)
  702. }
  703. }
  704. return filtered
  705. }
  706. type pageInfo struct {
  707. total int64
  708. size int64
  709. start int64
  710. end int64
  711. empty bool
  712. }
  713. func paginate(total int64, size int64, start int64) pageInfo {
  714. if start < 0 {
  715. start = 0
  716. }
  717. if start >= total {
  718. return pageInfo{total: total, size: size, start: start, end: start, empty: true}
  719. }
  720. end := min(start+size, total)
  721. return pageInfo{total: total, size: size, start: start, end: end, empty: false}
  722. }
  723. /*
  724. This function is ONLY a helper for the UI - the arguments are validated properly
  725. on the StartAction -> Executor chain. This is here basically to provide helpful
  726. error messages more quickly before starting the action.
  727. It uses the same validation logic as the executor, including mangling argument
  728. values (e.g., datetime formatting, checkbox title-to-value conversion).
  729. */
  730. func (api *oliveTinAPI) argumentNotFoundForValidation(msg *apiv1.ValidateArgumentTypeRequest) bool {
  731. if msg.BindingId == "" || msg.ArgumentName == "" {
  732. return false
  733. }
  734. arg, _ := api.findArgumentForValidation(msg.BindingId, msg.ArgumentName)
  735. return arg == nil
  736. }
  737. func (api *oliveTinAPI) validateArgumentTypeBindingAccess(user *authpublic.AuthenticatedUser, msg *apiv1.ValidateArgumentTypeRequest) error {
  738. if msg == nil || msg.BindingId == "" {
  739. return nil
  740. }
  741. return api.errUnlessUserMayValidateArgumentTypeForBinding(user, msg.BindingId)
  742. }
  743. func (api *oliveTinAPI) errUnlessUserMayValidateArgumentTypeForBinding(user *authpublic.AuthenticatedUser, bindingID string) error {
  744. binding := api.executor.FindBindingByID(bindingID)
  745. if binding == nil || binding.Action == nil {
  746. return connect.NewError(connect.CodeNotFound, fmt.Errorf("action or argument not found for binding ID %s", bindingID))
  747. }
  748. if !api.userCanViewBinding(user, binding) {
  749. return connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
  750. }
  751. return nil
  752. }
  753. func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) {
  754. user := auth.UserFromApiCall(ctx, req, api.cfg)
  755. if err := api.checkDashboardAccess(user); err != nil {
  756. return nil, err
  757. }
  758. if err := api.validateArgumentTypeBindingAccess(user, req.Msg); err != nil {
  759. return nil, err
  760. }
  761. if api.argumentNotFoundForValidation(req.Msg) {
  762. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action or argument not found for binding ID %s", req.Msg.BindingId))
  763. }
  764. if err := api.validateArgumentTypeEntityAccess(user, req.Msg); err != nil {
  765. return nil, err
  766. }
  767. return api.validateArgumentTypeConnectResponse(req.Msg)
  768. }
  769. func (api *oliveTinAPI) validateArgumentTypeEntityAccess(user *authpublic.AuthenticatedUser, msg *apiv1.ValidateArgumentTypeRequest) error {
  770. arg := api.argumentFromValidationRequest(msg)
  771. if arg == nil {
  772. return nil
  773. }
  774. return api.errUnlessEntityArgumentAllowed(user, arg, "")
  775. }
  776. func (api *oliveTinAPI) argumentFromValidationRequest(msg *apiv1.ValidateArgumentTypeRequest) *config.ActionArgument {
  777. if msg == nil || msg.BindingId == "" || msg.ArgumentName == "" {
  778. return nil
  779. }
  780. arg, _ := api.findArgumentForValidation(msg.BindingId, msg.ArgumentName)
  781. return arg
  782. }
  783. func (api *oliveTinAPI) validateArgumentTypeConnectResponse(msg *apiv1.ValidateArgumentTypeRequest) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) {
  784. err := api.validateArgumentTypeInternal(msg)
  785. desc := ""
  786. if err != nil {
  787. desc = err.Error()
  788. }
  789. return connect.NewResponse(&apiv1.ValidateArgumentTypeResponse{
  790. Valid: err == nil,
  791. Description: desc,
  792. }), nil
  793. }
  794. func (api *oliveTinAPI) validateArgumentTypeInternal(msg *apiv1.ValidateArgumentTypeRequest) error {
  795. if msg.BindingId == "" || msg.ArgumentName == "" {
  796. return executor.TypeSafetyCheck("", msg.Value, msg.Type)
  797. }
  798. arg, action := api.findArgumentForValidation(msg.BindingId, msg.ArgumentName)
  799. if arg == nil {
  800. return fmt.Errorf("argument not found")
  801. }
  802. if err := errUnlessEntityArgumentValueAllowed(arg, msg.Value); err != nil {
  803. return err
  804. }
  805. return executor.ValidateArgument(arg, msg.Value, action)
  806. }
  807. func (api *oliveTinAPI) findArgumentForValidation(bindingId string, argumentName string) (*config.ActionArgument, *config.Action) {
  808. binding := api.executor.FindBindingByID(bindingId)
  809. if binding == nil || binding.Action == nil {
  810. return nil, nil
  811. }
  812. arg := api.findArgumentByName(binding.Action, argumentName)
  813. return arg, binding.Action
  814. }
  815. func (api *oliveTinAPI) findArgumentByName(action *config.Action, name string) *config.ActionArgument {
  816. for i := range action.Arguments {
  817. if action.Arguments[i].Name == name {
  818. return &action.Arguments[i]
  819. }
  820. }
  821. return nil
  822. }
  823. func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAmIRequest]) (*connect.Response[apiv1.WhoAmIResponse], error) {
  824. user := auth.UserFromApiCall(ctx, req, api.cfg)
  825. if err := api.checkDashboardAccess(user); err != nil {
  826. return nil, err
  827. }
  828. res := &apiv1.WhoAmIResponse{
  829. AuthenticatedUser: user.Username,
  830. Usergroup: user.UsergroupLine,
  831. Provider: user.Provider,
  832. Sid: user.SID,
  833. Acls: user.Acls,
  834. }
  835. return connect.NewResponse(res), nil
  836. }
  837. func (api *oliveTinAPI) ServerDiagnostics(ctx ctx.Context, req *connect.Request[apiv1.ServerDiagnosticsRequest]) (*connect.Response[apiv1.ServerDiagnosticsResponse], error) {
  838. user := auth.UserFromApiCall(ctx, req, api.cfg)
  839. redactVersion := !user.EffectivePolicy.ShowVersionNumber
  840. diagnostics := installationinfo.GetServerDiagnostics(redactVersion)
  841. if !api.cfg.InsecureAllowDumpServerDiagnostics {
  842. log.Info(diagnostics)
  843. diagnostics = "Your Server Diagnostics have been logged to OliveTin logs.\n\nIf you are in a safe network, you can temporarily set `insecureAllowDumpServerDiagnostics: true` in your config.yaml, restart OliveTin, and refresh this page - it will put the output directly in the browser."
  844. }
  845. ret := &apiv1.ServerDiagnosticsResponse{
  846. Alert: diagnostics,
  847. }
  848. return connect.NewResponse(ret), nil
  849. }
  850. func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.DumpVarsRequest]) (*connect.Response[apiv1.DumpVarsResponse], error) {
  851. res := &apiv1.DumpVarsResponse{}
  852. if !api.cfg.InsecureAllowDumpVars {
  853. res.Alert = "Dumping variables is not allowed by default because it is insecure."
  854. return connect.NewResponse(res), nil
  855. }
  856. jsonstring, err := json.MarshalIndent(tpl.GetNewGeneralTemplateContext(), "", " ")
  857. if err != nil {
  858. log.WithError(err).Error("DumpVars: failed to marshal template context from GetNewGeneralTemplateContext")
  859. return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("dump vars: marshal template context: %w", err))
  860. }
  861. fmt.Printf("%s", jsonstring)
  862. res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
  863. return connect.NewResponse(res), nil
  864. }
  865. func debugBindingActionTitle(binding *executor.ActionBinding) string {
  866. if binding == nil || binding.Action == nil {
  867. return ""
  868. }
  869. return binding.Action.Title
  870. }
  871. func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) {
  872. res := &apiv1.DumpPublicIdActionMapResponse{}
  873. res.Contents = make(map[string]*apiv1.DebugBinding)
  874. if !api.cfg.InsecureAllowDumpActionMap {
  875. res.Alert = "Dumping Public IDs is disallowed."
  876. return connect.NewResponse(res), nil
  877. }
  878. api.executor.MapActionBindingsLock.RLock()
  879. for k, v := range api.executor.MapActionBindings {
  880. res.Contents[k] = &apiv1.DebugBinding{
  881. ActionTitle: debugBindingActionTitle(v),
  882. }
  883. }
  884. api.executor.MapActionBindingsLock.RUnlock()
  885. res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
  886. return connect.NewResponse(res), nil
  887. }
  888. func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *connect.Request[apiv1.GetReadyzRequest]) (*connect.Response[apiv1.GetReadyzResponse], error) {
  889. res := &apiv1.GetReadyzResponse{
  890. Status: "OK",
  891. }
  892. return connect.NewResponse(res), nil
  893. }
  894. func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.EventStreamRequest], srv *connect.ServerStream[apiv1.EventStreamResponse]) error {
  895. log.Debugf("EventStream: %v", req.Msg)
  896. // Set X-Accel-Buffering header to disable nginx buffering for this stream
  897. // https://github.com/OliveTin/OliveTin/issues/765
  898. srv.ResponseHeader().Set("X-Accel-Buffering", "no")
  899. user := auth.UserFromApiCall(ctx, req, api.cfg)
  900. if err := api.checkDashboardAccess(user); err != nil {
  901. return err
  902. }
  903. client := &streamingClient{
  904. channel: make(chan *apiv1.EventStreamResponse, 10), // Buffered channel to hold Events
  905. AuthenticatedUser: user,
  906. heartbeatStop: make(chan struct{}),
  907. heartbeatDone: make(chan struct{}),
  908. }
  909. if err := api.registerStreamingClient(client); err != nil {
  910. return connect.NewError(connect.CodeResourceExhausted, err)
  911. }
  912. log.WithFields(log.Fields{
  913. "authenticatedUser": user.Username,
  914. }).Debugf("EventStream: client connected")
  915. go api.sendEventStreamHeartbeats(client)
  916. // loop over client channel and send events to connectedClient
  917. for msg := range client.channel {
  918. log.Debugf("Sending event to client: %v", msg)
  919. if err := srv.Send(msg); err != nil {
  920. log.Errorf("Error sending event to client: %v", err)
  921. // Remove disconnected client from the list
  922. api.removeClient(client)
  923. break
  924. }
  925. }
  926. log.Infof("EventStream: client disconnected")
  927. return nil
  928. }
  929. func (api *oliveTinAPI) registerStreamingClient(client *streamingClient) error {
  930. api.streamingClientsMutex.Lock()
  931. defer api.streamingClientsMutex.Unlock()
  932. if len(api.streamingClients) >= maxEventStreamClients {
  933. log.WithFields(log.Fields{
  934. "limit": maxEventStreamClients,
  935. }).Warn("EventStream: rejecting client; concurrent client limit reached")
  936. return errEventStreamClientLimit
  937. }
  938. api.streamingClients[client] = struct{}{}
  939. return nil
  940. }
  941. func (api *oliveTinAPI) sendEventStreamHeartbeats(client *streamingClient) {
  942. defer close(client.heartbeatDone)
  943. if !api.sendEventStreamHeartbeat(client) {
  944. go api.removeClient(client)
  945. return
  946. }
  947. ticker := time.NewTicker(10 * time.Second)
  948. defer ticker.Stop()
  949. api.runEventStreamHeartbeatLoop(client, ticker)
  950. }
  951. func (api *oliveTinAPI) runEventStreamHeartbeatLoop(client *streamingClient, ticker *time.Ticker) {
  952. for {
  953. if api.waitEventStreamHeartbeatOrDone(client.heartbeatStop, ticker) {
  954. return
  955. }
  956. if !api.sendEventStreamHeartbeat(client) {
  957. go api.removeClient(client)
  958. return
  959. }
  960. }
  961. }
  962. func (api *oliveTinAPI) waitEventStreamHeartbeatOrDone(done <-chan struct{}, ticker *time.Ticker) bool {
  963. select {
  964. case <-done:
  965. return true
  966. case <-ticker.C:
  967. return false
  968. }
  969. }
  970. func (api *oliveTinAPI) sendEventStreamHeartbeat(client *streamingClient) bool {
  971. msg := &apiv1.EventStreamResponse{
  972. Event: &apiv1.EventStreamResponse_Heartbeat{
  973. Heartbeat: &apiv1.EventHeartbeat{},
  974. },
  975. }
  976. return api.trySendEventToClient(client, msg)
  977. }
  978. func (api *oliveTinAPI) removeClient(clientToRemove *streamingClient) {
  979. if clientToRemove == nil {
  980. return
  981. }
  982. api.streamingClientsMutex.Lock()
  983. if _, exists := api.streamingClients[clientToRemove]; !exists {
  984. api.streamingClientsMutex.Unlock()
  985. return
  986. }
  987. delete(api.streamingClients, clientToRemove)
  988. api.streamingClientsMutex.Unlock()
  989. clientToRemove.stopHeartbeat()
  990. close(clientToRemove.channel)
  991. }
  992. func (api *oliveTinAPI) OnActionMapRebuilt() {
  993. toRemove := []*streamingClient{}
  994. for _, client := range api.copyOfStreamingClients() {
  995. msg := &apiv1.EventStreamResponse{
  996. Event: &apiv1.EventStreamResponse_ConfigChanged{
  997. ConfigChanged: &apiv1.EventConfigChanged{},
  998. },
  999. }
  1000. if !api.trySendEventToClient(client, msg) {
  1001. toRemove = append(toRemove, client)
  1002. }
  1003. }
  1004. for _, client := range toRemove {
  1005. api.removeClient(client)
  1006. }
  1007. }
  1008. func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
  1009. toRemove := []*streamingClient{}
  1010. for _, client := range api.copyOfStreamingClients() {
  1011. api.maybeSendExecutionStarted(client, ex, &toRemove)
  1012. }
  1013. for _, client := range toRemove {
  1014. api.removeClient(client)
  1015. }
  1016. }
  1017. func (api *oliveTinAPI) maybeSendExecutionStarted(client *streamingClient, ex *executor.InternalLogEntry, toRemove *[]*streamingClient) {
  1018. if client == nil {
  1019. return
  1020. }
  1021. if !api.mayViewExecutionEvent(ex, client.AuthenticatedUser) {
  1022. return
  1023. }
  1024. msg := &apiv1.EventStreamResponse{
  1025. Event: &apiv1.EventStreamResponse_ExecutionStarted{
  1026. ExecutionStarted: &apiv1.EventExecutionStarted{
  1027. LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
  1028. },
  1029. },
  1030. }
  1031. if !api.trySendEventToClient(client, msg) {
  1032. *toRemove = append(*toRemove, client)
  1033. }
  1034. }
  1035. func (api *oliveTinAPI) OnExecutionFinished(ile *executor.InternalLogEntry) {
  1036. toRemove := []*streamingClient{}
  1037. for _, client := range api.copyOfStreamingClients() {
  1038. api.maybeSendExecutionFinished(client, ile, &toRemove)
  1039. }
  1040. for _, client := range toRemove {
  1041. api.removeClient(client)
  1042. }
  1043. }
  1044. func (api *oliveTinAPI) maybeSendExecutionFinished(client *streamingClient, ile *executor.InternalLogEntry, toRemove *[]*streamingClient) {
  1045. if client == nil {
  1046. return
  1047. }
  1048. if !api.mayViewExecutionEvent(ile, client.AuthenticatedUser) {
  1049. return
  1050. }
  1051. msg := &apiv1.EventStreamResponse{
  1052. Event: &apiv1.EventStreamResponse_ExecutionFinished{
  1053. ExecutionFinished: &apiv1.EventExecutionFinished{
  1054. LogEntry: api.internalLogEntryToPb(ile, client.AuthenticatedUser),
  1055. },
  1056. },
  1057. }
  1058. if !api.trySendEventToClient(client, msg) {
  1059. *toRemove = append(*toRemove, client)
  1060. }
  1061. }
  1062. func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[apiv1.GetDiagnosticsRequest]) (*connect.Response[apiv1.GetDiagnosticsResponse], error) {
  1063. user := auth.UserFromApiCall(ctx, req, api.cfg)
  1064. if err := api.checkDashboardAccess(user); err != nil {
  1065. return nil, err
  1066. }
  1067. if !user.EffectivePolicy.ShowDiagnostics {
  1068. return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("diagnostics are not available for your account"))
  1069. }
  1070. res := &apiv1.GetDiagnosticsResponse{
  1071. SshFoundKey: installationinfo.Runtime.SshFoundKey,
  1072. SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
  1073. ConfigIssues: api.buildConfigIssuesForUser(user),
  1074. }
  1075. return connect.NewResponse(res), nil
  1076. }
  1077. func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitRequest]) (*connect.Response[apiv1.InitResponse], error) {
  1078. user := auth.UserFromApiCall(ctx, req, api.cfg)
  1079. return connect.NewResponse(api.buildInitResponse(user)), nil
  1080. }
  1081. func (api *oliveTinAPI) buildInitResponse(user *authpublic.AuthenticatedUser) *apiv1.InitResponse {
  1082. loginRequired := user.IsGuest() && api.cfg.AuthRequireGuestsToLogin
  1083. currentVersion, availableVersion, showNewVersions := initVersionFields(user, api.cfg)
  1084. rootDashboardEntries := api.buildRootDashboardEntries(user, api.cfg.Dashboards)
  1085. res := &apiv1.InitResponse{
  1086. ShowFooter: api.cfg.ShowFooter,
  1087. ShowNavigation: api.cfg.ShowNavigation,
  1088. ShowNewVersions: showNewVersions,
  1089. AvailableVersion: availableVersion,
  1090. CurrentVersion: currentVersion,
  1091. PageTitle: api.cfg.PageTitle,
  1092. SectionNavigationStyle: api.cfg.SectionNavigationStyle,
  1093. DefaultIconForBack: api.cfg.DefaultIconForBack,
  1094. EnableCustomJs: api.cfg.EnableCustomJs,
  1095. AuthLoginUrl: api.cfg.AuthLoginUrl,
  1096. AuthLocalLogin: api.cfg.AuthLocalUsers.Enabled,
  1097. OAuth2Providers: buildPublicOAuth2ProvidersList(api.cfg),
  1098. AdditionalLinks: buildAdditionalLinks(api.cfg.AdditionalNavigationLinks),
  1099. StyleMods: api.cfg.StyleMods,
  1100. RootDashboards: rootDashboardTitles(rootDashboardEntries),
  1101. RootDashboardEntries: rootDashboardEntries,
  1102. AuthenticatedUser: user.Username,
  1103. AuthenticatedUserProvider: user.Provider,
  1104. EffectivePolicy: buildEffectivePolicy(user.EffectivePolicy),
  1105. BannerMessage: api.cfg.BannerMessage,
  1106. BannerCss: api.cfg.BannerCSS,
  1107. ShowDiagnostics: user.EffectivePolicy.ShowDiagnostics,
  1108. ShowLogList: user.EffectivePolicy.ShowLogList,
  1109. LoginRequired: loginRequired,
  1110. AvailableThemes: discoverAvailableThemes(api.cfg),
  1111. ShowNavigateOnStartIcons: api.cfg.ShowNavigateOnStartIcons,
  1112. ConfigIssueCount: configIssueCountForUser(api, user),
  1113. Features: &apiv1.Features{
  1114. HeaderSearch: api.cfg.Features.HeaderSearch,
  1115. },
  1116. SearchHints: api.initSearchHints(user, loginRequired),
  1117. }
  1118. return res
  1119. }
  1120. func initVersionFields(user *authpublic.AuthenticatedUser, cfg *config.Config) (currentVersion string, availableVersion string, showNewVersions bool) {
  1121. if !user.EffectivePolicy.ShowVersionNumber {
  1122. return "", "", false
  1123. }
  1124. return installationinfo.Build.Version, installationinfo.Runtime.AvailableVersion, cfg.ShowNewVersions
  1125. }
  1126. func (api *oliveTinAPI) initSearchHints(user *authpublic.AuthenticatedUser, loginRequired bool) *apiv1.SearchHints {
  1127. if loginRequired || !api.cfg.Features.HeaderSearch {
  1128. return nil
  1129. }
  1130. return api.buildSearchHints(user)
  1131. }
  1132. // discoverAvailableThemes finds all available themes in the custom-webui/themes directory.
  1133. // A theme is considered available if it has a theme.css file.
  1134. func discoverAvailableThemes(cfg *config.Config) []string {
  1135. configDir := cfg.GetDir()
  1136. if configDir == "" {
  1137. return []string{}
  1138. }
  1139. themesDir := path.Join(configDir, "custom-webui", "themes")
  1140. entries, err := os.ReadDir(themesDir)
  1141. if err != nil {
  1142. log.WithFields(log.Fields{
  1143. "themesDir": themesDir,
  1144. "error": err,
  1145. }).Tracef("Could not read themes directory")
  1146. return []string{}
  1147. }
  1148. themes := collectValidThemes(themesDir, entries)
  1149. sort.Strings(themes)
  1150. return themes
  1151. }
  1152. // collectValidThemes collects theme names from directory entries that have a theme.css file.
  1153. func collectValidThemes(themesDir string, entries []os.DirEntry) []string {
  1154. var themes []string
  1155. for _, entry := range entries {
  1156. if themeName := getValidThemeName(themesDir, entry); themeName != "" {
  1157. themes = append(themes, themeName)
  1158. }
  1159. }
  1160. return themes
  1161. }
  1162. // getValidThemeName returns the theme name if the entry is a valid theme directory with theme.css, otherwise returns empty string.
  1163. func getValidThemeName(themesDir string, entry os.DirEntry) string {
  1164. if !entry.IsDir() {
  1165. return ""
  1166. }
  1167. themeName := entry.Name()
  1168. themeCssPath := path.Join(themesDir, themeName, "theme.css")
  1169. if _, err := os.Stat(themeCssPath); err != nil {
  1170. return ""
  1171. }
  1172. return themeName
  1173. }
  1174. func (api *oliveTinAPI) buildRootDashboards(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []string {
  1175. return rootDashboardTitles(api.buildRootDashboardEntries(user, dashboards))
  1176. }
  1177. func rootDashboardTitles(entries []*apiv1.RootDashboard) []string {
  1178. titles := make([]string, 0, len(entries))
  1179. for _, entry := range entries {
  1180. titles = append(titles, entry.Title)
  1181. }
  1182. return titles
  1183. }
  1184. func (api *oliveTinAPI) buildRootDashboardEntries(user *authpublic.AuthenticatedUser, dashboards []*config.DashboardComponent) []*apiv1.RootDashboard {
  1185. var entries []*apiv1.RootDashboard
  1186. dashboardRenderRequest := api.createDashboardRenderRequest(user, "", "")
  1187. api.addDefaultDashboardEntryIfNeeded(&entries, dashboardRenderRequest)
  1188. api.addCustomDashboardEntries(&entries, dashboards, dashboardRenderRequest)
  1189. return entries
  1190. }
  1191. func (api *oliveTinAPI) addDefaultDashboardEntryIfNeeded(entries *[]*apiv1.RootDashboard, rr *DashboardRenderRequest) {
  1192. defaultDashboard := buildDefaultDashboard(rr)
  1193. if defaultDashboard != nil && len(defaultDashboard.Contents) > 0 {
  1194. log.Tracef("defaultDashboard: %+v", defaultDashboard.Contents)
  1195. *entries = append(*entries, &apiv1.RootDashboard{Title: "Actions"})
  1196. }
  1197. }
  1198. func (api *oliveTinAPI) addCustomDashboardEntries(entries *[]*apiv1.RootDashboard, dashboards []*config.DashboardComponent, rr *DashboardRenderRequest) {
  1199. for _, dashboard := range dashboards {
  1200. // We have to build the dashboard response instead of just looping over config.dashboards,
  1201. // because we need to check if the user has access to the dashboard
  1202. renderedDashboard := renderDashboard(rr, dashboard.Title)
  1203. if renderedDashboard != nil {
  1204. *entries = append(*entries, &apiv1.RootDashboard{
  1205. Title: dashboard.Title,
  1206. Category: dashboard.Category,
  1207. })
  1208. }
  1209. }
  1210. }
  1211. func buildPublicOAuth2ProvidersList(cfg *config.Config) []*apiv1.OAuth2Provider {
  1212. var publicProviders []*apiv1.OAuth2Provider
  1213. for providerKey, provider := range cfg.AuthOAuth2Providers {
  1214. publicProviders = append(publicProviders, &apiv1.OAuth2Provider{
  1215. Title: provider.Title,
  1216. Icon: provider.Icon,
  1217. Key: providerKey,
  1218. })
  1219. }
  1220. sort.Slice(publicProviders, func(i, j int) bool {
  1221. return publicProviders[i].Key < publicProviders[j].Key
  1222. })
  1223. return publicProviders
  1224. }
  1225. func buildAdditionalLinks(links []*config.NavigationLink) []*apiv1.AdditionalLink {
  1226. var additionalLinks []*apiv1.AdditionalLink
  1227. for _, link := range links {
  1228. additionalLinks = append(additionalLinks, &apiv1.AdditionalLink{
  1229. Title: link.Title,
  1230. Url: link.Url,
  1231. })
  1232. }
  1233. return additionalLinks
  1234. }
  1235. func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string) {
  1236. entry := api.getValidLogEntryForStreaming(executionTrackingId)
  1237. if entry == nil {
  1238. return
  1239. }
  1240. msg := &apiv1.EventStreamResponse{
  1241. Event: &apiv1.EventStreamResponse_OutputChunk{
  1242. OutputChunk: &apiv1.EventOutputChunk{
  1243. Output: string(content),
  1244. ExecutionTrackingId: executionTrackingId,
  1245. },
  1246. },
  1247. }
  1248. toRemove := []*streamingClient{}
  1249. for _, client := range api.copyOfStreamingClients() {
  1250. api.maybeSendOutputChunk(client, entry, msg, &toRemove)
  1251. }
  1252. for _, client := range toRemove {
  1253. api.removeClient(client)
  1254. }
  1255. }
  1256. func (api *oliveTinAPI) getValidLogEntryForStreaming(executionTrackingId string) *executor.InternalLogEntry {
  1257. entry, ok := api.executor.GetLog(executionTrackingId)
  1258. if !ok || !isValidLogEntry(entry) {
  1259. return nil
  1260. }
  1261. return entry
  1262. }
  1263. func (api *oliveTinAPI) maybeSendOutputChunk(client *streamingClient, entry *executor.InternalLogEntry, msg *apiv1.EventStreamResponse, toRemove *[]*streamingClient) {
  1264. if client == nil {
  1265. return
  1266. }
  1267. if !api.mayViewExecutionEvent(entry, client.AuthenticatedUser) {
  1268. return
  1269. }
  1270. if !api.trySendEventToClient(client, msg) {
  1271. *toRemove = append(*toRemove, client)
  1272. }
  1273. }
  1274. func (api *oliveTinAPI) GetEntities(ctx ctx.Context, req *connect.Request[apiv1.GetEntitiesRequest]) (*connect.Response[apiv1.GetEntitiesResponse], error) {
  1275. user := auth.UserFromApiCall(ctx, req, api.cfg)
  1276. if err := api.checkDashboardAccess(user); err != nil {
  1277. return nil, err
  1278. }
  1279. entityMap := entities.GetEntities()
  1280. entityDefinitions := api.buildEntityDefinitionsResponse(user, req.Msg, entityMap)
  1281. res := &apiv1.GetEntitiesResponse{
  1282. EntityDefinitions: entityDefinitions,
  1283. }
  1284. return connect.NewResponse(res), nil
  1285. }
  1286. func buildSortedEntityInstances(entityType string, entityInstances map[string]*entities.Entity, properties []config.EntityProperty) []*apiv1.Entity {
  1287. instanceKeys := make([]string, 0, len(entityInstances))
  1288. for key := range entityInstances {
  1289. instanceKeys = append(instanceKeys, key)
  1290. }
  1291. sort.Strings(instanceKeys)
  1292. instances := make([]*apiv1.Entity, 0, len(instanceKeys))
  1293. for _, key := range instanceKeys {
  1294. e := entityInstances[key]
  1295. instances = append(instances, &apiv1.Entity{
  1296. Title: e.Title,
  1297. UniqueKey: e.UniqueKey,
  1298. Type: entityType,
  1299. Fields: entityListFields(e.Data, properties),
  1300. })
  1301. }
  1302. return instances
  1303. }
  1304. func findDashboardsForEntity(entityTitle string, dashboards []*config.DashboardComponent) []string {
  1305. var foundDashboards []string
  1306. seen := make(map[string]bool)
  1307. findEntityInComponents(entityTitle, "", dashboards, &foundDashboards, seen)
  1308. return foundDashboards
  1309. }
  1310. func findEntityInComponents(entityTitle string, parentTitle string, components []*config.DashboardComponent, foundDashboards *[]string, seen map[string]bool) {
  1311. for _, component := range components {
  1312. if component.Entity == entityTitle {
  1313. addEntityDashboard(component, parentTitle, foundDashboards, seen)
  1314. }
  1315. if len(component.Contents) > 0 {
  1316. findEntityInComponents(entityTitle, component.Title, component.Contents, foundDashboards, seen)
  1317. }
  1318. }
  1319. }
  1320. func addEntityDashboard(component *config.DashboardComponent, parentTitle string, foundDashboards *[]string, seen map[string]bool) {
  1321. if component.Type == "directory" {
  1322. addEntityDirectory(component, foundDashboards, seen)
  1323. } else {
  1324. addParentDashboard(parentTitle, foundDashboards, seen)
  1325. }
  1326. }
  1327. func addEntityDirectory(component *config.DashboardComponent, foundDashboards *[]string, seen map[string]bool) {
  1328. dashboardTitle := component.Title + " [Entity Directory]"
  1329. if !seen[dashboardTitle] {
  1330. *foundDashboards = append(*foundDashboards, dashboardTitle)
  1331. seen[dashboardTitle] = true
  1332. seen[component.Title] = true
  1333. }
  1334. }
  1335. func addParentDashboard(parentTitle string, foundDashboards *[]string, seen map[string]bool) {
  1336. if parentTitle != "" && !seen[parentTitle] {
  1337. *foundDashboards = append(*foundDashboards, parentTitle)
  1338. seen[parentTitle] = true
  1339. }
  1340. }
  1341. func findDirectoriesInEntityFieldsets(entityType string, dashboards []*config.DashboardComponent) []string {
  1342. var directories []string
  1343. for _, dashboard := range dashboards {
  1344. findDirectoriesInEntityFieldsetsRecursive(entityType, dashboard, &directories)
  1345. }
  1346. return directories
  1347. }
  1348. func findDirectoriesInEntityFieldsetsRecursive(entityType string, component *config.DashboardComponent, directories *[]string) {
  1349. if component.Entity == entityType {
  1350. collectDirectoriesFromComponent(component, directories)
  1351. }
  1352. if len(component.Contents) > 0 {
  1353. searchSubcomponentsForDirectories(entityType, component.Contents, directories)
  1354. }
  1355. }
  1356. func collectDirectoriesFromComponent(component *config.DashboardComponent, directories *[]string) {
  1357. for _, subitem := range component.Contents {
  1358. if subitem.Type == "directory" {
  1359. *directories = append(*directories, subitem.Title)
  1360. }
  1361. }
  1362. }
  1363. func searchSubcomponentsForDirectories(entityType string, contents []*config.DashboardComponent, directories *[]string) {
  1364. for _, subitem := range contents {
  1365. findDirectoriesInEntityFieldsetsRecursive(entityType, subitem, directories)
  1366. }
  1367. }
  1368. func (api *oliveTinAPI) GetEntity(ctx ctx.Context, req *connect.Request[apiv1.GetEntityRequest]) (*connect.Response[apiv1.Entity], error) {
  1369. user := auth.UserFromApiCall(ctx, req, api.cfg)
  1370. if err := api.checkDashboardAccess(user); err != nil {
  1371. return nil, err
  1372. }
  1373. instances := entities.GetEntityInstances(req.Msg.Type)
  1374. if len(instances) == 0 {
  1375. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity type %s not found", req.Msg.Type))
  1376. }
  1377. if !api.userCanViewEntityType(user, req.Msg.Type) {
  1378. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity type %s not found", req.Msg.Type))
  1379. }
  1380. entity, ok := instances[req.Msg.UniqueKey]
  1381. if !ok {
  1382. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("entity with unique key %s not found in type %s", req.Msg.UniqueKey, req.Msg.Type))
  1383. }
  1384. res := buildEntityResponse(entity, req.Msg.Type, api.cfg)
  1385. res.RelatedActions = api.relatedActionsForEntity(user, req.Msg.Type, entity)
  1386. return connect.NewResponse(res), nil
  1387. }
  1388. func entityTypeIcon(cfg *config.Config, entityType string) string {
  1389. entityFile := entityFileForType(cfg, entityType)
  1390. if entityFile == nil {
  1391. return ""
  1392. }
  1393. return entityFile.Icon
  1394. }
  1395. func entityFileForType(cfg *config.Config, entityType string) *config.EntityFile {
  1396. for _, entityFile := range cfg.Entities {
  1397. if entityFile != nil && entityFile.Name == entityType {
  1398. return entityFile
  1399. }
  1400. }
  1401. return nil
  1402. }
  1403. func entityPropertiesFromFile(entityFile *config.EntityFile) []config.EntityProperty {
  1404. if entityFile == nil {
  1405. return nil
  1406. }
  1407. return entityFile.Properties
  1408. }
  1409. func entityDefinitionProperties(properties []config.EntityProperty) []*apiv1.EntityProperty {
  1410. if len(properties) == 0 {
  1411. return nil
  1412. }
  1413. result := make([]*apiv1.EntityProperty, 0, len(properties))
  1414. for _, property := range properties {
  1415. result = append(result, &apiv1.EntityProperty{
  1416. Name: property.Name,
  1417. Title: property.Title,
  1418. })
  1419. }
  1420. return result
  1421. }
  1422. func entityListFields(data any, properties []config.EntityProperty) map[string]string {
  1423. if len(properties) == 0 {
  1424. return nil
  1425. }
  1426. fields := make(map[string]string, len(properties))
  1427. for _, property := range properties {
  1428. fields[property.Name] = entityPropertyValue(data, property.Name)
  1429. }
  1430. return fields
  1431. }
  1432. func entityPropertyValue(data any, propertyName string) string {
  1433. dataMap, ok := data.(map[string]any)
  1434. if !ok {
  1435. return ""
  1436. }
  1437. if value, found := dataMap[propertyName]; found {
  1438. return fmt.Sprintf("%v", value)
  1439. }
  1440. return entityPropertyValueCaseInsensitive(dataMap, propertyName)
  1441. }
  1442. func entityPropertyValueCaseInsensitive(dataMap map[string]any, propertyName string) string {
  1443. propertyNameLower := strings.ToLower(propertyName)
  1444. for key, value := range dataMap {
  1445. if strings.ToLower(key) == propertyNameLower {
  1446. return fmt.Sprintf("%v", value)
  1447. }
  1448. }
  1449. return ""
  1450. }
  1451. func buildEntityResponse(entity *entities.Entity, entityType string, cfg *config.Config) *apiv1.Entity {
  1452. properties := entityPropertiesFromFile(entityFileForType(cfg, entityType))
  1453. res := &apiv1.Entity{
  1454. Title: entity.Title,
  1455. UniqueKey: entity.UniqueKey,
  1456. Type: entityType,
  1457. Directories: findDirectoriesInEntityFieldsets(entityType, cfg.Dashboards),
  1458. Fields: entityFieldsForResponse(entity.Data, properties),
  1459. Icon: entityTypeIcon(cfg, entityType),
  1460. }
  1461. return res
  1462. }
  1463. func serializeEntityFields(data any) map[string]string {
  1464. if data == nil {
  1465. return nil
  1466. }
  1467. dataMap, ok := data.(map[string]any)
  1468. if !ok {
  1469. return nil
  1470. }
  1471. fields := make(map[string]string)
  1472. for k, v := range dataMap {
  1473. fields[k] = fmt.Sprintf("%v", v)
  1474. }
  1475. return fields
  1476. }
  1477. func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv1.RestartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {
  1478. execReqLogEntry, err := api.restartActionLogEntry(req.Msg.ExecutionTrackingId)
  1479. if err != nil {
  1480. return nil, err
  1481. }
  1482. if err := validateRestartLogEntry(execReqLogEntry); err != nil {
  1483. return nil, err
  1484. }
  1485. authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg)
  1486. restartArgs := copyStringMap(execReqLogEntry.Arguments)
  1487. if err := api.errUnlessStartEntityAccessAllowed(authenticatedUser, execReqLogEntry.Binding, restartArgs); err != nil {
  1488. return nil, err
  1489. }
  1490. execReq := executor.ExecutionRequest{
  1491. Binding: execReqLogEntry.Binding,
  1492. Arguments: restartArgs,
  1493. Justification: execReqLogEntry.Justification,
  1494. AuthenticatedUser: authenticatedUser,
  1495. Cfg: api.cfg,
  1496. }
  1497. api.executor.ExecRequest(&execReq)
  1498. return connect.NewResponse(&apiv1.StartActionResponse{
  1499. ExecutionTrackingId: execReq.TrackingID,
  1500. }), nil
  1501. }
  1502. func (api *oliveTinAPI) restartActionLogEntry(executionTrackingId string) (*executor.InternalLogEntry, error) {
  1503. execReqLogEntry, found := api.executor.GetLog(executionTrackingId)
  1504. if !found {
  1505. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s", executionTrackingId))
  1506. }
  1507. if execReqLogEntry.Binding == nil {
  1508. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("log entry has no binding for tracking ID %s", executionTrackingId))
  1509. }
  1510. if execReqLogEntry.Binding.Action == nil {
  1511. return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action not found for tracking ID %s", executionTrackingId))
  1512. }
  1513. return execReqLogEntry, nil
  1514. }
  1515. var (
  1516. executorListenersMu sync.Mutex
  1517. executorListeners = map[*executor.Executor]*oliveTinAPI{}
  1518. )
  1519. // RegisterExecutorListener registers the API server as an executor listener during startup.
  1520. // Call this before background goroutines that may trigger RebuildActionMap.
  1521. func RegisterExecutorListener(ex *executor.Executor) {
  1522. ensureExecutorListener(ex)
  1523. }
  1524. func ensureExecutorListener(ex *executor.Executor) *oliveTinAPI {
  1525. executorListenersMu.Lock()
  1526. defer executorListenersMu.Unlock()
  1527. if server, ok := executorListeners[ex]; ok {
  1528. return server
  1529. }
  1530. server := newServer(ex)
  1531. executorListeners[ex] = server
  1532. return server
  1533. }
  1534. func newServer(ex *executor.Executor) *oliveTinAPI {
  1535. server := &oliveTinAPI{
  1536. cfg: ex.Cfg,
  1537. executor: ex,
  1538. streamingClients: make(map[*streamingClient]struct{}),
  1539. }
  1540. ex.AddListener(server)
  1541. return server
  1542. }
  1543. func GetNewHandler(ex *executor.Executor) (string, http.Handler) {
  1544. server := ensureExecutorListener(ex)
  1545. jsonOpt := connectproto.WithJSON(
  1546. protojson.MarshalOptions{
  1547. EmitUnpopulated: true, // https://github.com/OliveTin/OliveTin/issues/674
  1548. },
  1549. protojson.UnmarshalOptions{
  1550. DiscardUnknown: true,
  1551. },
  1552. )
  1553. return apiv1connect.NewOliveTinApiServiceHandler(server, jsonOpt)
  1554. }