api.go 64 KB

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