eventrule.go 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743
  1. // Copyright (C) 2019 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package dataprovider
  15. import (
  16. "context"
  17. "crypto/tls"
  18. "encoding/json"
  19. "errors"
  20. "fmt"
  21. "net/http"
  22. "path"
  23. "path/filepath"
  24. "strings"
  25. "time"
  26. "github.com/robfig/cron/v3"
  27. "github.com/drakkan/sftpgo/v2/internal/kms"
  28. "github.com/drakkan/sftpgo/v2/internal/logger"
  29. "github.com/drakkan/sftpgo/v2/internal/util"
  30. )
  31. // Supported event actions
  32. const (
  33. ActionTypeHTTP = iota + 1
  34. ActionTypeCommand
  35. ActionTypeEmail
  36. ActionTypeBackup
  37. ActionTypeUserQuotaReset
  38. ActionTypeFolderQuotaReset
  39. ActionTypeTransferQuotaReset
  40. ActionTypeDataRetentionCheck
  41. ActionTypeFilesystem
  42. ActionTypeMetadataCheck
  43. ActionTypePasswordExpirationCheck
  44. ActionTypeUserExpirationCheck
  45. ActionTypeIDPAccountCheck
  46. )
  47. var (
  48. supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
  49. ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
  50. ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypePasswordExpirationCheck,
  51. ActionTypeUserExpirationCheck, ActionTypeIDPAccountCheck}
  52. )
  53. func isActionTypeValid(action int) bool {
  54. return util.Contains(supportedEventActions, action)
  55. }
  56. func getActionTypeAsString(action int) string {
  57. switch action {
  58. case ActionTypeHTTP:
  59. return "HTTP"
  60. case ActionTypeEmail:
  61. return "Email"
  62. case ActionTypeBackup:
  63. return "Backup"
  64. case ActionTypeUserQuotaReset:
  65. return "User quota reset"
  66. case ActionTypeFolderQuotaReset:
  67. return "Folder quota reset"
  68. case ActionTypeTransferQuotaReset:
  69. return "Transfer quota reset"
  70. case ActionTypeDataRetentionCheck:
  71. return "Data retention check"
  72. case ActionTypeMetadataCheck:
  73. return "Metadata check"
  74. case ActionTypeFilesystem:
  75. return "Filesystem"
  76. case ActionTypePasswordExpirationCheck:
  77. return "Password expiration check"
  78. case ActionTypeUserExpirationCheck:
  79. return "User expiration check"
  80. case ActionTypeIDPAccountCheck:
  81. return "Identity Provider account check"
  82. default:
  83. return "Command"
  84. }
  85. }
  86. // Supported event triggers
  87. const (
  88. // Filesystem events such as upload, download, mkdir ...
  89. EventTriggerFsEvent = iota + 1
  90. // Provider events such as add, update, delete
  91. EventTriggerProviderEvent
  92. EventTriggerSchedule
  93. EventTriggerIPBlocked
  94. EventTriggerCertificate
  95. EventTriggerOnDemand
  96. EventTriggerIDPLogin
  97. )
  98. var (
  99. supportedEventTriggers = []int{EventTriggerFsEvent, EventTriggerProviderEvent, EventTriggerSchedule,
  100. EventTriggerIPBlocked, EventTriggerCertificate, EventTriggerIDPLogin, EventTriggerOnDemand}
  101. )
  102. func isEventTriggerValid(trigger int) bool {
  103. return util.Contains(supportedEventTriggers, trigger)
  104. }
  105. func getTriggerTypeAsString(trigger int) string {
  106. switch trigger {
  107. case EventTriggerFsEvent:
  108. return "Filesystem event"
  109. case EventTriggerProviderEvent:
  110. return "Provider event"
  111. case EventTriggerIPBlocked:
  112. return "IP blocked"
  113. case EventTriggerCertificate:
  114. return "Certificate renewal"
  115. case EventTriggerOnDemand:
  116. return "On demand"
  117. case EventTriggerIDPLogin:
  118. return "Identity Provider login"
  119. default:
  120. return "Schedule"
  121. }
  122. }
  123. // Supported IDP login events
  124. const (
  125. IDPLoginAny = iota
  126. IDPLoginUser
  127. IDPLoginAdmin
  128. )
  129. var (
  130. supportedIDPLoginEvents = []int{IDPLoginAny, IDPLoginUser, IDPLoginAdmin}
  131. )
  132. // Supported filesystem actions
  133. const (
  134. FilesystemActionRename = iota + 1
  135. FilesystemActionDelete
  136. FilesystemActionMkdirs
  137. FilesystemActionExist
  138. FilesystemActionCompress
  139. FilesystemActionCopy
  140. )
  141. const (
  142. // RetentionReportPlaceHolder defines the placeholder for data retention reports
  143. RetentionReportPlaceHolder = "{{RetentionReports}}"
  144. )
  145. var (
  146. supportedFsActions = []int{FilesystemActionRename, FilesystemActionDelete, FilesystemActionMkdirs,
  147. FilesystemActionCopy, FilesystemActionCompress, FilesystemActionExist}
  148. )
  149. func isFilesystemActionValid(value int) bool {
  150. return util.Contains(supportedFsActions, value)
  151. }
  152. func getFsActionTypeAsString(value int) string {
  153. switch value {
  154. case FilesystemActionRename:
  155. return "Rename"
  156. case FilesystemActionDelete:
  157. return "Delete"
  158. case FilesystemActionExist:
  159. return "Paths exist"
  160. case FilesystemActionCompress:
  161. return "Compress"
  162. case FilesystemActionCopy:
  163. return "Copy"
  164. default:
  165. return "Create directories"
  166. }
  167. }
  168. // TODO: replace the copied strings with shared constants
  169. var (
  170. // SupportedFsEvents defines the supported filesystem events
  171. SupportedFsEvents = []string{"upload", "pre-upload", "first-upload", "download", "pre-download",
  172. "first-download", "delete", "pre-delete", "rename", "mkdir", "rmdir", "copy", "ssh_cmd"}
  173. // SupportedProviderEvents defines the supported provider events
  174. SupportedProviderEvents = []string{operationAdd, operationUpdate, operationDelete}
  175. // SupportedRuleConditionProtocols defines the supported protcols for rule conditions
  176. SupportedRuleConditionProtocols = []string{"SFTP", "SCP", "SSH", "FTP", "DAV", "HTTP", "HTTPShare",
  177. "OIDC"}
  178. // SupporteRuleConditionProviderObjects defines the supported provider objects for rule conditions
  179. SupporteRuleConditionProviderObjects = []string{actionObjectUser, actionObjectFolder, actionObjectGroup,
  180. actionObjectAdmin, actionObjectAPIKey, actionObjectShare, actionObjectEventRule, actionObjectEventAction}
  181. // SupportedHTTPActionMethods defines the supported methods for HTTP actions
  182. SupportedHTTPActionMethods = []string{http.MethodPost, http.MethodGet, http.MethodPut, http.MethodDelete}
  183. allowedSyncFsEvents = []string{"upload", "pre-upload", "pre-download", "pre-delete"}
  184. mandatorySyncFsEvents = []string{"pre-upload", "pre-download", "pre-delete"}
  185. )
  186. // enum mappings
  187. var (
  188. EventActionTypes []EnumMapping
  189. EventTriggerTypes []EnumMapping
  190. FsActionTypes []EnumMapping
  191. )
  192. func init() {
  193. for _, t := range supportedEventActions {
  194. EventActionTypes = append(EventActionTypes, EnumMapping{
  195. Value: t,
  196. Name: getActionTypeAsString(t),
  197. })
  198. }
  199. for _, t := range supportedEventTriggers {
  200. EventTriggerTypes = append(EventTriggerTypes, EnumMapping{
  201. Value: t,
  202. Name: getTriggerTypeAsString(t),
  203. })
  204. }
  205. for _, t := range supportedFsActions {
  206. FsActionTypes = append(FsActionTypes, EnumMapping{
  207. Value: t,
  208. Name: getFsActionTypeAsString(t),
  209. })
  210. }
  211. }
  212. // EnumMapping defines a mapping between enum values and names
  213. type EnumMapping struct {
  214. Name string
  215. Value int
  216. }
  217. // KeyValue defines a key/value pair
  218. type KeyValue struct {
  219. Key string `json:"key"`
  220. Value string `json:"value"`
  221. }
  222. func (k *KeyValue) isNotValid() bool {
  223. return k.Key == "" || k.Value == ""
  224. }
  225. // HTTPPart defines a part for HTTP multipart requests
  226. type HTTPPart struct {
  227. Name string `json:"name,omitempty"`
  228. Filepath string `json:"filepath,omitempty"`
  229. Headers []KeyValue `json:"headers,omitempty"`
  230. Body string `json:"body,omitempty"`
  231. Order int `json:"-"`
  232. }
  233. func (p *HTTPPart) validate() error {
  234. if p.Name == "" {
  235. return util.NewValidationError("HTTP part name is required")
  236. }
  237. for _, kv := range p.Headers {
  238. if kv.isNotValid() {
  239. return util.NewValidationError("invalid HTTP part headers")
  240. }
  241. }
  242. if p.Filepath == "" {
  243. if p.Body == "" {
  244. return util.NewValidationError("HTTP part body is required if no file path is provided")
  245. }
  246. } else {
  247. p.Body = ""
  248. if p.Filepath != RetentionReportPlaceHolder {
  249. p.Filepath = util.CleanPath(p.Filepath)
  250. }
  251. }
  252. return nil
  253. }
  254. // EventActionHTTPConfig defines the configuration for an HTTP event target
  255. type EventActionHTTPConfig struct {
  256. Endpoint string `json:"endpoint,omitempty"`
  257. Username string `json:"username,omitempty"`
  258. Password *kms.Secret `json:"password,omitempty"`
  259. Headers []KeyValue `json:"headers,omitempty"`
  260. Timeout int `json:"timeout,omitempty"`
  261. SkipTLSVerify bool `json:"skip_tls_verify,omitempty"`
  262. Method string `json:"method,omitempty"`
  263. QueryParameters []KeyValue `json:"query_parameters,omitempty"`
  264. Body string `json:"body,omitempty"`
  265. Parts []HTTPPart `json:"parts,omitempty"`
  266. }
  267. // HasJSONBody returns true if the content type header indicates a JSON body
  268. func (c *EventActionHTTPConfig) HasJSONBody() bool {
  269. for _, h := range c.Headers {
  270. if http.CanonicalHeaderKey(h.Key) == "Content-Type" {
  271. return strings.Contains(strings.ToLower(h.Value), "application/json")
  272. }
  273. }
  274. return false
  275. }
  276. func (c *EventActionHTTPConfig) isTimeoutNotValid() bool {
  277. if c.HasMultipartFiles() {
  278. return false
  279. }
  280. return c.Timeout < 1 || c.Timeout > 180
  281. }
  282. func (c *EventActionHTTPConfig) validateMultiparts() error {
  283. filePaths := make(map[string]bool)
  284. for idx := range c.Parts {
  285. if err := c.Parts[idx].validate(); err != nil {
  286. return err
  287. }
  288. if filePath := c.Parts[idx].Filepath; filePath != "" {
  289. if filePaths[filePath] {
  290. return fmt.Errorf("filepath %q is duplicated", filePath)
  291. }
  292. filePaths[filePath] = true
  293. }
  294. }
  295. if len(c.Parts) > 0 {
  296. if c.Body != "" {
  297. return util.NewValidationError("multipart requests require no body. The request body is build from the specified parts")
  298. }
  299. for _, k := range c.Headers {
  300. if strings.ToLower(k.Key) == "content-type" {
  301. return util.NewValidationError("content type is automatically set for multipart requests")
  302. }
  303. }
  304. }
  305. return nil
  306. }
  307. func (c *EventActionHTTPConfig) validate(additionalData string) error {
  308. if c.Endpoint == "" {
  309. return util.NewValidationError("HTTP endpoint is required")
  310. }
  311. if !util.IsStringPrefixInSlice(c.Endpoint, []string{"http://", "https://"}) {
  312. return util.NewValidationError("invalid HTTP endpoint schema: http and https are supported")
  313. }
  314. if c.isTimeoutNotValid() {
  315. return util.NewValidationError(fmt.Sprintf("invalid HTTP timeout %d", c.Timeout))
  316. }
  317. for _, kv := range c.Headers {
  318. if kv.isNotValid() {
  319. return util.NewValidationError("invalid HTTP headers")
  320. }
  321. }
  322. if err := c.validateMultiparts(); err != nil {
  323. return err
  324. }
  325. if c.Password.IsRedacted() {
  326. return util.NewValidationError("cannot save HTTP configuration with a redacted secret")
  327. }
  328. if c.Password.IsPlain() {
  329. c.Password.SetAdditionalData(additionalData)
  330. err := c.Password.Encrypt()
  331. if err != nil {
  332. return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP password: %v", err))
  333. }
  334. }
  335. if !util.Contains(SupportedHTTPActionMethods, c.Method) {
  336. return util.NewValidationError(fmt.Sprintf("unsupported HTTP method: %s", c.Method))
  337. }
  338. for _, kv := range c.QueryParameters {
  339. if kv.isNotValid() {
  340. return util.NewValidationError("invalid HTTP query parameters")
  341. }
  342. }
  343. return nil
  344. }
  345. // GetContext returns the context and the cancel func to use for the HTTP request
  346. func (c *EventActionHTTPConfig) GetContext() (context.Context, context.CancelFunc) {
  347. if c.HasMultipartFiles() {
  348. return context.WithCancel(context.Background())
  349. }
  350. return context.WithTimeout(context.Background(), time.Duration(c.Timeout)*time.Second)
  351. }
  352. // HasObjectData returns true if the {{ObjectData}} placeholder is defined
  353. func (c *EventActionHTTPConfig) HasObjectData() bool {
  354. if strings.Contains(c.Body, "{{ObjectData}}") {
  355. return true
  356. }
  357. for _, part := range c.Parts {
  358. if strings.Contains(part.Body, "{{ObjectData}}") {
  359. return true
  360. }
  361. }
  362. return false
  363. }
  364. // HasMultipartFiles returns true if at least a file must be uploaded via a multipart request
  365. func (c *EventActionHTTPConfig) HasMultipartFiles() bool {
  366. for _, part := range c.Parts {
  367. if part.Filepath != "" && part.Filepath != RetentionReportPlaceHolder {
  368. return true
  369. }
  370. }
  371. return false
  372. }
  373. // TryDecryptPassword decrypts the password if encryptet
  374. func (c *EventActionHTTPConfig) TryDecryptPassword() error {
  375. if c.Password != nil && !c.Password.IsEmpty() {
  376. if err := c.Password.TryDecrypt(); err != nil {
  377. return fmt.Errorf("unable to decrypt HTTP password: %w", err)
  378. }
  379. }
  380. return nil
  381. }
  382. // GetHTTPClient returns an HTTP client based on the config
  383. func (c *EventActionHTTPConfig) GetHTTPClient() *http.Client {
  384. client := &http.Client{}
  385. if c.SkipTLSVerify {
  386. transport := http.DefaultTransport.(*http.Transport).Clone()
  387. if transport.TLSClientConfig != nil {
  388. transport.TLSClientConfig.InsecureSkipVerify = true
  389. } else {
  390. transport.TLSClientConfig = &tls.Config{
  391. InsecureSkipVerify: true,
  392. }
  393. }
  394. client.Transport = transport
  395. }
  396. return client
  397. }
  398. // EventActionCommandConfig defines the configuration for a command event target
  399. type EventActionCommandConfig struct {
  400. Cmd string `json:"cmd,omitempty"`
  401. Args []string `json:"args,omitempty"`
  402. Timeout int `json:"timeout,omitempty"`
  403. EnvVars []KeyValue `json:"env_vars,omitempty"`
  404. }
  405. func (c *EventActionCommandConfig) validate() error {
  406. if c.Cmd == "" {
  407. return util.NewValidationError("command is required")
  408. }
  409. if !filepath.IsAbs(c.Cmd) {
  410. return util.NewValidationError("invalid command, it must be an absolute path")
  411. }
  412. if c.Timeout < 1 || c.Timeout > 120 {
  413. return util.NewValidationError(fmt.Sprintf("invalid command action timeout %d", c.Timeout))
  414. }
  415. for _, kv := range c.EnvVars {
  416. if kv.isNotValid() {
  417. return util.NewValidationError("invalid command env vars")
  418. }
  419. }
  420. c.Args = util.RemoveDuplicates(c.Args, true)
  421. for _, arg := range c.Args {
  422. if arg == "" {
  423. return util.NewValidationError("invalid command args")
  424. }
  425. }
  426. return nil
  427. }
  428. // GetArgumentsAsString returns the list of command arguments as comma separated string
  429. func (c EventActionCommandConfig) GetArgumentsAsString() string {
  430. return strings.Join(c.Args, ",")
  431. }
  432. // EventActionEmailConfig defines the configuration options for SMTP event actions
  433. type EventActionEmailConfig struct {
  434. Recipients []string `json:"recipients,omitempty"`
  435. Bcc []string `json:"bcc,omitempty"`
  436. Subject string `json:"subject,omitempty"`
  437. Body string `json:"body,omitempty"`
  438. Attachments []string `json:"attachments,omitempty"`
  439. ContentType int `json:"content_type,omitempty"`
  440. }
  441. // GetRecipientsAsString returns the list of recipients as comma separated string
  442. func (c EventActionEmailConfig) GetRecipientsAsString() string {
  443. return strings.Join(c.Recipients, ",")
  444. }
  445. // GetBccAsString returns the list of bcc as comma separated string
  446. func (c EventActionEmailConfig) GetBccAsString() string {
  447. return strings.Join(c.Bcc, ",")
  448. }
  449. // GetAttachmentsAsString returns the list of attachments as comma separated string
  450. func (c EventActionEmailConfig) GetAttachmentsAsString() string {
  451. return strings.Join(c.Attachments, ",")
  452. }
  453. func (c *EventActionEmailConfig) hasFilesAttachments() bool {
  454. for _, a := range c.Attachments {
  455. if a != RetentionReportPlaceHolder {
  456. return true
  457. }
  458. }
  459. return false
  460. }
  461. func (c *EventActionEmailConfig) validate() error {
  462. if len(c.Recipients) == 0 {
  463. return util.NewValidationError("at least one email recipient is required")
  464. }
  465. c.Recipients = util.RemoveDuplicates(c.Recipients, false)
  466. for _, r := range c.Recipients {
  467. if r == "" {
  468. return util.NewValidationError("invalid email recipients")
  469. }
  470. }
  471. c.Bcc = util.RemoveDuplicates(c.Bcc, false)
  472. for _, r := range c.Bcc {
  473. if r == "" {
  474. return util.NewValidationError("invalid email bcc")
  475. }
  476. }
  477. if c.Subject == "" {
  478. return util.NewValidationError("email subject is required")
  479. }
  480. if c.Body == "" {
  481. return util.NewValidationError("email body is required")
  482. }
  483. if c.ContentType < 0 || c.ContentType > 1 {
  484. return util.NewValidationError("invalid email content type")
  485. }
  486. for idx, val := range c.Attachments {
  487. val = strings.TrimSpace(val)
  488. if val == "" {
  489. return util.NewValidationError("invalid path to attach")
  490. }
  491. if val == RetentionReportPlaceHolder {
  492. c.Attachments[idx] = val
  493. } else {
  494. c.Attachments[idx] = util.CleanPath(val)
  495. }
  496. }
  497. c.Attachments = util.RemoveDuplicates(c.Attachments, false)
  498. return nil
  499. }
  500. // FolderRetention defines a folder retention configuration
  501. type FolderRetention struct {
  502. // Path is the virtual directory path, if no other specific retention is defined,
  503. // the retention applies for sub directories too. For example if retention is defined
  504. // for the paths "/" and "/sub" then the retention for "/" is applied for any file outside
  505. // the "/sub" directory
  506. Path string `json:"path"`
  507. // Retention time in hours. 0 means exclude this path
  508. Retention int `json:"retention"`
  509. // DeleteEmptyDirs defines if empty directories will be deleted.
  510. // The user need the delete permission
  511. DeleteEmptyDirs bool `json:"delete_empty_dirs,omitempty"`
  512. // IgnoreUserPermissions defines whether to delete files even if the user does not have the delete permission.
  513. // The default is "false" which means that files will be skipped if the user does not have the permission
  514. // to delete them. This applies to sub directories too.
  515. IgnoreUserPermissions bool `json:"ignore_user_permissions,omitempty"`
  516. }
  517. // Validate returns an error if the configuration is not valid
  518. func (f *FolderRetention) Validate() error {
  519. f.Path = util.CleanPath(f.Path)
  520. if f.Retention < 0 {
  521. return util.NewValidationError(fmt.Sprintf("invalid folder retention %v, it must be greater or equal to zero",
  522. f.Retention))
  523. }
  524. return nil
  525. }
  526. // EventActionDataRetentionConfig defines the configuration for a data retention check
  527. type EventActionDataRetentionConfig struct {
  528. Folders []FolderRetention `json:"folders,omitempty"`
  529. }
  530. func (c *EventActionDataRetentionConfig) validate() error {
  531. folderPaths := make(map[string]bool)
  532. nothingToDo := true
  533. for idx := range c.Folders {
  534. f := &c.Folders[idx]
  535. if err := f.Validate(); err != nil {
  536. return err
  537. }
  538. if f.Retention > 0 {
  539. nothingToDo = false
  540. }
  541. if _, ok := folderPaths[f.Path]; ok {
  542. return util.NewValidationError(fmt.Sprintf("duplicated folder path %q", f.Path))
  543. }
  544. folderPaths[f.Path] = true
  545. }
  546. if nothingToDo {
  547. return util.NewValidationError("nothing to delete!")
  548. }
  549. return nil
  550. }
  551. // EventActionFsCompress defines the configuration for the compress filesystem action
  552. type EventActionFsCompress struct {
  553. // Archive path
  554. Name string `json:"name,omitempty"`
  555. // Paths to compress
  556. Paths []string `json:"paths,omitempty"`
  557. }
  558. func (c *EventActionFsCompress) validate() error {
  559. if c.Name == "" {
  560. return util.NewValidationError("archive name is mandatory")
  561. }
  562. c.Name = util.CleanPath(strings.TrimSpace(c.Name))
  563. if c.Name == "/" {
  564. return util.NewValidationError("invalid archive name")
  565. }
  566. if len(c.Paths) == 0 {
  567. return util.NewValidationError("no path to compress specified")
  568. }
  569. for idx, val := range c.Paths {
  570. val = strings.TrimSpace(val)
  571. if val == "" {
  572. return util.NewValidationError("invalid path to compress")
  573. }
  574. c.Paths[idx] = util.CleanPath(val)
  575. }
  576. c.Paths = util.RemoveDuplicates(c.Paths, false)
  577. return nil
  578. }
  579. // EventActionFilesystemConfig defines the configuration for filesystem actions
  580. type EventActionFilesystemConfig struct {
  581. // Filesystem actions, see the above enum
  582. Type int `json:"type,omitempty"`
  583. // files/dirs to rename, key is the source and target the value
  584. Renames []KeyValue `json:"renames,omitempty"`
  585. // directories to create
  586. MkDirs []string `json:"mkdirs,omitempty"`
  587. // files/dirs to delete
  588. Deletes []string `json:"deletes,omitempty"`
  589. // file/dirs to check for existence
  590. Exist []string `json:"exist,omitempty"`
  591. // files/dirs to copy, key is the source and target the value
  592. Copy []KeyValue `json:"copy,omitempty"`
  593. // paths to compress and archive name
  594. Compress EventActionFsCompress `json:"compress"`
  595. }
  596. // GetDeletesAsString returns the list of items to delete as comma separated string.
  597. // Using a pointer receiver will not work in web templates
  598. func (c EventActionFilesystemConfig) GetDeletesAsString() string {
  599. return strings.Join(c.Deletes, ",")
  600. }
  601. // GetMkDirsAsString returns the list of directories to create as comma separated string.
  602. // Using a pointer receiver will not work in web templates
  603. func (c EventActionFilesystemConfig) GetMkDirsAsString() string {
  604. return strings.Join(c.MkDirs, ",")
  605. }
  606. // GetExistAsString returns the list of items to check for existence as comma separated string.
  607. // Using a pointer receiver will not work in web templates
  608. func (c EventActionFilesystemConfig) GetExistAsString() string {
  609. return strings.Join(c.Exist, ",")
  610. }
  611. // GetCompressPathsAsString returns the list of items to compress as comma separated string.
  612. // Using a pointer receiver will not work in web templates
  613. func (c EventActionFilesystemConfig) GetCompressPathsAsString() string {
  614. return strings.Join(c.Compress.Paths, ",")
  615. }
  616. func (c *EventActionFilesystemConfig) validateRenames() error {
  617. if len(c.Renames) == 0 {
  618. return util.NewValidationError("no path to rename specified")
  619. }
  620. for idx, kv := range c.Renames {
  621. key := strings.TrimSpace(kv.Key)
  622. value := strings.TrimSpace(kv.Value)
  623. if key == "" || value == "" {
  624. return util.NewValidationError("invalid paths to rename")
  625. }
  626. key = util.CleanPath(key)
  627. value = util.CleanPath(value)
  628. if key == value {
  629. return util.NewValidationError("rename source and target cannot be equal")
  630. }
  631. if key == "/" || value == "/" {
  632. return util.NewValidationError("renaming the root directory is not allowed")
  633. }
  634. c.Renames[idx] = KeyValue{
  635. Key: key,
  636. Value: value,
  637. }
  638. }
  639. return nil
  640. }
  641. func (c *EventActionFilesystemConfig) validateCopy() error {
  642. if len(c.Copy) == 0 {
  643. return util.NewValidationError("no path to copy specified")
  644. }
  645. for idx, kv := range c.Copy {
  646. key := strings.TrimSpace(kv.Key)
  647. value := strings.TrimSpace(kv.Value)
  648. if key == "" || value == "" {
  649. return util.NewValidationError("invalid paths to copy")
  650. }
  651. key = util.CleanPath(key)
  652. value = util.CleanPath(value)
  653. if key == value {
  654. return util.NewValidationError("copy source and target cannot be equal")
  655. }
  656. if key == "/" || value == "/" {
  657. return util.NewValidationError("copying the root directory is not allowed")
  658. }
  659. if strings.HasSuffix(c.Copy[idx].Key, "/") {
  660. key += "/"
  661. }
  662. if strings.HasSuffix(c.Copy[idx].Value, "/") {
  663. value += "/"
  664. }
  665. c.Copy[idx] = KeyValue{
  666. Key: key,
  667. Value: value,
  668. }
  669. }
  670. return nil
  671. }
  672. func (c *EventActionFilesystemConfig) validateDeletes() error {
  673. if len(c.Deletes) == 0 {
  674. return util.NewValidationError("no path to delete specified")
  675. }
  676. for idx, val := range c.Deletes {
  677. val = strings.TrimSpace(val)
  678. if val == "" {
  679. return util.NewValidationError("invalid path to delete")
  680. }
  681. c.Deletes[idx] = util.CleanPath(val)
  682. }
  683. c.Deletes = util.RemoveDuplicates(c.Deletes, false)
  684. return nil
  685. }
  686. func (c *EventActionFilesystemConfig) validateMkdirs() error {
  687. if len(c.MkDirs) == 0 {
  688. return util.NewValidationError("no directory to create specified")
  689. }
  690. for idx, val := range c.MkDirs {
  691. val = strings.TrimSpace(val)
  692. if val == "" {
  693. return util.NewValidationError("invalid directory to create")
  694. }
  695. c.MkDirs[idx] = util.CleanPath(val)
  696. }
  697. c.MkDirs = util.RemoveDuplicates(c.MkDirs, false)
  698. return nil
  699. }
  700. func (c *EventActionFilesystemConfig) validateExist() error {
  701. if len(c.Exist) == 0 {
  702. return util.NewValidationError("no path to check for existence specified")
  703. }
  704. for idx, val := range c.Exist {
  705. val = strings.TrimSpace(val)
  706. if val == "" {
  707. return util.NewValidationError("invalid path to check for existence")
  708. }
  709. c.Exist[idx] = util.CleanPath(val)
  710. }
  711. c.Exist = util.RemoveDuplicates(c.Exist, false)
  712. return nil
  713. }
  714. func (c *EventActionFilesystemConfig) validate() error {
  715. if !isFilesystemActionValid(c.Type) {
  716. return util.NewValidationError(fmt.Sprintf("invalid filesystem action type: %d", c.Type))
  717. }
  718. switch c.Type {
  719. case FilesystemActionRename:
  720. c.MkDirs = nil
  721. c.Deletes = nil
  722. c.Exist = nil
  723. c.Copy = nil
  724. c.Compress = EventActionFsCompress{}
  725. if err := c.validateRenames(); err != nil {
  726. return err
  727. }
  728. case FilesystemActionDelete:
  729. c.Renames = nil
  730. c.MkDirs = nil
  731. c.Exist = nil
  732. c.Copy = nil
  733. c.Compress = EventActionFsCompress{}
  734. if err := c.validateDeletes(); err != nil {
  735. return err
  736. }
  737. case FilesystemActionMkdirs:
  738. c.Renames = nil
  739. c.Deletes = nil
  740. c.Exist = nil
  741. c.Copy = nil
  742. c.Compress = EventActionFsCompress{}
  743. if err := c.validateMkdirs(); err != nil {
  744. return err
  745. }
  746. case FilesystemActionExist:
  747. c.Renames = nil
  748. c.Deletes = nil
  749. c.MkDirs = nil
  750. c.Copy = nil
  751. c.Compress = EventActionFsCompress{}
  752. if err := c.validateExist(); err != nil {
  753. return err
  754. }
  755. case FilesystemActionCompress:
  756. c.Renames = nil
  757. c.MkDirs = nil
  758. c.Deletes = nil
  759. c.Exist = nil
  760. c.Copy = nil
  761. if err := c.Compress.validate(); err != nil {
  762. return err
  763. }
  764. case FilesystemActionCopy:
  765. c.Renames = nil
  766. c.Deletes = nil
  767. c.MkDirs = nil
  768. c.Exist = nil
  769. c.Compress = EventActionFsCompress{}
  770. if err := c.validateCopy(); err != nil {
  771. return err
  772. }
  773. }
  774. return nil
  775. }
  776. func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {
  777. mkdirs := make([]string, len(c.MkDirs))
  778. copy(mkdirs, c.MkDirs)
  779. deletes := make([]string, len(c.Deletes))
  780. copy(deletes, c.Deletes)
  781. exist := make([]string, len(c.Exist))
  782. copy(exist, c.Exist)
  783. compressPaths := make([]string, len(c.Compress.Paths))
  784. copy(compressPaths, c.Compress.Paths)
  785. return EventActionFilesystemConfig{
  786. Type: c.Type,
  787. Renames: cloneKeyValues(c.Renames),
  788. MkDirs: mkdirs,
  789. Deletes: deletes,
  790. Exist: exist,
  791. Copy: cloneKeyValues(c.Copy),
  792. Compress: EventActionFsCompress{
  793. Paths: compressPaths,
  794. Name: c.Compress.Name,
  795. },
  796. }
  797. }
  798. // EventActionPasswordExpiration defines the configuration for password expiration actions
  799. type EventActionPasswordExpiration struct {
  800. // An email notification will be generated for users whose password expires in a number
  801. // of days less than or equal to this threshold
  802. Threshold int `json:"threshold,omitempty"`
  803. }
  804. func (c *EventActionPasswordExpiration) validate() error {
  805. if c.Threshold <= 0 {
  806. return util.NewValidationError("threshold must be greater than 0")
  807. }
  808. return nil
  809. }
  810. // EventActionIDPAccountCheck defines the check to execute after a successful IDP login
  811. type EventActionIDPAccountCheck struct {
  812. // 0 create/update, 1 create the account if it doesn't exist
  813. Mode int `json:"mode,omitempty"`
  814. TemplateUser string `json:"template_user,omitempty"`
  815. TemplateAdmin string `json:"template_admin,omitempty"`
  816. }
  817. func (c *EventActionIDPAccountCheck) validate() error {
  818. if c.TemplateAdmin == "" && c.TemplateUser == "" {
  819. return util.NewValidationError("at least a template must be set")
  820. }
  821. if c.Mode < 0 || c.Mode > 1 {
  822. return util.NewValidationError(fmt.Sprintf("invalid account check mode: %d", c.Mode))
  823. }
  824. return nil
  825. }
  826. // BaseEventActionOptions defines the supported configuration options for a base event actions
  827. type BaseEventActionOptions struct {
  828. HTTPConfig EventActionHTTPConfig `json:"http_config"`
  829. CmdConfig EventActionCommandConfig `json:"cmd_config"`
  830. EmailConfig EventActionEmailConfig `json:"email_config"`
  831. RetentionConfig EventActionDataRetentionConfig `json:"retention_config"`
  832. FsConfig EventActionFilesystemConfig `json:"fs_config"`
  833. PwdExpirationConfig EventActionPasswordExpiration `json:"pwd_expiration_config"`
  834. IDPConfig EventActionIDPAccountCheck `json:"idp_config"`
  835. }
  836. func (o *BaseEventActionOptions) getACopy() BaseEventActionOptions {
  837. o.SetEmptySecretsIfNil()
  838. emailRecipients := make([]string, len(o.EmailConfig.Recipients))
  839. copy(emailRecipients, o.EmailConfig.Recipients)
  840. emailBcc := make([]string, len(o.EmailConfig.Bcc))
  841. copy(emailBcc, o.EmailConfig.Bcc)
  842. emailAttachments := make([]string, len(o.EmailConfig.Attachments))
  843. copy(emailAttachments, o.EmailConfig.Attachments)
  844. cmdArgs := make([]string, len(o.CmdConfig.Args))
  845. copy(cmdArgs, o.CmdConfig.Args)
  846. folders := make([]FolderRetention, 0, len(o.RetentionConfig.Folders))
  847. for _, folder := range o.RetentionConfig.Folders {
  848. folders = append(folders, FolderRetention{
  849. Path: folder.Path,
  850. Retention: folder.Retention,
  851. DeleteEmptyDirs: folder.DeleteEmptyDirs,
  852. IgnoreUserPermissions: folder.IgnoreUserPermissions,
  853. })
  854. }
  855. httpParts := make([]HTTPPart, 0, len(o.HTTPConfig.Parts))
  856. for _, part := range o.HTTPConfig.Parts {
  857. httpParts = append(httpParts, HTTPPart{
  858. Name: part.Name,
  859. Filepath: part.Filepath,
  860. Headers: cloneKeyValues(part.Headers),
  861. Body: part.Body,
  862. })
  863. }
  864. return BaseEventActionOptions{
  865. HTTPConfig: EventActionHTTPConfig{
  866. Endpoint: o.HTTPConfig.Endpoint,
  867. Username: o.HTTPConfig.Username,
  868. Password: o.HTTPConfig.Password.Clone(),
  869. Headers: cloneKeyValues(o.HTTPConfig.Headers),
  870. Timeout: o.HTTPConfig.Timeout,
  871. SkipTLSVerify: o.HTTPConfig.SkipTLSVerify,
  872. Method: o.HTTPConfig.Method,
  873. QueryParameters: cloneKeyValues(o.HTTPConfig.QueryParameters),
  874. Body: o.HTTPConfig.Body,
  875. Parts: httpParts,
  876. },
  877. CmdConfig: EventActionCommandConfig{
  878. Cmd: o.CmdConfig.Cmd,
  879. Args: cmdArgs,
  880. Timeout: o.CmdConfig.Timeout,
  881. EnvVars: cloneKeyValues(o.CmdConfig.EnvVars),
  882. },
  883. EmailConfig: EventActionEmailConfig{
  884. Recipients: emailRecipients,
  885. Bcc: emailBcc,
  886. Subject: o.EmailConfig.Subject,
  887. ContentType: o.EmailConfig.ContentType,
  888. Body: o.EmailConfig.Body,
  889. Attachments: emailAttachments,
  890. },
  891. RetentionConfig: EventActionDataRetentionConfig{
  892. Folders: folders,
  893. },
  894. PwdExpirationConfig: EventActionPasswordExpiration{
  895. Threshold: o.PwdExpirationConfig.Threshold,
  896. },
  897. IDPConfig: EventActionIDPAccountCheck{
  898. Mode: o.IDPConfig.Mode,
  899. TemplateUser: o.IDPConfig.TemplateUser,
  900. TemplateAdmin: o.IDPConfig.TemplateAdmin,
  901. },
  902. FsConfig: o.FsConfig.getACopy(),
  903. }
  904. }
  905. // SetEmptySecretsIfNil sets the secrets to empty if nil
  906. func (o *BaseEventActionOptions) SetEmptySecretsIfNil() {
  907. if o.HTTPConfig.Password == nil {
  908. o.HTTPConfig.Password = kms.NewEmptySecret()
  909. }
  910. }
  911. func (o *BaseEventActionOptions) setNilSecretsIfEmpty() {
  912. if o.HTTPConfig.Password != nil && o.HTTPConfig.Password.IsEmpty() {
  913. o.HTTPConfig.Password = nil
  914. }
  915. }
  916. func (o *BaseEventActionOptions) hideConfidentialData() {
  917. if o.HTTPConfig.Password != nil {
  918. o.HTTPConfig.Password.Hide()
  919. }
  920. }
  921. func (o *BaseEventActionOptions) validate(action int, name string) error {
  922. o.SetEmptySecretsIfNil()
  923. switch action {
  924. case ActionTypeHTTP:
  925. o.CmdConfig = EventActionCommandConfig{}
  926. o.EmailConfig = EventActionEmailConfig{}
  927. o.RetentionConfig = EventActionDataRetentionConfig{}
  928. o.FsConfig = EventActionFilesystemConfig{}
  929. o.PwdExpirationConfig = EventActionPasswordExpiration{}
  930. o.IDPConfig = EventActionIDPAccountCheck{}
  931. return o.HTTPConfig.validate(name)
  932. case ActionTypeCommand:
  933. o.HTTPConfig = EventActionHTTPConfig{}
  934. o.EmailConfig = EventActionEmailConfig{}
  935. o.RetentionConfig = EventActionDataRetentionConfig{}
  936. o.FsConfig = EventActionFilesystemConfig{}
  937. o.PwdExpirationConfig = EventActionPasswordExpiration{}
  938. o.IDPConfig = EventActionIDPAccountCheck{}
  939. return o.CmdConfig.validate()
  940. case ActionTypeEmail:
  941. o.HTTPConfig = EventActionHTTPConfig{}
  942. o.CmdConfig = EventActionCommandConfig{}
  943. o.RetentionConfig = EventActionDataRetentionConfig{}
  944. o.FsConfig = EventActionFilesystemConfig{}
  945. o.PwdExpirationConfig = EventActionPasswordExpiration{}
  946. o.IDPConfig = EventActionIDPAccountCheck{}
  947. return o.EmailConfig.validate()
  948. case ActionTypeDataRetentionCheck:
  949. o.HTTPConfig = EventActionHTTPConfig{}
  950. o.CmdConfig = EventActionCommandConfig{}
  951. o.EmailConfig = EventActionEmailConfig{}
  952. o.FsConfig = EventActionFilesystemConfig{}
  953. o.PwdExpirationConfig = EventActionPasswordExpiration{}
  954. o.IDPConfig = EventActionIDPAccountCheck{}
  955. return o.RetentionConfig.validate()
  956. case ActionTypeFilesystem:
  957. o.HTTPConfig = EventActionHTTPConfig{}
  958. o.CmdConfig = EventActionCommandConfig{}
  959. o.EmailConfig = EventActionEmailConfig{}
  960. o.RetentionConfig = EventActionDataRetentionConfig{}
  961. o.PwdExpirationConfig = EventActionPasswordExpiration{}
  962. o.IDPConfig = EventActionIDPAccountCheck{}
  963. return o.FsConfig.validate()
  964. case ActionTypePasswordExpirationCheck:
  965. o.HTTPConfig = EventActionHTTPConfig{}
  966. o.CmdConfig = EventActionCommandConfig{}
  967. o.EmailConfig = EventActionEmailConfig{}
  968. o.RetentionConfig = EventActionDataRetentionConfig{}
  969. o.FsConfig = EventActionFilesystemConfig{}
  970. o.IDPConfig = EventActionIDPAccountCheck{}
  971. return o.PwdExpirationConfig.validate()
  972. case ActionTypeIDPAccountCheck:
  973. o.HTTPConfig = EventActionHTTPConfig{}
  974. o.CmdConfig = EventActionCommandConfig{}
  975. o.EmailConfig = EventActionEmailConfig{}
  976. o.RetentionConfig = EventActionDataRetentionConfig{}
  977. o.FsConfig = EventActionFilesystemConfig{}
  978. o.PwdExpirationConfig = EventActionPasswordExpiration{}
  979. return o.IDPConfig.validate()
  980. default:
  981. o.HTTPConfig = EventActionHTTPConfig{}
  982. o.CmdConfig = EventActionCommandConfig{}
  983. o.EmailConfig = EventActionEmailConfig{}
  984. o.RetentionConfig = EventActionDataRetentionConfig{}
  985. o.FsConfig = EventActionFilesystemConfig{}
  986. o.PwdExpirationConfig = EventActionPasswordExpiration{}
  987. o.IDPConfig = EventActionIDPAccountCheck{}
  988. }
  989. return nil
  990. }
  991. // BaseEventAction defines the common fields for an event action
  992. type BaseEventAction struct {
  993. // Data provider unique identifier
  994. ID int64 `json:"id"`
  995. // Action name
  996. Name string `json:"name"`
  997. // optional description
  998. Description string `json:"description,omitempty"`
  999. // ActionType, see the above enum
  1000. Type int `json:"type"`
  1001. // Configuration options specific for the action type
  1002. Options BaseEventActionOptions `json:"options"`
  1003. // list of rule names associated with this event action
  1004. Rules []string `json:"rules,omitempty"`
  1005. }
  1006. func (a *BaseEventAction) getACopy() BaseEventAction {
  1007. rules := make([]string, len(a.Rules))
  1008. copy(rules, a.Rules)
  1009. return BaseEventAction{
  1010. ID: a.ID,
  1011. Name: a.Name,
  1012. Description: a.Description,
  1013. Type: a.Type,
  1014. Options: a.Options.getACopy(),
  1015. Rules: rules,
  1016. }
  1017. }
  1018. // GetTypeAsString returns the action type as string
  1019. func (a *BaseEventAction) GetTypeAsString() string {
  1020. return getActionTypeAsString(a.Type)
  1021. }
  1022. // GetRulesAsString returns the list of rules as comma separated string
  1023. func (a *BaseEventAction) GetRulesAsString() string {
  1024. return strings.Join(a.Rules, ",")
  1025. }
  1026. // PrepareForRendering prepares a BaseEventAction for rendering.
  1027. // It hides confidential data and set to nil the empty secrets
  1028. // so they are not serialized
  1029. func (a *BaseEventAction) PrepareForRendering() {
  1030. a.Options.setNilSecretsIfEmpty()
  1031. a.Options.hideConfidentialData()
  1032. }
  1033. // RenderAsJSON implements the renderer interface used within plugins
  1034. func (a *BaseEventAction) RenderAsJSON(reload bool) ([]byte, error) {
  1035. if reload {
  1036. action, err := provider.eventActionExists(a.Name)
  1037. if err != nil {
  1038. providerLog(logger.LevelError, "unable to reload event action before rendering as json: %v", err)
  1039. return nil, err
  1040. }
  1041. action.PrepareForRendering()
  1042. return json.Marshal(action)
  1043. }
  1044. a.PrepareForRendering()
  1045. return json.Marshal(a)
  1046. }
  1047. func (a *BaseEventAction) validate() error {
  1048. if a.Name == "" {
  1049. return util.NewValidationError("name is mandatory")
  1050. }
  1051. if !isActionTypeValid(a.Type) {
  1052. return util.NewValidationError(fmt.Sprintf("invalid action type: %d", a.Type))
  1053. }
  1054. return a.Options.validate(a.Type, a.Name)
  1055. }
  1056. // EventActionOptions defines the supported configuration options for an event action
  1057. type EventActionOptions struct {
  1058. IsFailureAction bool `json:"is_failure_action"`
  1059. StopOnFailure bool `json:"stop_on_failure"`
  1060. ExecuteSync bool `json:"execute_sync"`
  1061. }
  1062. // EventAction defines an event action
  1063. type EventAction struct {
  1064. BaseEventAction
  1065. // Order defines the execution order
  1066. Order int `json:"order,omitempty"`
  1067. Options EventActionOptions `json:"relation_options"`
  1068. }
  1069. func (a *EventAction) getACopy() EventAction {
  1070. return EventAction{
  1071. BaseEventAction: a.BaseEventAction.getACopy(),
  1072. Order: a.Order,
  1073. Options: EventActionOptions{
  1074. IsFailureAction: a.Options.IsFailureAction,
  1075. StopOnFailure: a.Options.StopOnFailure,
  1076. ExecuteSync: a.Options.ExecuteSync,
  1077. },
  1078. }
  1079. }
  1080. func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error {
  1081. if a.Options.IsFailureAction {
  1082. if a.Options.ExecuteSync {
  1083. return util.NewValidationError("sync execution is not supported for failure actions")
  1084. }
  1085. }
  1086. if a.Options.ExecuteSync {
  1087. if trigger != EventTriggerFsEvent && trigger != EventTriggerIDPLogin {
  1088. return util.NewValidationError("sync execution is only supported for some filesystem events and Identity Provider logins")
  1089. }
  1090. if trigger == EventTriggerFsEvent {
  1091. for _, ev := range fsEvents {
  1092. if !util.Contains(allowedSyncFsEvents, ev) {
  1093. return util.NewValidationError("sync execution is only supported for upload and pre-* events")
  1094. }
  1095. }
  1096. }
  1097. }
  1098. return nil
  1099. }
  1100. // ConditionPattern defines a pattern for condition filters
  1101. type ConditionPattern struct {
  1102. Pattern string `json:"pattern,omitempty"`
  1103. InverseMatch bool `json:"inverse_match,omitempty"`
  1104. }
  1105. func (p *ConditionPattern) validate() error {
  1106. if p.Pattern == "" {
  1107. return util.NewValidationError("empty condition pattern not allowed")
  1108. }
  1109. _, err := path.Match(p.Pattern, "abc")
  1110. if err != nil {
  1111. return util.NewValidationError(fmt.Sprintf("invalid condition pattern %q", p.Pattern))
  1112. }
  1113. return nil
  1114. }
  1115. // ConditionOptions defines options for event conditions
  1116. type ConditionOptions struct {
  1117. // Usernames or folder names
  1118. Names []ConditionPattern `json:"names,omitempty"`
  1119. // Group names
  1120. GroupNames []ConditionPattern `json:"group_names,omitempty"`
  1121. // Role names
  1122. RoleNames []ConditionPattern `json:"role_names,omitempty"`
  1123. // Virtual paths
  1124. FsPaths []ConditionPattern `json:"fs_paths,omitempty"`
  1125. Protocols []string `json:"protocols,omitempty"`
  1126. ProviderObjects []string `json:"provider_objects,omitempty"`
  1127. MinFileSize int64 `json:"min_size,omitempty"`
  1128. MaxFileSize int64 `json:"max_size,omitempty"`
  1129. // allow to execute scheduled tasks concurrently from multiple instances
  1130. ConcurrentExecution bool `json:"concurrent_execution,omitempty"`
  1131. }
  1132. func (f *ConditionOptions) getACopy() ConditionOptions {
  1133. protocols := make([]string, len(f.Protocols))
  1134. copy(protocols, f.Protocols)
  1135. providerObjects := make([]string, len(f.ProviderObjects))
  1136. copy(providerObjects, f.ProviderObjects)
  1137. return ConditionOptions{
  1138. Names: cloneConditionPatterns(f.Names),
  1139. GroupNames: cloneConditionPatterns(f.GroupNames),
  1140. RoleNames: cloneConditionPatterns(f.RoleNames),
  1141. FsPaths: cloneConditionPatterns(f.FsPaths),
  1142. Protocols: protocols,
  1143. ProviderObjects: providerObjects,
  1144. MinFileSize: f.MinFileSize,
  1145. MaxFileSize: f.MaxFileSize,
  1146. ConcurrentExecution: f.ConcurrentExecution,
  1147. }
  1148. }
  1149. func (f *ConditionOptions) validate() error {
  1150. if err := validateConditionPatterns(f.Names); err != nil {
  1151. return err
  1152. }
  1153. if err := validateConditionPatterns(f.GroupNames); err != nil {
  1154. return err
  1155. }
  1156. if err := validateConditionPatterns(f.RoleNames); err != nil {
  1157. return err
  1158. }
  1159. if err := validateConditionPatterns(f.FsPaths); err != nil {
  1160. return err
  1161. }
  1162. for _, p := range f.Protocols {
  1163. if !util.Contains(SupportedRuleConditionProtocols, p) {
  1164. return util.NewValidationError(fmt.Sprintf("unsupported rule condition protocol: %q", p))
  1165. }
  1166. }
  1167. for _, p := range f.ProviderObjects {
  1168. if !util.Contains(SupporteRuleConditionProviderObjects, p) {
  1169. return util.NewValidationError(fmt.Sprintf("unsupported provider object: %q", p))
  1170. }
  1171. }
  1172. if f.MinFileSize > 0 && f.MaxFileSize > 0 {
  1173. if f.MaxFileSize <= f.MinFileSize {
  1174. return util.NewValidationError(fmt.Sprintf("invalid max file size %s, it is lesser or equal than min file size %s",
  1175. util.ByteCountSI(f.MaxFileSize), util.ByteCountSI(f.MinFileSize)))
  1176. }
  1177. }
  1178. if config.IsShared == 0 {
  1179. f.ConcurrentExecution = false
  1180. }
  1181. return nil
  1182. }
  1183. // Schedule defines an event schedule
  1184. type Schedule struct {
  1185. Hours string `json:"hour"`
  1186. DayOfWeek string `json:"day_of_week"`
  1187. DayOfMonth string `json:"day_of_month"`
  1188. Month string `json:"month"`
  1189. }
  1190. // GetCronSpec returns the cron compatible schedule string
  1191. func (s *Schedule) GetCronSpec() string {
  1192. return fmt.Sprintf("0 %s %s %s %s", s.Hours, s.DayOfMonth, s.Month, s.DayOfWeek)
  1193. }
  1194. func (s *Schedule) validate() error {
  1195. _, err := cron.ParseStandard(s.GetCronSpec())
  1196. if err != nil {
  1197. return util.NewValidationError(fmt.Sprintf("invalid schedule, hour: %q, day of month: %q, month: %q, day of week: %q",
  1198. s.Hours, s.DayOfMonth, s.Month, s.DayOfWeek))
  1199. }
  1200. return nil
  1201. }
  1202. // EventConditions defines the conditions for an event rule
  1203. type EventConditions struct {
  1204. // Only one between FsEvents, ProviderEvents and Schedule is allowed
  1205. FsEvents []string `json:"fs_events,omitempty"`
  1206. ProviderEvents []string `json:"provider_events,omitempty"`
  1207. Schedules []Schedule `json:"schedules,omitempty"`
  1208. // 0 any, 1 user, 2 admin
  1209. IDPLoginEvent int `json:"idp_login_event,omitempty"`
  1210. Options ConditionOptions `json:"options"`
  1211. }
  1212. func (c *EventConditions) getACopy() EventConditions {
  1213. fsEvents := make([]string, len(c.FsEvents))
  1214. copy(fsEvents, c.FsEvents)
  1215. providerEvents := make([]string, len(c.ProviderEvents))
  1216. copy(providerEvents, c.ProviderEvents)
  1217. schedules := make([]Schedule, 0, len(c.Schedules))
  1218. for _, schedule := range c.Schedules {
  1219. schedules = append(schedules, Schedule{
  1220. Hours: schedule.Hours,
  1221. DayOfWeek: schedule.DayOfWeek,
  1222. DayOfMonth: schedule.DayOfMonth,
  1223. Month: schedule.Month,
  1224. })
  1225. }
  1226. return EventConditions{
  1227. FsEvents: fsEvents,
  1228. ProviderEvents: providerEvents,
  1229. Schedules: schedules,
  1230. IDPLoginEvent: c.IDPLoginEvent,
  1231. Options: c.Options.getACopy(),
  1232. }
  1233. }
  1234. func (c *EventConditions) validateSchedules() error {
  1235. if len(c.Schedules) == 0 {
  1236. return util.NewValidationError("at least one schedule is required")
  1237. }
  1238. for _, schedule := range c.Schedules {
  1239. if err := schedule.validate(); err != nil {
  1240. return err
  1241. }
  1242. }
  1243. return nil
  1244. }
  1245. func (c *EventConditions) validate(trigger int) error {
  1246. switch trigger {
  1247. case EventTriggerFsEvent:
  1248. c.ProviderEvents = nil
  1249. c.Schedules = nil
  1250. c.Options.ProviderObjects = nil
  1251. c.IDPLoginEvent = 0
  1252. if len(c.FsEvents) == 0 {
  1253. return util.NewValidationError("at least one filesystem event is required")
  1254. }
  1255. for _, ev := range c.FsEvents {
  1256. if !util.Contains(SupportedFsEvents, ev) {
  1257. return util.NewValidationError(fmt.Sprintf("unsupported fs event: %q", ev))
  1258. }
  1259. }
  1260. case EventTriggerProviderEvent:
  1261. c.FsEvents = nil
  1262. c.Schedules = nil
  1263. c.Options.GroupNames = nil
  1264. c.Options.FsPaths = nil
  1265. c.Options.Protocols = nil
  1266. c.Options.MinFileSize = 0
  1267. c.Options.MaxFileSize = 0
  1268. c.IDPLoginEvent = 0
  1269. if len(c.ProviderEvents) == 0 {
  1270. return util.NewValidationError("at least one provider event is required")
  1271. }
  1272. for _, ev := range c.ProviderEvents {
  1273. if !util.Contains(SupportedProviderEvents, ev) {
  1274. return util.NewValidationError(fmt.Sprintf("unsupported provider event: %q", ev))
  1275. }
  1276. }
  1277. case EventTriggerSchedule:
  1278. c.FsEvents = nil
  1279. c.ProviderEvents = nil
  1280. c.Options.FsPaths = nil
  1281. c.Options.Protocols = nil
  1282. c.Options.MinFileSize = 0
  1283. c.Options.MaxFileSize = 0
  1284. c.Options.ProviderObjects = nil
  1285. c.IDPLoginEvent = 0
  1286. if err := c.validateSchedules(); err != nil {
  1287. return err
  1288. }
  1289. case EventTriggerIPBlocked, EventTriggerCertificate:
  1290. c.FsEvents = nil
  1291. c.ProviderEvents = nil
  1292. c.Options.Names = nil
  1293. c.Options.GroupNames = nil
  1294. c.Options.RoleNames = nil
  1295. c.Options.FsPaths = nil
  1296. c.Options.Protocols = nil
  1297. c.Options.MinFileSize = 0
  1298. c.Options.MaxFileSize = 0
  1299. c.Schedules = nil
  1300. c.IDPLoginEvent = 0
  1301. case EventTriggerOnDemand:
  1302. c.FsEvents = nil
  1303. c.ProviderEvents = nil
  1304. c.Options.FsPaths = nil
  1305. c.Options.Protocols = nil
  1306. c.Options.MinFileSize = 0
  1307. c.Options.MaxFileSize = 0
  1308. c.Options.ProviderObjects = nil
  1309. c.Schedules = nil
  1310. c.IDPLoginEvent = 0
  1311. c.Options.ConcurrentExecution = false
  1312. case EventTriggerIDPLogin:
  1313. c.FsEvents = nil
  1314. c.ProviderEvents = nil
  1315. c.Options.GroupNames = nil
  1316. c.Options.RoleNames = nil
  1317. c.Options.FsPaths = nil
  1318. c.Options.Protocols = nil
  1319. c.Options.MinFileSize = 0
  1320. c.Options.MaxFileSize = 0
  1321. c.Schedules = nil
  1322. if !util.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
  1323. return util.NewValidationError(fmt.Sprintf("invalid Identity Provider login event %d", c.IDPLoginEvent))
  1324. }
  1325. default:
  1326. c.FsEvents = nil
  1327. c.ProviderEvents = nil
  1328. c.Options.GroupNames = nil
  1329. c.Options.RoleNames = nil
  1330. c.Options.FsPaths = nil
  1331. c.Options.Protocols = nil
  1332. c.Options.MinFileSize = 0
  1333. c.Options.MaxFileSize = 0
  1334. c.Schedules = nil
  1335. c.IDPLoginEvent = 0
  1336. }
  1337. return c.Options.validate()
  1338. }
  1339. // EventRule defines the trigger, conditions and actions for an event
  1340. type EventRule struct {
  1341. // Data provider unique identifier
  1342. ID int64 `json:"id"`
  1343. // Rule name
  1344. Name string `json:"name"`
  1345. // 1 enabled, 0 disabled
  1346. Status int `json:"status"`
  1347. // optional description
  1348. Description string `json:"description,omitempty"`
  1349. // Creation time as unix timestamp in milliseconds
  1350. CreatedAt int64 `json:"created_at"`
  1351. // last update time as unix timestamp in milliseconds
  1352. UpdatedAt int64 `json:"updated_at"`
  1353. // Event trigger
  1354. Trigger int `json:"trigger"`
  1355. // Event conditions
  1356. Conditions EventConditions `json:"conditions"`
  1357. // actions to execute
  1358. Actions []EventAction `json:"actions"`
  1359. // in multi node setups we mark the rule as deleted to be able to update the cache
  1360. DeletedAt int64 `json:"-"`
  1361. }
  1362. func (r *EventRule) getACopy() EventRule {
  1363. actions := make([]EventAction, 0, len(r.Actions))
  1364. for _, action := range r.Actions {
  1365. actions = append(actions, action.getACopy())
  1366. }
  1367. return EventRule{
  1368. ID: r.ID,
  1369. Name: r.Name,
  1370. Status: r.Status,
  1371. Description: r.Description,
  1372. CreatedAt: r.CreatedAt,
  1373. UpdatedAt: r.UpdatedAt,
  1374. Trigger: r.Trigger,
  1375. Conditions: r.Conditions.getACopy(),
  1376. Actions: actions,
  1377. DeletedAt: r.DeletedAt,
  1378. }
  1379. }
  1380. // GuardFromConcurrentExecution returns true if the rule cannot be executed concurrently
  1381. // from multiple instances
  1382. func (r *EventRule) GuardFromConcurrentExecution() bool {
  1383. if config.IsShared == 0 {
  1384. return false
  1385. }
  1386. return !r.Conditions.Options.ConcurrentExecution
  1387. }
  1388. // GetTriggerAsString returns the rule trigger as string
  1389. func (r *EventRule) GetTriggerAsString() string {
  1390. return getTriggerTypeAsString(r.Trigger)
  1391. }
  1392. // GetActionsAsString returns the list of action names as comma separated string
  1393. func (r *EventRule) GetActionsAsString() string {
  1394. actions := make([]string, 0, len(r.Actions))
  1395. for _, action := range r.Actions {
  1396. actions = append(actions, action.Name)
  1397. }
  1398. return strings.Join(actions, ",")
  1399. }
  1400. func (r *EventRule) isStatusValid() bool {
  1401. return r.Status >= 0 && r.Status <= 1
  1402. }
  1403. func (r *EventRule) validate() error {
  1404. if r.Name == "" {
  1405. return util.NewValidationError("name is mandatory")
  1406. }
  1407. if !r.isStatusValid() {
  1408. return util.NewValidationError(fmt.Sprintf("invalid event rule status: %d", r.Status))
  1409. }
  1410. if !isEventTriggerValid(r.Trigger) {
  1411. return util.NewValidationError(fmt.Sprintf("invalid event rule trigger: %d", r.Trigger))
  1412. }
  1413. if err := r.Conditions.validate(r.Trigger); err != nil {
  1414. return err
  1415. }
  1416. if len(r.Actions) == 0 {
  1417. return util.NewValidationError("at least one action is required")
  1418. }
  1419. actionNames := make(map[string]bool)
  1420. actionOrders := make(map[int]bool)
  1421. failureActions := 0
  1422. hasSyncAction := false
  1423. for idx := range r.Actions {
  1424. if r.Actions[idx].Name == "" {
  1425. return util.NewValidationError(fmt.Sprintf("invalid action at position %d, name not specified", idx))
  1426. }
  1427. if actionNames[r.Actions[idx].Name] {
  1428. return util.NewValidationError(fmt.Sprintf("duplicated action %q", r.Actions[idx].Name))
  1429. }
  1430. if actionOrders[r.Actions[idx].Order] {
  1431. return util.NewValidationError(fmt.Sprintf("duplicated order %d for action %q",
  1432. r.Actions[idx].Order, r.Actions[idx].Name))
  1433. }
  1434. if err := r.Actions[idx].validateAssociation(r.Trigger, r.Conditions.FsEvents); err != nil {
  1435. return err
  1436. }
  1437. if r.Actions[idx].Options.IsFailureAction {
  1438. failureActions++
  1439. }
  1440. if r.Actions[idx].Options.ExecuteSync {
  1441. hasSyncAction = true
  1442. }
  1443. actionNames[r.Actions[idx].Name] = true
  1444. actionOrders[r.Actions[idx].Order] = true
  1445. }
  1446. if len(r.Actions) == failureActions {
  1447. return util.NewValidationError("at least a non-failure action is required")
  1448. }
  1449. if !hasSyncAction {
  1450. return r.validateMandatorySyncActions()
  1451. }
  1452. return nil
  1453. }
  1454. func (r *EventRule) validateMandatorySyncActions() error {
  1455. if r.Trigger != EventTriggerFsEvent {
  1456. return nil
  1457. }
  1458. for _, ev := range r.Conditions.FsEvents {
  1459. if util.Contains(mandatorySyncFsEvents, ev) {
  1460. return util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev))
  1461. }
  1462. }
  1463. return nil
  1464. }
  1465. func (r *EventRule) checkIPBlockedAndCertificateActions() error {
  1466. unavailableActions := []int{ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
  1467. ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck,
  1468. ActionTypeUserExpirationCheck}
  1469. for _, action := range r.Actions {
  1470. if util.Contains(unavailableActions, action.Type) {
  1471. return fmt.Errorf("action %q, type %q is not supported for event trigger %q",
  1472. action.Name, getActionTypeAsString(action.Type), getTriggerTypeAsString(r.Trigger))
  1473. }
  1474. }
  1475. return nil
  1476. }
  1477. func (r *EventRule) checkProviderEventActions(providerObjectType string) error {
  1478. // user quota reset, transfer quota reset, data retention check and filesystem actions
  1479. // can be executed only if we modify a user. They will be executed for the
  1480. // affected user. Folder quota reset can be executed only for folders.
  1481. userSpecificActions := []int{ActionTypeUserQuotaReset, ActionTypeTransferQuotaReset,
  1482. ActionTypeDataRetentionCheck, ActionTypeMetadataCheck, ActionTypeFilesystem,
  1483. ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck}
  1484. for _, action := range r.Actions {
  1485. if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
  1486. return fmt.Errorf("action %q, type %q is only supported for provider user events",
  1487. action.Name, getActionTypeAsString(action.Type))
  1488. }
  1489. if action.Type == ActionTypeFolderQuotaReset && providerObjectType != actionObjectFolder {
  1490. return fmt.Errorf("action %q, type %q is only supported for provider folder events",
  1491. action.Name, getActionTypeAsString(action.Type))
  1492. }
  1493. }
  1494. return nil
  1495. }
  1496. func (r *EventRule) hasUserAssociated(providerObjectType string) bool {
  1497. switch r.Trigger {
  1498. case EventTriggerProviderEvent:
  1499. return providerObjectType == actionObjectUser
  1500. case EventTriggerFsEvent:
  1501. return true
  1502. default:
  1503. if len(r.Actions) > 0 {
  1504. // should we allow schedules where backup is not the first action?
  1505. // maybe we could pass the action index and check before that index
  1506. return r.Actions[0].Type == ActionTypeBackup
  1507. }
  1508. }
  1509. return false
  1510. }
  1511. func (r *EventRule) checkActions(providerObjectType string) error {
  1512. numSyncAction := 0
  1513. hasIDPAccountCheck := false
  1514. for _, action := range r.Actions {
  1515. if action.Options.ExecuteSync {
  1516. numSyncAction++
  1517. }
  1518. if action.Type == ActionTypeEmail && action.BaseEventAction.Options.EmailConfig.hasFilesAttachments() {
  1519. if !r.hasUserAssociated(providerObjectType) {
  1520. return errors.New("cannot send an email with attachments for a rule with no user associated")
  1521. }
  1522. }
  1523. if action.Type == ActionTypeHTTP && action.BaseEventAction.Options.HTTPConfig.HasMultipartFiles() {
  1524. if !r.hasUserAssociated(providerObjectType) {
  1525. return errors.New("cannot upload file/s for a rule with no user associated")
  1526. }
  1527. }
  1528. if action.Type == ActionTypeIDPAccountCheck {
  1529. if r.Trigger != EventTriggerIDPLogin {
  1530. return errors.New("IDP account check action is only supported for IDP login trigger")
  1531. }
  1532. if !action.Options.ExecuteSync {
  1533. return errors.New("IDP account check must be a sync action")
  1534. }
  1535. hasIDPAccountCheck = true
  1536. }
  1537. }
  1538. if hasIDPAccountCheck && numSyncAction != 1 {
  1539. return errors.New("IDP account check must be the only sync action")
  1540. }
  1541. return nil
  1542. }
  1543. // CheckActionsConsistency returns an error if the actions cannot be executed
  1544. func (r *EventRule) CheckActionsConsistency(providerObjectType string) error {
  1545. switch r.Trigger {
  1546. case EventTriggerProviderEvent:
  1547. if err := r.checkProviderEventActions(providerObjectType); err != nil {
  1548. return err
  1549. }
  1550. case EventTriggerFsEvent:
  1551. // folder quota reset cannot be executed
  1552. for _, action := range r.Actions {
  1553. if action.Type == ActionTypeFolderQuotaReset {
  1554. return fmt.Errorf("action %q, type %q is not supported for filesystem events",
  1555. action.Name, getActionTypeAsString(action.Type))
  1556. }
  1557. }
  1558. case EventTriggerIPBlocked, EventTriggerCertificate:
  1559. if err := r.checkIPBlockedAndCertificateActions(); err != nil {
  1560. return err
  1561. }
  1562. }
  1563. return r.checkActions(providerObjectType)
  1564. }
  1565. // PrepareForRendering prepares an EventRule for rendering.
  1566. // It hides confidential data and set to nil the empty secrets
  1567. // so they are not serialized
  1568. func (r *EventRule) PrepareForRendering() {
  1569. for idx := range r.Actions {
  1570. r.Actions[idx].PrepareForRendering()
  1571. }
  1572. }
  1573. // RenderAsJSON implements the renderer interface used within plugins
  1574. func (r *EventRule) RenderAsJSON(reload bool) ([]byte, error) {
  1575. if reload {
  1576. rule, err := provider.eventRuleExists(r.Name)
  1577. if err != nil {
  1578. providerLog(logger.LevelError, "unable to reload event rule before rendering as json: %v", err)
  1579. return nil, err
  1580. }
  1581. rule.PrepareForRendering()
  1582. return json.Marshal(rule)
  1583. }
  1584. r.PrepareForRendering()
  1585. return json.Marshal(r)
  1586. }
  1587. func cloneKeyValues(keyVals []KeyValue) []KeyValue {
  1588. res := make([]KeyValue, 0, len(keyVals))
  1589. for _, kv := range keyVals {
  1590. res = append(res, KeyValue{
  1591. Key: kv.Key,
  1592. Value: kv.Value,
  1593. })
  1594. }
  1595. return res
  1596. }
  1597. func cloneConditionPatterns(patterns []ConditionPattern) []ConditionPattern {
  1598. res := make([]ConditionPattern, 0, len(patterns))
  1599. for _, p := range patterns {
  1600. res = append(res, ConditionPattern{
  1601. Pattern: p.Pattern,
  1602. InverseMatch: p.InverseMatch,
  1603. })
  1604. }
  1605. return res
  1606. }
  1607. func validateConditionPatterns(patterns []ConditionPattern) error {
  1608. for _, name := range patterns {
  1609. if err := name.validate(); err != nil {
  1610. return err
  1611. }
  1612. }
  1613. return nil
  1614. }
  1615. // Task stores the state for a scheduled task
  1616. type Task struct {
  1617. Name string `json:"name"`
  1618. UpdateAt int64 `json:"updated_at"`
  1619. Version int64 `json:"version"`
  1620. }