api.go 62 KB

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