github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/jobs/jobs.go (about)

     1  package jobs
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"io"
     7  	"net/http"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/justincampbell/bigduration"
    13  
    14  	"github.com/cozy/cozy-stack/model/bi"
    15  	"github.com/cozy/cozy-stack/model/instance"
    16  	"github.com/cozy/cozy-stack/model/job"
    17  	"github.com/cozy/cozy-stack/model/permission"
    18  	"github.com/cozy/cozy-stack/model/settings"
    19  	"github.com/cozy/cozy-stack/pkg/config/config"
    20  	"github.com/cozy/cozy-stack/pkg/consts"
    21  	"github.com/cozy/cozy-stack/pkg/couchdb"
    22  	"github.com/cozy/cozy-stack/pkg/emailer"
    23  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    24  	"github.com/cozy/cozy-stack/pkg/limits"
    25  	"github.com/cozy/cozy-stack/pkg/mail"
    26  	"github.com/cozy/cozy-stack/pkg/metadata"
    27  	"github.com/cozy/cozy-stack/web/middlewares"
    28  	multierror "github.com/hashicorp/go-multierror"
    29  	"github.com/labstack/echo/v4"
    30  
    31  	// import workers
    32  	_ "github.com/cozy/cozy-stack/worker/archive"
    33  	"github.com/cozy/cozy-stack/worker/exec"
    34  	_ "github.com/cozy/cozy-stack/worker/log"
    35  	_ "github.com/cozy/cozy-stack/worker/mails"
    36  	_ "github.com/cozy/cozy-stack/worker/migrations"
    37  	_ "github.com/cozy/cozy-stack/worker/moves"
    38  	_ "github.com/cozy/cozy-stack/worker/notes"
    39  	_ "github.com/cozy/cozy-stack/worker/oauth"
    40  	_ "github.com/cozy/cozy-stack/worker/push"
    41  	_ "github.com/cozy/cozy-stack/worker/share"
    42  	_ "github.com/cozy/cozy-stack/worker/sms"
    43  	_ "github.com/cozy/cozy-stack/worker/thumbnail"
    44  	_ "github.com/cozy/cozy-stack/worker/trash"
    45  )
    46  
    47  type (
    48  	apiJob struct {
    49  		j *job.Job
    50  	}
    51  	apiJobRequest struct {
    52  		Arguments   json.RawMessage `json:"arguments"`
    53  		Manual      bool            `json:"manual"`
    54  		ForwardLogs bool            `json:"forward_logs"`
    55  		Options     *apiJobOptions  `json:"options"`
    56  	}
    57  	apiJobOptions struct {
    58  		MaxExecCount int `json:"max_exec_count"`
    59  		Timeout      int `json:"timeout"`
    60  	}
    61  	apiSupport struct {
    62  		Arguments map[string]string `json:"arguments"`
    63  	}
    64  	apiCampaign struct {
    65  		Arguments apiCampaignArgs `json:"arguments"`
    66  	}
    67  	apiCampaignArgs struct {
    68  		Subject string      `json:"subject"`
    69  		Parts   []mail.Part `json:"parts"`
    70  	}
    71  	apiQueue struct {
    72  		workerType string
    73  	}
    74  	// apiTrigger is the jsonapi representation for a trigger
    75  	apiTrigger struct {
    76  		t    *job.TriggerInfos
    77  		inst *instance.Instance
    78  	}
    79  	apiTriggerState struct {
    80  		t *job.TriggerInfos
    81  		s *job.TriggerState
    82  	}
    83  	apiTriggerRequest struct {
    84  		Type            string          `json:"type"`
    85  		Arguments       string          `json:"arguments"`
    86  		WorkerType      string          `json:"worker"`
    87  		Message         json.RawMessage `json:"message"`
    88  		WorkerArguments json.RawMessage `json:"worker_arguments"`
    89  		Debounce        string          `json:"debounce"`
    90  		Options         *job.JobOptions `json:"options"`
    91  	}
    92  )
    93  
    94  func (j apiJob) ID() string                             { return j.j.ID() }
    95  func (j apiJob) Rev() string                            { return j.j.Rev() }
    96  func (j apiJob) DocType() string                        { return consts.Jobs }
    97  func (j apiJob) Clone() couchdb.Doc                     { return j }
    98  func (j apiJob) SetID(_ string)                         {}
    99  func (j apiJob) SetRev(_ string)                        {}
   100  func (j apiJob) Relationships() jsonapi.RelationshipMap { return nil }
   101  func (j apiJob) Included() []jsonapi.Object             { return nil }
   102  func (j apiJob) Links() *jsonapi.LinksList {
   103  	return &jsonapi.LinksList{Self: "/jobs/" + j.j.WorkerType + "/" + j.j.ID()}
   104  }
   105  
   106  func (j apiJob) MarshalJSON() ([]byte, error) {
   107  	return json.Marshal(j.j)
   108  }
   109  
   110  func (q apiQueue) ID() string      { return q.workerType }
   111  func (q apiQueue) DocType() string { return consts.Jobs }
   112  func (q apiQueue) Fetch(field string) []string {
   113  	switch field {
   114  	case "worker":
   115  		return []string{q.workerType}
   116  	default:
   117  		return nil
   118  	}
   119  }
   120  
   121  // NewAPITrigger creates a jsonapi representation of a trigger.
   122  func NewAPITrigger(infos *job.TriggerInfos, inst *instance.Instance) jsonapi.Object {
   123  	return apiTrigger{infos, inst}
   124  }
   125  
   126  func (t apiTrigger) ID() string                             { return t.t.TID }
   127  func (t apiTrigger) Rev() string                            { return "" }
   128  func (t apiTrigger) DocType() string                        { return consts.Triggers }
   129  func (t apiTrigger) Clone() couchdb.Doc                     { return t }
   130  func (t apiTrigger) SetID(_ string)                         {}
   131  func (t apiTrigger) SetRev(_ string)                        {}
   132  func (t apiTrigger) Relationships() jsonapi.RelationshipMap { return nil }
   133  func (t apiTrigger) Included() []jsonapi.Object             { return nil }
   134  func (t apiTrigger) Links() *jsonapi.LinksList {
   135  	links := &jsonapi.LinksList{Self: "/jobs/triggers/" + t.ID()}
   136  	if t.t.Type == "@webhook" {
   137  		links.Webhook = t.inst.PageURL("/jobs/webhooks/"+t.ID(), nil)
   138  	}
   139  	return links
   140  }
   141  
   142  func (t apiTrigger) MarshalJSON() ([]byte, error) {
   143  	return json.Marshal(t.t)
   144  }
   145  
   146  func (t apiTriggerState) ID() string                             { return t.t.TID }
   147  func (t apiTriggerState) Rev() string                            { return "" }
   148  func (t apiTriggerState) DocType() string                        { return consts.TriggersState }
   149  func (t apiTriggerState) Clone() couchdb.Doc                     { return t }
   150  func (t apiTriggerState) SetID(_ string)                         {}
   151  func (t apiTriggerState) SetRev(_ string)                        {}
   152  func (t apiTriggerState) Relationships() jsonapi.RelationshipMap { return nil }
   153  func (t apiTriggerState) Included() []jsonapi.Object             { return nil }
   154  func (t apiTriggerState) Links() *jsonapi.LinksList {
   155  	return &jsonapi.LinksList{Self: "/jobs/triggers/" + t.ID() + "/state"}
   156  }
   157  
   158  func (t apiTriggerState) MarshalJSON() ([]byte, error) {
   159  	return json.Marshal(t.s)
   160  }
   161  
   162  const bearerAuthScheme = "Bearer "
   163  
   164  // HTTPHandler handle all the `/jobs` routes.
   165  type HTTPHandler struct {
   166  	emailer emailer.Emailer
   167  }
   168  
   169  // NewHTTPHandler instantiates a new [HTTPHandler].
   170  func NewHTTPHandler(emailer emailer.Emailer) *HTTPHandler {
   171  	return &HTTPHandler{emailer}
   172  }
   173  
   174  func (h *HTTPHandler) getQueue(c echo.Context) error {
   175  	instance := middlewares.GetInstance(c)
   176  	workerType := c.Param("worker-type")
   177  
   178  	o := apiQueue{workerType: workerType}
   179  	if err := middlewares.Allow(c, permission.GET, o); err != nil {
   180  		return err
   181  	}
   182  
   183  	js, err := job.GetQueuedJobs(instance, workerType)
   184  	if err != nil {
   185  		return wrapJobsError(err)
   186  	}
   187  
   188  	objs := make([]jsonapi.Object, len(js))
   189  	for i, j := range js {
   190  		objs[i] = apiJob{j}
   191  	}
   192  
   193  	return jsonapi.DataList(c, http.StatusOK, objs, nil)
   194  }
   195  
   196  func (h *HTTPHandler) pushJob(c echo.Context) error {
   197  	instance := middlewares.GetInstance(c)
   198  
   199  	req := apiJobRequest{}
   200  	if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil {
   201  		return wrapJobsError(err)
   202  	}
   203  	var opts *job.JobOptions
   204  	if req.Options != nil {
   205  		opts = &job.JobOptions{
   206  			MaxExecCount: req.Options.MaxExecCount,
   207  			Timeout:      time.Duration(req.Options.Timeout) * time.Second,
   208  		}
   209  	}
   210  
   211  	jr := &job.JobRequest{
   212  		WorkerType:  c.Param("worker-type"),
   213  		Options:     opts,
   214  		Manual:      req.Manual,
   215  		ForwardLogs: req.ForwardLogs,
   216  		Message:     job.Message(req.Arguments),
   217  	}
   218  
   219  	if err := middlewares.Allow(c, permission.POST, jr); err != nil {
   220  		return err
   221  	}
   222  
   223  	permd, err := middlewares.GetPermission(c)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	if permd.Type != permission.TypeCLI {
   228  		if jr.ForwardLogs {
   229  			return echo.NewHTTPError(http.StatusForbidden)
   230  		}
   231  		if err := checkReservedWorker(jr.WorkerType); err != nil {
   232  			return err
   233  		}
   234  	}
   235  
   236  	j, err := job.System().PushJob(instance, jr)
   237  	if err != nil {
   238  		return wrapJobsError(err)
   239  	}
   240  
   241  	return jsonapi.Data(c, http.StatusAccepted, apiJob{j}, nil)
   242  }
   243  
   244  func (h *HTTPHandler) contactSupport(c echo.Context) error {
   245  	inst := middlewares.GetInstance(c)
   246  
   247  	req := apiSupport{}
   248  	if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil {
   249  		return wrapJobsError(err)
   250  	}
   251  
   252  	name, _ := settings.PublicName(inst)
   253  	msg, err := job.NewMessage(mail.Options{
   254  		Mode:         mail.ModeSupport,
   255  		TemplateName: "support_request",
   256  		TemplateValues: map[string]interface{}{
   257  			"Name": name,
   258  			"Body": req.Arguments["body"],
   259  		},
   260  		Subject: req.Arguments["subject"],
   261  		Layout:  mail.CozyCloudLayout,
   262  	})
   263  	if err != nil {
   264  		return err
   265  	}
   266  	jr := &job.JobRequest{WorkerType: "sendmail", Message: msg}
   267  
   268  	if err := middlewares.AllowWholeType(c, permission.POST, consts.Support); err != nil {
   269  		if middlewares.Allow(c, permission.POST, jr) != nil {
   270  			return err
   271  		}
   272  	}
   273  
   274  	if _, err = job.System().PushJob(inst, jr); err != nil {
   275  		return wrapJobsError(err)
   276  	}
   277  	return c.NoContent(http.StatusNoContent)
   278  }
   279  
   280  func (h *HTTPHandler) sendCampaignEmail(c echo.Context) error {
   281  	inst := middlewares.GetInstance(c)
   282  
   283  	if err := middlewares.Allow(c, permission.POST, &job.JobRequest{WorkerType: "sendmail"}); err != nil {
   284  		return err
   285  	}
   286  
   287  	req := apiCampaign{}
   288  	if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil {
   289  		return wrapJobsError(err)
   290  	}
   291  
   292  	err := h.emailer.SendCampaignEmail(inst, &emailer.CampaignEmailCmd{
   293  		Subject: req.Arguments.Subject,
   294  		Parts:   req.Arguments.Parts,
   295  	})
   296  	if err != nil {
   297  		return wrapJobsError(err)
   298  	}
   299  
   300  	return c.NoContent(http.StatusNoContent)
   301  }
   302  
   303  func (h *HTTPHandler) newTrigger(c echo.Context) error {
   304  	instance := middlewares.GetInstance(c)
   305  	sched := job.System()
   306  	req := apiTriggerRequest{}
   307  	if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil {
   308  		return wrapJobsError(err)
   309  	}
   310  
   311  	if req.Debounce != "" {
   312  		if _, err := time.ParseDuration(req.Debounce); err != nil {
   313  			return jsonapi.InvalidAttribute("debounce", err)
   314  		}
   315  	}
   316  
   317  	// Handle metadata
   318  	md := metadata.New()
   319  	if claims := c.Get("claims"); claims != nil {
   320  		cl := claims.(permission.Claims)
   321  		if cl.Subject != "" {
   322  			md.CreatedByApp = cl.Subject
   323  		}
   324  	}
   325  	md.DocTypeVersion = job.DocTypeVersionTrigger
   326  
   327  	msg := req.Message
   328  	if req.Message == nil || len(req.Message) == 0 {
   329  		msg = req.WorkerArguments
   330  	}
   331  	t, err := job.NewTrigger(instance, job.TriggerInfos{
   332  		Type:       req.Type,
   333  		WorkerType: req.WorkerType,
   334  		Domain:     instance.Domain,
   335  		Arguments:  req.Arguments,
   336  		Debounce:   req.Debounce,
   337  		Options:    req.Options,
   338  		Metadata:   md,
   339  	}, msg)
   340  	if err != nil {
   341  		return wrapJobsError(err)
   342  	}
   343  	if err = middlewares.Allow(c, permission.POST, t); err != nil {
   344  		return err
   345  	}
   346  	permd, err := middlewares.GetPermission(c)
   347  	if err != nil {
   348  		return err
   349  	}
   350  	if permd.Type != permission.TypeCLI {
   351  		if err := checkReservedWorker(req.WorkerType); err != nil {
   352  			return err
   353  		}
   354  	}
   355  
   356  	if err = sched.AddTrigger(t); err != nil {
   357  		return wrapJobsError(err)
   358  	}
   359  	return jsonapi.Data(c, http.StatusCreated, apiTrigger{t.Infos(), instance}, nil)
   360  }
   361  
   362  func (h *HTTPHandler) getTrigger(c echo.Context) error {
   363  	instance := middlewares.GetInstance(c)
   364  	sched := job.System()
   365  	t, err := sched.GetTrigger(instance, c.Param("trigger-id"))
   366  	if err != nil {
   367  		return wrapJobsError(err)
   368  	}
   369  	infos := t.Infos()
   370  	if err = middlewares.Allow(c, permission.GET, t); err != nil {
   371  		if !allowKonnectorForItsOwnTrigger(c, infos) {
   372  			return err
   373  		}
   374  	}
   375  	infos.CurrentState, err = job.GetTriggerState(t, t.ID())
   376  	if err != nil {
   377  		return wrapJobsError(err)
   378  	}
   379  	return jsonapi.Data(c, http.StatusOK, apiTrigger{infos, instance}, nil)
   380  }
   381  
   382  func (h *HTTPHandler) getTriggerState(c echo.Context) error {
   383  	instance := middlewares.GetInstance(c)
   384  	sched := job.System()
   385  	t, err := sched.GetTrigger(instance, c.Param("trigger-id"))
   386  	if err != nil {
   387  		return wrapJobsError(err)
   388  	}
   389  	infos := t.Infos()
   390  	if err = middlewares.Allow(c, permission.GET, t); err != nil {
   391  		if !allowKonnectorForItsOwnTrigger(c, infos) {
   392  			return err
   393  		}
   394  	}
   395  
   396  	state, err := job.GetTriggerState(t, t.ID())
   397  	if err != nil {
   398  		return wrapJobsError(err)
   399  	}
   400  	return jsonapi.Data(c, http.StatusOK, apiTriggerState{t: infos, s: state}, nil)
   401  }
   402  
   403  func (h *HTTPHandler) getTriggerJobs(c echo.Context) error {
   404  	instance := middlewares.GetInstance(c)
   405  
   406  	var err error
   407  
   408  	var limit int
   409  	if queryLimit := c.QueryParam("Limit"); queryLimit != "" {
   410  		limit, err = strconv.Atoi(queryLimit)
   411  		if err != nil {
   412  			return echo.NewHTTPError(http.StatusBadRequest, err)
   413  		}
   414  	}
   415  
   416  	sched := job.System()
   417  	t, err := sched.GetTrigger(instance, c.Param("trigger-id"))
   418  	if err != nil {
   419  		return wrapJobsError(err)
   420  	}
   421  	if err = middlewares.Allow(c, permission.GET, t); err != nil {
   422  		return err
   423  	}
   424  
   425  	js, err := job.GetJobs(t, t.ID(), limit)
   426  	if err != nil {
   427  		return wrapJobsError(err)
   428  	}
   429  
   430  	objs := make([]jsonapi.Object, len(js))
   431  	for i, j := range js {
   432  		objs[i] = apiJob{j}
   433  	}
   434  
   435  	return jsonapi.DataList(c, http.StatusOK, objs, nil)
   436  }
   437  
   438  func (h *HTTPHandler) patchTrigger(c echo.Context) error {
   439  	inst := middlewares.GetInstance(c)
   440  	sched := job.System()
   441  	t, err := sched.GetTrigger(inst, c.Param("trigger-id"))
   442  	if err != nil {
   443  		return wrapJobsError(err)
   444  	}
   445  	infos := t.Infos()
   446  	if err := middlewares.Allow(c, permission.PATCH, t); err != nil {
   447  		if !allowKonnectorForItsOwnTrigger(c, infos) {
   448  			return err
   449  		}
   450  	}
   451  
   452  	req := apiTriggerRequest{}
   453  	if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil {
   454  		return wrapJobsError(err)
   455  	}
   456  	if req.Arguments == "" && len(req.Message) == 0 {
   457  		return jsonapi.BadRequest(errors.New("Only arguments and message can be patched"))
   458  	}
   459  
   460  	if len(req.Message) > 0 {
   461  		if err := sched.UpdateMessage(inst, t, req.Message); err != nil {
   462  			return wrapJobsError(err)
   463  		}
   464  	}
   465  
   466  	if req.Arguments != "" {
   467  		if err := sched.UpdateCron(inst, t, req.Arguments); err != nil {
   468  			return wrapJobsError(err)
   469  		}
   470  	}
   471  
   472  	return jsonapi.Data(c, http.StatusOK, apiTrigger{infos, inst}, nil)
   473  }
   474  
   475  func (h *HTTPHandler) launchTrigger(c echo.Context) error {
   476  	instance := middlewares.GetInstance(c)
   477  	t, err := job.System().GetTrigger(instance, c.Param("trigger-id"))
   478  	if err != nil {
   479  		return wrapJobsError(err)
   480  	}
   481  	if err = middlewares.Allow(c, permission.POST, t); err != nil {
   482  		return err
   483  	}
   484  	req := t.Infos().JobRequest()
   485  	req.Manual = true
   486  	j, err := job.System().PushJob(instance, req)
   487  	if err != nil {
   488  		return wrapJobsError(err)
   489  	}
   490  	if j.WorkerType == "client" {
   491  		if err := j.AckConsumed(); err != nil {
   492  			return wrapJobsError(err)
   493  		}
   494  	}
   495  	return jsonapi.Data(c, http.StatusCreated, apiJob{j}, nil)
   496  }
   497  
   498  func (h *HTTPHandler) deleteTrigger(c echo.Context) error {
   499  	instance := middlewares.GetInstance(c)
   500  	sched := job.System()
   501  	t, err := sched.GetTrigger(instance, c.Param("trigger-id"))
   502  	if err != nil {
   503  		return wrapJobsError(err)
   504  	}
   505  	infos := t.Infos()
   506  	if err := middlewares.Allow(c, permission.DELETE, t); err != nil {
   507  		if !allowKonnectorForItsOwnTrigger(c, infos) {
   508  			return err
   509  		}
   510  	}
   511  	if err := sched.DeleteTrigger(instance, c.Param("trigger-id")); err != nil {
   512  		return wrapJobsError(err)
   513  	}
   514  	return c.NoContent(http.StatusNoContent)
   515  }
   516  
   517  func (h *HTTPHandler) fireBIWebhook(c echo.Context) error {
   518  	inst := middlewares.GetInstance(c)
   519  	err := config.GetRateLimiter().CheckRateLimit(inst, limits.WebhookTriggerType)
   520  	if limits.IsLimitReachedOrExceeded(err) {
   521  		return echo.NewHTTPError(http.StatusNotFound, "Not found")
   522  	}
   523  
   524  	header := c.Request().Header.Get(echo.HeaderAuthorization)
   525  	if !strings.HasPrefix(header, bearerAuthScheme) {
   526  		return middlewares.ErrForbidden
   527  	}
   528  	token := strings.TrimPrefix(header, bearerAuthScheme)
   529  
   530  	var payload map[string]interface{}
   531  	if err := c.Bind(&payload); err != nil {
   532  		return jsonapi.BadRequest(err)
   533  	}
   534  
   535  	biEvent, err := bi.ParseEventBI(c.QueryParam("event"))
   536  	if err != nil {
   537  		return jsonapi.BadRequest(err)
   538  	}
   539  
   540  	// The stack will create or delete accounts and triggers on some webhooks,
   541  	// so it is safer to avoid concurrency on this part of the code.
   542  	mutex := config.Lock().ReadWrite(inst, "bi")
   543  	if err := mutex.Lock(); err != nil {
   544  		return err
   545  	}
   546  	defer mutex.Unlock()
   547  
   548  	call := &bi.WebhookCall{
   549  		Instance: inst,
   550  		Token:    token,
   551  		BIurl:    c.QueryParam("bi_url"),
   552  		Event:    biEvent,
   553  		Payload:  payload,
   554  	}
   555  	if err := call.Fire(); err != nil {
   556  		return jsonapi.BadRequest(err)
   557  	}
   558  	return c.NoContent(http.StatusNoContent)
   559  }
   560  
   561  func (h *HTTPHandler) fireWebhook(c echo.Context) error {
   562  	inst := middlewares.GetInstance(c)
   563  	err := config.GetRateLimiter().CheckRateLimit(inst, limits.WebhookTriggerType)
   564  	if limits.IsLimitReachedOrExceeded(err) {
   565  		return echo.NewHTTPError(http.StatusNotFound, "Not found")
   566  	}
   567  
   568  	t, err := job.System().GetTrigger(inst, c.Param("trigger-id"))
   569  	if err != nil {
   570  		return wrapJobsError(err)
   571  	}
   572  	webhook, ok := t.(*job.WebhookTrigger)
   573  	if !ok {
   574  		return jsonapi.InvalidAttribute("Type", errors.New("Not a webhook"))
   575  	}
   576  
   577  	payload, err := io.ReadAll(c.Request().Body)
   578  	if err != nil {
   579  		return wrapJobsError(err)
   580  	}
   581  
   582  	manual := false
   583  	if c.QueryParam("Manual") == "true" {
   584  		manual = true
   585  	}
   586  	webhook.Fire(payload, manual)
   587  	return c.NoContent(http.StatusNoContent)
   588  }
   589  
   590  func (h *HTTPHandler) getAllTriggers(c echo.Context) error {
   591  	instance := middlewares.GetInstance(c)
   592  
   593  	var workerTypes, triggerTypes []string
   594  	if str := c.QueryParam("Worker"); str != "" {
   595  		workerTypes = strings.Split(str, ",")
   596  	}
   597  	if str := c.QueryParam("Type"); str != "" {
   598  		triggerTypes = strings.Split(str, ",")
   599  	}
   600  
   601  	if err := middlewares.AllowWholeType(c, permission.GET, consts.Triggers); err != nil {
   602  		if len(workerTypes) != 1 {
   603  			return err
   604  		}
   605  		o := &job.TriggerInfos{WorkerType: workerTypes[0]}
   606  		if err := middlewares.AllowOnFields(c, permission.GET, o, "worker"); err != nil {
   607  			return err
   608  		}
   609  	}
   610  
   611  	sched := job.System()
   612  	ts, err := sched.GetAllTriggers(instance)
   613  	if err != nil {
   614  		return wrapJobsError(err)
   615  	}
   616  
   617  	objs := make([]jsonapi.Object, 0, len(ts))
   618  	for _, t := range ts {
   619  		tInfos := t.Infos()
   620  		if hasWorker(tInfos, workerTypes) && hasType(tInfos, triggerTypes) {
   621  			tInfos.CurrentState, err = job.GetTriggerState(t, t.ID())
   622  			if err != nil {
   623  				return wrapJobsError(err)
   624  			}
   625  			objs = append(objs, apiTrigger{tInfos, instance})
   626  		}
   627  	}
   628  
   629  	return jsonapi.DataList(c, http.StatusOK, objs, nil)
   630  }
   631  
   632  func hasWorker(infos *job.TriggerInfos, workers []string) bool {
   633  	if len(workers) == 0 {
   634  		return true
   635  	}
   636  	for _, w := range workers {
   637  		if infos.WorkerType == w {
   638  			return true
   639  		}
   640  	}
   641  	return false
   642  }
   643  
   644  func hasType(infos *job.TriggerInfos, triggerTypes []string) bool {
   645  	if len(triggerTypes) == 0 {
   646  		return true
   647  	}
   648  	for _, typ := range triggerTypes {
   649  		if infos.Type == typ {
   650  			return true
   651  		}
   652  	}
   653  	return false
   654  }
   655  
   656  func (h *HTTPHandler) getJob(c echo.Context) error {
   657  	instance := middlewares.GetInstance(c)
   658  	j, err := job.Get(instance, c.Param("job-id"))
   659  	if err != nil {
   660  		return err
   661  	}
   662  	if err := middlewares.Allow(c, permission.GET, j); err != nil {
   663  		return err
   664  	}
   665  	return jsonapi.Data(c, http.StatusOK, apiJob{j}, nil)
   666  }
   667  
   668  func (h *HTTPHandler) patchJob(c echo.Context) error {
   669  	inst := middlewares.GetInstance(c)
   670  	j, err := job.Get(inst, c.Param("job-id"))
   671  	if err != nil {
   672  		return err
   673  	}
   674  	if err := middlewares.Allow(c, permission.PATCH, j); err != nil {
   675  		return err
   676  	}
   677  	if j.WorkerType != "client" {
   678  		return middlewares.ErrForbidden
   679  	}
   680  
   681  	req := job.Job{}
   682  	if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil {
   683  		return wrapJobsError(err)
   684  	}
   685  
   686  	log := inst.Logger().
   687  		WithNamespace("jobs").
   688  		WithField("job_id", j.ID()).
   689  		WithField("worker_id", "client")
   690  
   691  	msg := &exec.KonnectorMessage{}
   692  
   693  	if err := j.Message.Unmarshal(&msg); err == nil {
   694  		log = log.
   695  			WithField("slug", msg.Konnector).
   696  			WithField("account_id", msg.Account).
   697  			WithField("exec_time", time.Since(j.StartedAt))
   698  	}
   699  
   700  	switch req.State {
   701  	case job.Errored:
   702  		err = j.Nack(req.Error)
   703  		log.Infof("Konnector failure: %s", req.Error)
   704  		log.Errorf("error while performing job: %s", req.Error)
   705  	case job.Done:
   706  		err = j.Ack()
   707  		log.Info("Konnector success")
   708  	default:
   709  		err = jsonapi.InvalidAttribute("State", errors.New("State must be done or errored"))
   710  	}
   711  	if err != nil {
   712  		return wrapJobsError(err)
   713  	}
   714  
   715  	return jsonapi.Data(c, http.StatusOK, apiJob{j}, nil)
   716  }
   717  
   718  func (h *HTTPHandler) cleanJobs(c echo.Context) error {
   719  	instance := middlewares.GetInstance(c)
   720  	if err := middlewares.AllowWholeType(c, permission.POST, consts.Jobs); err != nil {
   721  		return err
   722  	}
   723  	var ups []*job.Job
   724  	now := time.Now()
   725  	err := couchdb.ForeachDocs(instance, consts.Jobs, func(_ string, data json.RawMessage) error {
   726  		var j *job.Job
   727  		if err := json.Unmarshal(data, &j); err != nil {
   728  			return err
   729  		}
   730  		if j.State == job.Running || j.State == job.Queued {
   731  			if j.StartedAt.Add(1 * time.Hour).Before(now) {
   732  				ups = append(ups, j)
   733  			}
   734  		}
   735  		return nil
   736  	})
   737  	if err != nil && !couchdb.IsNoDatabaseError(err) {
   738  		return err
   739  	}
   740  	var errf error
   741  	for _, j := range ups {
   742  		j.State = job.Done
   743  		err := couchdb.UpdateDoc(instance, j)
   744  		if err != nil {
   745  			errf = multierror.Append(errf, err)
   746  		}
   747  	}
   748  	if errf != nil {
   749  		return errf
   750  	}
   751  	return c.JSON(200, map[string]int{"deleted": len(ups)})
   752  }
   753  
   754  func (h *HTTPHandler) purgeJobs(c echo.Context) error {
   755  	instance := middlewares.GetInstance(c)
   756  	if err := middlewares.AllowWholeType(c, permission.DELETE, consts.Jobs); err != nil {
   757  		return err
   758  	}
   759  
   760  	workersParam := c.QueryParam("workers")
   761  	durationParam := c.QueryParam("duration")
   762  
   763  	conf := config.GetConfig().Jobs
   764  	dur, err := bigduration.ParseDuration(conf.DefaultDurationToKeep)
   765  	if err != nil {
   766  		return err
   767  	}
   768  
   769  	if durationParam != "" {
   770  		dur, err = bigduration.ParseDuration(durationParam)
   771  		if err != nil {
   772  			return err
   773  		}
   774  	}
   775  	workers := job.GetWorkersNamesList()
   776  	if workersParam != "" {
   777  		workers = strings.Split(workersParam, ",")
   778  	}
   779  
   780  	allJobs, err := job.GetAllJobs(instance)
   781  	if err != nil {
   782  		return err
   783  	}
   784  
   785  	// Step 1: We want to get all the jobs prior to the date parameter.
   786  	// Jobs returned are the ones we want to remove
   787  	d := time.Now().Add(-dur)
   788  	jobsBeforeDate := job.FilterJobsBeforeDate(allJobs, d)
   789  
   790  	// Step 2: We also want to keep a minimum number of jobs for each state.
   791  	// Jobs returned will be kept.
   792  	lastsJobs := map[string]struct{}{}
   793  	for _, w := range workers {
   794  		jobs, err := job.GetLastsJobs(allJobs, w)
   795  		if err != nil {
   796  			return err
   797  		}
   798  		for _, j := range jobs {
   799  			lastsJobs[j.ID()] = struct{}{}
   800  		}
   801  	}
   802  
   803  	// Step 3: cleaning.
   804  	// - Removing jobs from the ids if they exists.
   805  	// - Skipping worker types
   806  	var finalJobs []*job.Job
   807  
   808  	for _, j := range jobsBeforeDate {
   809  		validWorker := false
   810  
   811  		for _, wt := range workers {
   812  			if j.WorkerType == wt {
   813  				validWorker = true
   814  				break
   815  			}
   816  		}
   817  		// Check the job is not existing in the lasts jobs
   818  		if validWorker {
   819  			_, ok := lastsJobs[j.ID()]
   820  
   821  			if !ok {
   822  				finalJobs = append(finalJobs, j)
   823  			}
   824  		}
   825  	}
   826  
   827  	// Bulk-deleting the jobs
   828  	jobsToDelete := make([]couchdb.Doc, len(finalJobs))
   829  	for i, j := range finalJobs {
   830  		jobsToDelete[i] = j
   831  	}
   832  
   833  	chunkSize := 1000
   834  
   835  	for i := 0; i < len(jobsToDelete); i += chunkSize {
   836  		end := i + chunkSize
   837  
   838  		if end > len(jobsToDelete) {
   839  			end = len(jobsToDelete)
   840  		}
   841  
   842  		err = couchdb.BulkDeleteDocs(instance, consts.Jobs, jobsToDelete[i:end])
   843  		if err != nil {
   844  			return err
   845  		}
   846  	}
   847  
   848  	return c.JSON(http.StatusOK, map[string]int{"deleted": len(jobsToDelete)})
   849  }
   850  
   851  // Register all the `/jobs` routes to the given router
   852  func (h *HTTPHandler) Register(router *echo.Group) {
   853  	router.GET("/queue/:worker-type", h.getQueue)
   854  	router.POST("/queue/:worker-type", h.pushJob)
   855  	router.POST("/support", h.contactSupport)
   856  	router.POST("/campaign-emails", h.sendCampaignEmail)
   857  
   858  	router.POST("/triggers", h.newTrigger)
   859  	router.GET("/triggers", h.getAllTriggers)
   860  	router.GET("/triggers/:trigger-id", h.getTrigger)
   861  	router.GET("/triggers/:trigger-id/state", h.getTriggerState)
   862  	router.GET("/triggers/:trigger-id/jobs", h.getTriggerJobs)
   863  	router.PATCH("/triggers/:trigger-id", h.patchTrigger)
   864  	router.POST("/triggers/:trigger-id/launch", h.launchTrigger)
   865  	router.DELETE("/triggers/:trigger-id", h.deleteTrigger)
   866  
   867  	router.POST("/webhooks/bi", h.fireBIWebhook)
   868  	router.POST("/webhooks/:trigger-id", h.fireWebhook)
   869  
   870  	router.POST("/clean", h.cleanJobs)
   871  	router.DELETE("/purge", h.purgeJobs)
   872  	router.GET("/:job-id", h.getJob)
   873  	router.PATCH("/:job-id", h.patchJob)
   874  }
   875  
   876  func wrapJobsError(err error) error {
   877  	switch err {
   878  	case job.ErrNotFoundTrigger,
   879  		job.ErrNotFoundJob,
   880  		job.ErrUnknownWorker:
   881  		return jsonapi.NotFound(err)
   882  	case job.ErrUnknownTrigger,
   883  		job.ErrNotCronTrigger:
   884  		return jsonapi.InvalidAttribute("Type", err)
   885  	case emailer.ErrMissingSubject,
   886  		emailer.ErrMissingContent,
   887  		limits.ErrRateLimitReached,
   888  		limits.ErrRateLimitExceeded:
   889  		return jsonapi.BadRequest(err)
   890  	}
   891  	return err
   892  }
   893  
   894  // checkReservedWorker returns an error if the worker should only by used by
   895  // the stack, and the clients must not push jobs for it.
   896  func checkReservedWorker(worker string) error {
   897  	reserved, err := job.System().WorkerIsReserved(worker)
   898  	if err != nil {
   899  		if errors.Is(err, job.ErrUnknownWorker) {
   900  			return echo.NewHTTPError(http.StatusNotFound)
   901  		}
   902  		return err
   903  	}
   904  	if reserved {
   905  		return echo.NewHTTPError(http.StatusForbidden)
   906  	}
   907  	return nil
   908  }
   909  
   910  func allowKonnectorForItsOwnTrigger(c echo.Context, infos *job.TriggerInfos) bool {
   911  	if infos.WorkerType != "konnector" {
   912  		return false
   913  	}
   914  	var msg map[string]interface{}
   915  	if errb := json.Unmarshal(infos.Message, &msg); errb != nil {
   916  		return false
   917  	}
   918  	slug, _ := msg["konnector"].(string)
   919  	if slug == "" {
   920  		return false
   921  	}
   922  	err := middlewares.AllowForKonnector(c, slug)
   923  	return err == nil
   924  }