| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595 |
- package api
- import (
- ctx "context"
- "connectrpc.com/connect"
- apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
- apiv1connect "github.com/OliveTin/OliveTin/gen/olivetin/api/v1/apiv1connect"
- "github.com/google/uuid"
- log "github.com/sirupsen/logrus"
- "fmt"
- "net/http"
- acl "github.com/OliveTin/OliveTin/internal/acl"
- config "github.com/OliveTin/OliveTin/internal/config"
- executor "github.com/OliveTin/OliveTin/internal/executor"
- installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
- sv "github.com/OliveTin/OliveTin/internal/stringvariables"
- )
- type oliveTinAPI struct {
- executor *executor.Executor
- cfg *config.Config
- connectedClients []*connectedClients
- }
- type connectedClients struct {
- channel chan *apiv1.EventStreamResponse
- AuthenticatedUser *acl.AuthenticatedUser
- }
- func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.KillActionRequest]) (*connect.Response[apiv1.KillActionResponse], error) {
- ret := &apiv1.KillActionResponse{
- ExecutionTrackingId: req.Msg.ExecutionTrackingId,
- }
- var execReqLogEntry *executor.InternalLogEntry
- execReqLogEntry, ret.Found = api.executor.GetLog(req.Msg.ExecutionTrackingId)
- if !ret.Found {
- log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.Msg.ExecutionTrackingId)
- return connect.NewResponse(ret), nil
- }
- log.Warnf("Killing execution request by tracking ID: %v", req.Msg.ExecutionTrackingId)
- action := api.cfg.FindAction(execReqLogEntry.ActionTitle)
- if action == nil {
- log.Warnf("Killing execution request not possible - action not found: %v", execReqLogEntry.ActionTitle)
- ret.Killed = false
- return connect.NewResponse(ret), nil
- }
- user := acl.UserFromContext(ctx, api.cfg)
- api.killActionByTrackingId(user, action, execReqLogEntry, ret)
- return connect.NewResponse(ret), nil
- }
- func (api *oliveTinAPI) killActionByTrackingId(user *acl.AuthenticatedUser, action *config.Action, execReqLogEntry *executor.InternalLogEntry, ret *apiv1.KillActionResponse) {
- if !acl.IsAllowedKill(api.cfg, user, action) {
- log.Warnf("Killing execution request not possible - user not allowed to kill this action: %v", execReqLogEntry.ExecutionTrackingID)
- ret.Killed = false
- }
- err := api.executor.Kill(execReqLogEntry)
- if err != nil {
- log.Warnf("Killing execution request err: %v", err)
- ret.AlreadyCompleted = true
- ret.Killed = false
- } else {
- ret.Killed = true
- }
- }
- func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *connect.Request[apiv1.StartActionRequest]) (*connect.Response[apiv1.StartActionResponse], error) {
- args := make(map[string]string)
- for _, arg := range req.Msg.Arguments {
- args[arg.Name] = arg.Value
- }
- api.executor.MapActionIdToBindingLock.RLock()
- pair := api.executor.MapActionIdToBinding[req.Msg.ActionId]
- api.executor.MapActionIdToBindingLock.RUnlock()
- if pair == nil || pair.Action == nil {
- return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("action with ID %s not found", req.Msg.ActionId))
- }
- authenticatedUser := acl.UserFromContext(ctx, api.cfg)
- execReq := executor.ExecutionRequest{
- Action: pair.Action,
- EntityPrefix: pair.EntityPrefix,
- TrackingID: req.Msg.UniqueTrackingId,
- Arguments: args,
- AuthenticatedUser: authenticatedUser,
- Cfg: api.cfg,
- }
- api.executor.ExecRequest(&execReq)
- ret := &apiv1.StartActionResponse{
- ExecutionTrackingId: execReq.TrackingID,
- }
- return connect.NewResponse(ret), nil
- }
- func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1.PasswordHashRequest]) (*connect.Response[apiv1.PasswordHashResponse], error) {
- hash, err := createHash(req.Msg.Password)
- if err != nil {
- return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("error creating hash: %w", err))
- }
- ret := &apiv1.PasswordHashResponse{
- Hash: hash,
- }
- return connect.NewResponse(ret), nil
- }
- func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) {
- match := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password)
- if match {
- //grpc.SendHeader(ctx, metadata.Pairs("set-username", req.Username))
- log.WithFields(log.Fields{
- "username": req.Msg.Username,
- }).Info("LocalUserLogin: User logged in successfully.")
- } else {
- log.WithFields(log.Fields{
- "username": req.Msg.Username,
- }).Warn("LocalUserLogin: User login failed.")
- }
- return connect.NewResponse(&apiv1.LocalUserLoginResponse{
- Success: match,
- }), nil
- }
- func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionAndWaitRequest]) (*connect.Response[apiv1.StartActionAndWaitResponse], error) {
- args := make(map[string]string)
- for _, arg := range req.Msg.Arguments {
- args[arg.Name] = arg.Value
- }
- user := acl.UserFromContext(ctx, api.cfg)
- execReq := executor.ExecutionRequest{
- Action: api.executor.FindActionBindingByID(req.Msg.ActionId),
- TrackingID: uuid.NewString(),
- Arguments: args,
- AuthenticatedUser: user,
- Cfg: api.cfg,
- }
- wg, _ := api.executor.ExecRequest(&execReq)
- wg.Wait()
- internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
- if ok {
- return connect.NewResponse(&apiv1.StartActionAndWaitResponse{
- LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
- }), nil
- } else {
- return nil, fmt.Errorf("execution not found")
- }
- }
- func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetRequest]) (*connect.Response[apiv1.StartActionByGetResponse], error) {
- args := make(map[string]string)
- execReq := executor.ExecutionRequest{
- Action: api.executor.FindActionBindingByID(req.Msg.ActionId),
- TrackingID: uuid.NewString(),
- Arguments: args,
- AuthenticatedUser: acl.UserFromContext(ctx, api.cfg),
- Cfg: api.cfg,
- }
- _, uniqueTrackingId := api.executor.ExecRequest(&execReq)
- return connect.NewResponse(&apiv1.StartActionByGetResponse{
- ExecutionTrackingId: uniqueTrackingId,
- }), nil
- }
- func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *connect.Request[apiv1.StartActionByGetAndWaitRequest]) (*connect.Response[apiv1.StartActionByGetAndWaitResponse], error) {
- args := make(map[string]string)
- user := acl.UserFromContext(ctx, api.cfg)
- execReq := executor.ExecutionRequest{
- Action: api.executor.FindActionBindingByID(req.Msg.ActionId),
- TrackingID: uuid.NewString(),
- Arguments: args,
- AuthenticatedUser: user,
- Cfg: api.cfg,
- }
- wg, _ := api.executor.ExecRequest(&execReq)
- wg.Wait()
- internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
- if ok {
- return connect.NewResponse(&apiv1.StartActionByGetAndWaitResponse{
- LogEntry: api.internalLogEntryToPb(internalLogEntry, user),
- }), nil
- } else {
- return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found"))
- }
- }
- func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry, authenticatedUser *acl.AuthenticatedUser) *apiv1.LogEntry {
- pble := &apiv1.LogEntry{
- ActionTitle: logEntry.ActionTitle,
- ActionIcon: logEntry.ActionIcon,
- ActionId: logEntry.ActionId,
- DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
- DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
- DatetimeIndex: logEntry.Index,
- Output: logEntry.Output,
- TimedOut: logEntry.TimedOut,
- Blocked: logEntry.Blocked,
- ExitCode: logEntry.ExitCode,
- Tags: logEntry.Tags,
- ExecutionTrackingId: logEntry.ExecutionTrackingID,
- ExecutionStarted: logEntry.ExecutionStarted,
- ExecutionFinished: logEntry.ExecutionFinished,
- User: logEntry.Username,
- }
- if !pble.ExecutionFinished {
- pble.CanKill = acl.IsAllowedKill(api.cfg, authenticatedUser, api.cfg.FindAction(logEntry.ActionConfigTitle))
- }
- return pble
- }
- func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
- logEntry, ok := api.executor.GetLog(executionTrackingId)
- if !ok {
- return nil
- }
- return logEntry
- }
- func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
- var ile *executor.InternalLogEntry
- logs := api.executor.GetLogsByActionId(actionId)
- if len(logs) == 0 {
- return nil
- } else {
- // Get last log entry
- ile = logs[len(logs)-1]
- }
- return ile
- }
- func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) {
- res := &apiv1.ExecutionStatusResponse{}
- user := acl.UserFromContext(ctx, api.cfg)
- var ile *executor.InternalLogEntry
- if req.Msg.ExecutionTrackingId != "" {
- ile = getExecutionStatusByTrackingID(api, req.Msg.ExecutionTrackingId)
- } else {
- ile = getMostRecentExecutionStatusById(api, req.Msg.ActionId)
- }
- if ile == nil {
- return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s or action ID %s", req.Msg.ExecutionTrackingId, req.Msg.ActionId))
- } else {
- res.LogEntry = api.internalLogEntryToPb(ile, user)
- }
- return connect.NewResponse(res), nil
- }
- /**
- func (api *oliveTinAPI) WatchExecution(req *apiv1.WatchExecutionRequest, srv apiv1.OliveTinApi_WatchExecutionServer) error {
- log.Infof("Watch")
- if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
- log.Errorf("Execution not found: %v", req.ExecutionUuid)
- return nil
- } else {
- if logEntry.ExecutionStarted {
- for !logEntry.ExecutionCompleted {
- tmp := make([]byte, 256)
- red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
- log.Infof("%v %v", red, err)
- srv.Send(&apiv1.WatchExecutionUpdate{
- Update: string(tmp),
- })
- }
- }
- return nil
- }
- }
- */
- func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.LogoutRequest]) (*connect.Response[apiv1.LogoutResponse], error) {
- //user := acl.UserFromContext(ctx, cfg)
- //grpc.SendHeader(ctx, metadata.Pairs("logout-provider", user.Provider))
- //grpc.SendHeader(ctx, metadata.Pairs("logout-sid", user.SID))
- return nil, nil
- }
- func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *connect.Request[apiv1.GetDashboardComponentsRequest]) (*connect.Response[apiv1.GetDashboardComponentsResponse], error) {
- user := acl.UserFromContext(ctx, api.cfg)
- if user.IsGuest() && api.cfg.AuthRequireGuestsToLogin {
- return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("guests are not allowed to access the dashboard"))
- }
- res := buildDashboardResponse(api.executor, api.cfg, user)
- /*
- if len(res.Actions) == 0 {
- log.WithFields(log.Fields{
- "username": user.Username,
- "usergroupLine": user.UsergroupLine,
- "provider": user.Provider,
- "acls": user.Acls,
- "availableActions": len(api.cfg.Actions),
- }).Warn("Zero actions found for user")
- }
- */
- log.Tracef("GetDashboardComponents: %v", res)
- return connect.NewResponse(res), nil
- }
- func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetLogsRequest]) (*connect.Response[apiv1.GetLogsResponse], error) {
- user := acl.UserFromContext(ctx, api.cfg)
- ret := &apiv1.GetLogsResponse{}
- logEntries, countRemaining := api.executor.GetLogTrackingIds(req.Msg.StartOffset, api.cfg.LogHistoryPageSize)
- for _, logEntry := range logEntries {
- action := api.cfg.FindAction(logEntry.ActionTitle)
- if action == nil || acl.IsAllowedLogs(api.cfg, user, action) {
- pbLogEntry := api.internalLogEntryToPb(logEntry, user)
- ret.Logs = append(ret.Logs, pbLogEntry)
- }
- }
- ret.CountRemaining = countRemaining
- ret.PageSize = api.cfg.LogHistoryPageSize
- return connect.NewResponse(ret), nil
- }
- /*
- This function is ONLY a helper for the UI - the arguments are validated properly
- on the StartAction -> Executor chain. This is here basically to provide helpful
- error messages more quickly before starting the action.
- */
- func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *connect.Request[apiv1.ValidateArgumentTypeRequest]) (*connect.Response[apiv1.ValidateArgumentTypeResponse], error) {
- err := executor.TypeSafetyCheck("", req.Msg.Value, req.Msg.Type)
- desc := ""
- if err != nil {
- desc = err.Error()
- }
- return connect.NewResponse(&apiv1.ValidateArgumentTypeResponse{
- Valid: err == nil,
- Description: desc,
- }), nil
- }
- func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAmIRequest]) (*connect.Response[apiv1.WhoAmIResponse], error) {
- user := acl.UserFromContext(ctx, api.cfg)
- res := &apiv1.WhoAmIResponse{
- AuthenticatedUser: user.Username,
- Usergroup: user.UsergroupLine,
- Provider: user.Provider,
- Sid: user.SID,
- Acls: user.Acls,
- }
- return connect.NewResponse(res), nil
- }
- func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *connect.Request[apiv1.SosReportRequest]) (*connect.Response[apiv1.SosReportResponse], error) {
- sos := installationinfo.GetSosReport()
- if !api.cfg.InsecureAllowDumpSos {
- log.Info(sos)
- 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."
- }
- ret := &apiv1.SosReportResponse{
- Alert: sos,
- }
- return connect.NewResponse(ret), nil
- }
- func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.DumpVarsRequest]) (*connect.Response[apiv1.DumpVarsResponse], error) {
- res := &apiv1.DumpVarsResponse{}
- if !api.cfg.InsecureAllowDumpVars {
- res.Alert = "Dumping variables is not allowed by default because it is insecure."
- return connect.NewResponse(res), nil
- }
- res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
- res.Contents = sv.GetAll()
- return connect.NewResponse(res), nil
- }
- func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *connect.Request[apiv1.DumpPublicIdActionMapRequest]) (*connect.Response[apiv1.DumpPublicIdActionMapResponse], error) {
- res := &apiv1.DumpPublicIdActionMapResponse{}
- res.Contents = make(map[string]*apiv1.ActionEntityPair)
- if !api.cfg.InsecureAllowDumpActionMap {
- res.Alert = "Dumping Public IDs is disallowed."
- return connect.NewResponse(res), nil
- }
- api.executor.MapActionIdToBindingLock.RLock()
- for k, v := range api.executor.MapActionIdToBinding {
- res.Contents[k] = &apiv1.ActionEntityPair{
- ActionTitle: v.Action.Title,
- EntityPrefix: v.EntityPrefix,
- }
- }
- api.executor.MapActionIdToBindingLock.RUnlock()
- res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
- return connect.NewResponse(res), nil
- }
- func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *connect.Request[apiv1.GetReadyzRequest]) (*connect.Response[apiv1.GetReadyzResponse], error) {
- res := &apiv1.GetReadyzResponse{
- Status: "OK",
- }
- return connect.NewResponse(res), nil
- }
- func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.EventStreamRequest], srv *connect.ServerStream[apiv1.EventStreamResponse]) error {
- log.Debugf("EventStream: %v", req.Msg)
- client := &connectedClients{
- channel: make(chan *apiv1.EventStreamResponse, 10), // Buffered channel to hold Events
- AuthenticatedUser: acl.UserFromContext(ctx, api.cfg),
- }
- log.Infof("EventStream: client connected: %v", client.AuthenticatedUser.Username)
- api.connectedClients = append(api.connectedClients, client)
- // loop over client channel and send events to connectedClient
- for msg := range client.channel {
- log.Debugf("Sending event to client: %v", msg)
- if err := srv.Send(msg); err != nil {
- log.Errorf("Error sending event to client: %v", err)
- }
- }
- log.Infof("EventStream: client disconnected")
- return nil
- }
- func (api *oliveTinAPI) OnActionMapRebuilt() {
- for _, client := range api.connectedClients {
- select {
- case client.channel <- &apiv1.EventStreamResponse{
- Event: &apiv1.EventStreamResponse_ConfigChanged{
- ConfigChanged: &apiv1.EventConfigChanged{},
- },
- }:
- default:
- log.Warnf("EventStream: client channel is full, dropping message")
- }
- }
- }
- func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
- for _, client := range api.connectedClients {
- select {
- case client.channel <- &apiv1.EventStreamResponse{
- Event: &apiv1.EventStreamResponse_ExecutionStarted{
- ExecutionStarted: &apiv1.EventExecutionStarted{
- LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
- },
- },
- }:
- default:
- log.Warnf("EventStream: client channel is full, dropping message")
- }
- }
- }
- func (api *oliveTinAPI) OnExecutionFinished(ex *executor.InternalLogEntry) {
- for _, client := range api.connectedClients {
- select {
- case client.channel <- &apiv1.EventStreamResponse{
- Event: &apiv1.EventStreamResponse_ExecutionFinished{
- ExecutionFinished: &apiv1.EventExecutionFinished{
- LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
- },
- },
- }:
- default:
- log.Warnf("EventStream: client channel is full, dropping message")
- }
- }
- }
- func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[apiv1.GetDiagnosticsRequest]) (*connect.Response[apiv1.GetDiagnosticsResponse], error) {
- res := &apiv1.GetDiagnosticsResponse{
- SshFoundKey: installationinfo.Runtime.SshFoundKey,
- SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
- }
- return connect.NewResponse(res), nil
- }
- func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string) {
- for _, client := range api.connectedClients {
- select {
- case client.channel <- &apiv1.EventStreamResponse{
- Event: &apiv1.EventStreamResponse_OutputChunk{
- OutputChunk: &apiv1.EventOutputChunk{
- Output: string(content),
- ExecutionTrackingId: executionTrackingId,
- },
- },
- }:
- default:
- log.Warnf("EventStream: client channel is full, dropping message")
- }
- }
- }
- func newServer(ex *executor.Executor) *oliveTinAPI {
- server := oliveTinAPI{}
- server.cfg = ex.Cfg
- server.executor = ex
- ex.AddListener(&server)
- return &server
- }
- func GetNewHandler(ex *executor.Executor) (string, http.Handler) {
- server := newServer(ex)
- return apiv1connect.NewOliveTinApiServiceHandler(server)
- }
|