api.go 49 KB

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