api.go 51 KB

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