4
0

api.go 53 KB

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