traffic_ops/traffic_ops_golang/invalidationjobs/invalidationjobs.go (1,394 lines of code) (raw):

package invalidationjobs /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import ( "database/sql" "encoding/json" "errors" "fmt" "net/http" "regexp" "strconv" "strings" "time" "github.com/apache/trafficcontrol/v8/lib/go-log" "github.com/apache/trafficcontrol/v8/lib/go-rfc" "github.com/apache/trafficcontrol/v8/lib/go-tc" "github.com/apache/trafficcontrol/v8/lib/go-util" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/api" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/dbhelpers" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/tenant" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/util/ims" validation "github.com/go-ozzo/ozzo-validation" "github.com/go-ozzo/ozzo-validation/is" "github.com/lib/pq" ) // Deprecated, only to be used with versions below 4.0 type InvalidationJob struct { api.APIInfoImpl `json:"-"` tc.InvalidationJob } type InvalidationJobV4 struct { api.APIInfoImpl `json:"-"` tc.InvalidationJobV4 } // Deprecated, only to be used with versions below 4.0 const insertQuery = ` INSERT INTO job ( ttl_hr, asset_url, start_time, entered_time, job_user, job_deliveryservice, invalidation_type) VALUES ( $1, ( SELECT o.protocol::text || '://' || o.fqdn || rtrim(concat(':', o.port::text), ':') FROM origin o WHERE o.deliveryservice = $2 AND o.is_primary ) || $3, $4, $5, $6, $7, $8 ) RETURNING asset_url, ( SELECT deliveryservice.xml_id FROM deliveryservice WHERE deliveryservice.id=job_deliveryservice) AS deliveryservice, id, ( SELECT tm_user.username FROM tm_user WHERE tm_user.id=job_user) AS createdBy, 'PURGE' AS keyword, CONCAT('TTL:', ttl_hr, 'h') AS parameters, start_time ` // Almost the same as insertQuery, but returns appropriate values for API 4.0+ const insertQueryV4 = ` INSERT INTO job ( ttl_hr, asset_url, start_time, entered_time, job_user, job_deliveryservice, invalidation_type) VALUES ( $1, ( SELECT o.protocol::text || '://' || o.fqdn || rtrim(concat(':', o.port::text), ':') FROM origin o WHERE o.deliveryservice = $2 AND o.is_primary ) || $3, $4, $5, $6, $7, $8 ) RETURNING id, asset_url, ( SELECT tm_user.username FROM tm_user WHERE tm_user.id=job_user) AS createdBy, ( SELECT deliveryservice.xml_id FROM deliveryservice WHERE deliveryservice.id=job_deliveryservice) AS deliveryServiceXML, ttl_hr as ttlHrs, invalidation_type as invalidationType, start_time as startTime ` const queueUpdateOrRevalQuery = ` UPDATE public.server SET %s = now() WHERE server.status IN ( SELECT status.id FROM status WHERE name IN ('ONLINE', 'REPORTED', 'ADMIN_DOWN') ) AND server.profile IN ( SELECT profile_parameter.profile FROM profile_parameter WHERE profile_parameter.parameter IN ( SELECT parameter.id FROM parameter WHERE parameter.name='location' AND parameter.config_file='regex_revalidate.config' ) ) AND server.cdn_id = ( SELECT deliveryservice.cdn_id FROM deliveryservice WHERE deliveryservice.%s=$1 ); ` const updateQuery = ` UPDATE job SET asset_url=$1, ttl_hr=$2, start_time=$3 WHERE job.id=$4 RETURNING asset_url, ( SELECT tm_user.username FROM tm_user WHERE tm_user.id=job.job_user ) AS created_by, ( SELECT deliveryservice.xml_id FROM deliveryservice WHERE deliveryservice.id=job.job_deliveryservice ) AS delivery_service, job.id, 'PURGE' as keyword, CONCAT('TTL:', ttl_hr, 'h') AS parameters, start_time ` // Almost the same as updateQuery, but returns appropriate values for API 4.0+ const updateQueryV4 = ` UPDATE job SET asset_url=$1, ttl_hr=$2, start_time=$3, invalidation_type=$4 WHERE job.id=$5 RETURNING asset_url, ( SELECT tm_user.username FROM tm_user WHERE tm_user.id=job.job_user ) AS created_by, ( SELECT deliveryservice.xml_id FROM deliveryservice WHERE deliveryservice.id=job.job_deliveryservice ) AS delivery_service, job.id, ttl_hr, start_time, invalidation_type ` // Deprecated, only to be used with versions below 4.0 const putInfoQuery = ` SELECT job.id AS id, tm_user.username AS createdBy, job.job_user AS createdByID, job.job_deliveryservice AS dsid, deliveryservice.xml_id AS dsxmlid, job.asset_url AS assetURL, CONCAT('TTL:', ttl_hr, 'h') AS parameters, job.start_time AS start_time, origin.protocol || '://' || origin.fqdn || rtrim(concat(':', origin.port), ':') AS OFQDN FROM job INNER JOIN origin ON origin.deliveryservice=job.job_deliveryservice AND origin.is_primary INNER JOIN tm_user ON tm_user.id=job.job_user INNER JOIN deliveryservice ON deliveryservice.id=job.job_deliveryservice WHERE job.id=$1 ` // Almost the same as putInfoQuery, but returns appropriate values for API 4.0+ const putInfoQueryV4 = ` SELECT job.id AS id, tm_user.username AS createdBy, job.job_user AS createdByID, job.job_deliveryservice AS dsid, deliveryservice.xml_id AS dsxmlid, job.asset_url AS assetURL, job.ttl_hr AS ttlhrs, job.start_time AS start_time, job.invalidation_type as invalidationType, origin.protocol || '://' || origin.fqdn || rtrim(concat(':', origin.port), ':') AS OFQDN FROM job INNER JOIN origin ON origin.deliveryservice=job.job_deliveryservice AND origin.is_primary INNER JOIN tm_user ON tm_user.id=job.job_user INNER JOIN deliveryservice ON deliveryservice.id=job.job_deliveryservice WHERE job.id=$1 ` // Deprecated, only to be used with versions below 4.0 const deleteQuery = ` DELETE FROM job WHERE job.id=$1 RETURNING job.asset_url, ( SELECT tm_user.username FROM tm_user WHERE tm_user.id=job.job_user ) AS created_by, ( SELECT deliveryservice.xml_id FROM deliveryservice WHERE deliveryservice.id=job.job_deliveryservice ) AS deliveryservice, job.id, 'PURGE' as keyword, CONCAT('TTL:', ttl_hr, 'h') AS parameters, job.start_time ` // Almost the same as deleteQuery, but returns appropriate values for API 4.0+ const deleteQueryV4 = ` DELETE FROM job WHERE job.id=$1 RETURNING job.id, job.asset_url, ( SELECT tm_user.username FROM tm_user WHERE tm_user.id=job.job_user ) AS created_by, ( SELECT deliveryservice.xml_id FROM deliveryservice WHERE deliveryservice.id=job.job_deliveryservice ) AS deliveryservice, ttl_hr, job.invalidation_type, job.start_time ` type apiResponse struct { Alerts []tc.Alert `json:"alerts,omitempty"` Response tc.InvalidationJob `json:"response,omitempty"` } type apiResponseV4 struct { Alerts []tc.Alert `json:"alerts,omitempty"` Response tc.InvalidationJobV4 `json:"response,omitempty"` } func selectMaxLastUpdatedQuery(where string) string { return `SELECT max(t) from ( SELECT max(job.last_updated) as t FROM job JOIN tm_user u ON job.job_user = u.id JOIN deliveryservice ds ON job.job_deliveryservice = ds.id ` + where + ` UNION ALL select max(last_updated) as t from last_deleted l where l.table_name='job') as res` } // Deprecated, only to be used with versions below 4.0 const readQuery = ` SELECT job.id, 'PURGE' AS keyword, CONCAT('TTL::', ttl_hr, 'h') AS parameters, asset_url, start_time, u.username as createdBy, ds.xml_id as dsId FROM job JOIN tm_user u ON job.job_user = u.id JOIN deliveryservice ds ON job.job_deliveryservice = ds.id ` // Almost the same as readQuery, but returns appropriate values for API 4.0+ const readQueryV4 = ` SELECT job.id, asset_url, u.username as createdBy, ds.xml_id, ttl_hr, invalidation_type, start_time FROM job JOIN tm_user u ON job.job_user = u.id JOIN deliveryservice ds ON job.job_deliveryservice = ds.id ` // Used by GET requests to `/jobs`, simply returns a filtered list of // content invalidation jobs according to the provided query parameters. func (job *InvalidationJobV4) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) { var maxTime time.Time var runSecond bool queryParamsToSQLCols := map[string]dbhelpers.WhereColumnInfo{ "id": dbhelpers.WhereColumnInfo{Column: "job.id", Checker: api.IsInt}, "assetUrl": dbhelpers.WhereColumnInfo{Column: "asset_url"}, "startTime": dbhelpers.WhereColumnInfo{Column: "start_time"}, "userId": dbhelpers.WhereColumnInfo{Column: "job_user", Checker: api.IsInt}, "createdBy": dbhelpers.WhereColumnInfo{Column: `(SELECT tm_user.username FROM tm_user WHERE tm_user.id=job.job_user)`}, "deliveryService": dbhelpers.WhereColumnInfo{Column: `(SELECT deliveryservice.xml_id FROM deliveryservice WHERE deliveryservice.id=job.job_deliveryservice)`}, "dsId": dbhelpers.WhereColumnInfo{Column: "job.job_deliveryservice", Checker: api.IsInt}, "invalidationType": dbhelpers.WhereColumnInfo{Column: "invalidation_type"}, } where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(job.APIInfo().Params, queryParamsToSQLCols) if len(errs) > 0 { return nil, util.JoinErrs(errs), nil, http.StatusBadRequest, nil } accessibleTenants, err := tenant.GetUserTenantIDListTx(job.APIInfo().Tx.Tx, job.APIInfo().User.TenantID) if err != nil { return nil, nil, fmt.Errorf("getting accessible tenants for user - %v", err), http.StatusInternalServerError, nil } cdn := "" if cdnName, ok := job.APIInfo().Params["cdn"]; ok { queryValues["cdn"] = cdnName cdn = ` AND ds.cdn_id = (SELECT id FROM cdn WHERE name = :cdn) ` } maxDays := "" if _, ok := job.APIInfo().Params["maxRevalDurationDays"]; ok { // jobs started within the last $maxRevalDurationDays days (defaulting to 90 days if the parameter doesn't exist) maxDays = ` AND job.start_time >= NOW() - CAST( (SELECT COALESCE( (SELECT value FROM parameter WHERE name = 'maxRevalDurationDays' AND config_file = 'regex_revalidate.config' LIMIT 1), '90')) || ' days' AS INTERVAL) ` } if len(where) > 0 { where += " AND ds.tenant_id = ANY(:tenants) " + maxDays + cdn } else { where = dbhelpers.BaseWhere + " ds.tenant_id = ANY(:tenants) " + maxDays + cdn } queryValues["tenants"] = pq.Array(accessibleTenants) if useIMS { runSecond, maxTime = ims.TryIfModifiedSinceQuery(job.APIInfo().Tx, h, queryValues, selectMaxLastUpdatedQuery(where)) if !runSecond { log.Debugln("IMS HIT") return []interface{}{}, nil, nil, http.StatusNotModified, &maxTime } log.Debugln("IMS MISS") } else { log.Debugln("Non IMS request") } query := readQueryV4 + where + orderBy + pagination log.Debugln("generated job query: " + query) log.Debugf("executing with values: %++v\n", queryValues) returnable := []interface{}{} rows, err := job.APIInfo().Tx.NamedQuery(query, queryValues) if err != nil { return nil, nil, fmt.Errorf("querying: %v", err), http.StatusInternalServerError, nil } defer rows.Close() for rows.Next() { job := tc.InvalidationJobV4{} if err := rows.Scan(&job.ID, &job.AssetURL, &job.CreatedBy, &job.DeliveryService, &job.TTLHours, &job.InvalidationType, &job.StartTime); err != nil { return nil, nil, fmt.Errorf("parsing db response: %v", err), http.StatusInternalServerError, nil } returnable = append(returnable, job) } if err := rows.Err(); err != nil { return nil, nil, fmt.Errorf("Parsing db responses: %v", err), http.StatusInternalServerError, nil } return returnable, nil, nil, http.StatusOK, &maxTime } // Used by GET requests to `/jobs`, simply returns a filtered list of // content invalidation jobs according to the provided query parameters. // // Deprecated. To be used only with versions less than 4.0 func (job *InvalidationJob) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) { var maxTime time.Time var runSecond bool queryParamsToSQLCols := map[string]dbhelpers.WhereColumnInfo{ "id": dbhelpers.WhereColumnInfo{Column: "job.id", Checker: api.IsInt}, "keyword": dbhelpers.WhereColumnInfo{Column: "keyword"}, "assetUrl": dbhelpers.WhereColumnInfo{Column: "asset_url"}, "startTime": dbhelpers.WhereColumnInfo{Column: "start_time"}, "userId": dbhelpers.WhereColumnInfo{Column: "job_user", Checker: api.IsInt}, "createdBy": dbhelpers.WhereColumnInfo{Column: `(SELECT tm_user.username FROM tm_user WHERE tm_user.id=job.job_user)`}, "deliveryService": dbhelpers.WhereColumnInfo{Column: `(SELECT deliveryservice.xml_id FROM deliveryservice WHERE deliveryservice.id=job.job_deliveryservice)`}, "dsId": dbhelpers.WhereColumnInfo{Column: "job.job_deliveryservice", Checker: api.IsInt}, } where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(job.APIInfo().Params, queryParamsToSQLCols) if len(errs) > 0 { return nil, util.JoinErrs(errs), nil, http.StatusBadRequest, nil } accessibleTenants, err := tenant.GetUserTenantIDListTx(job.APIInfo().Tx.Tx, job.APIInfo().User.TenantID) if err != nil { return nil, nil, fmt.Errorf("getting accessible tenants for user - %v", err), http.StatusInternalServerError, nil } cdn := "" if cdnName, ok := job.APIInfo().Params["cdn"]; ok { queryValues["cdn"] = cdnName cdn = ` AND ds.cdn_id = (SELECT id FROM cdn WHERE name = :cdn) ` } maxDays := "" if _, ok := job.APIInfo().Params["maxRevalDurationDays"]; ok { // jobs started within the last $maxRevalDurationDays days (defaulting to 90 days if the parameter doesn't exist) maxDays = ` AND job.start_time >= NOW() - CAST( (SELECT COALESCE( (SELECT value FROM parameter WHERE name = 'maxRevalDurationDays' AND config_file = 'regex_revalidate.config' LIMIT 1), '90')) || ' days' AS INTERVAL) ` } if len(where) > 0 { where += " AND ds.tenant_id = ANY(:tenants) " + maxDays + cdn } else { where = dbhelpers.BaseWhere + " ds.tenant_id = ANY(:tenants) " + maxDays + cdn } queryValues["tenants"] = pq.Array(accessibleTenants) if useIMS { runSecond, maxTime = ims.TryIfModifiedSinceQuery(job.APIInfo().Tx, h, queryValues, selectMaxLastUpdatedQuery(where)) if !runSecond { log.Debugln("IMS HIT") return []interface{}{}, nil, nil, http.StatusNotModified, &maxTime } log.Debugln("IMS MISS") } else { log.Debugln("Non IMS request") } query := readQuery + where + orderBy + pagination log.Debugln("generated job query: " + query) log.Debugf("executing with values: %++v\n", queryValues) returnable := []interface{}{} rows, err := job.APIInfo().Tx.NamedQuery(query, queryValues) if err != nil { return nil, nil, fmt.Errorf("querying: %v", err), http.StatusInternalServerError, nil } defer rows.Close() for rows.Next() { j := tc.InvalidationJob{} err := rows.Scan(&j.ID, &j.Keyword, &j.Parameters, &j.AssetURL, &j.StartTime, &j.CreatedBy, &j.DeliveryService) if err != nil { return nil, nil, fmt.Errorf("parsing db response: %v", err), http.StatusInternalServerError, nil } returnable = append(returnable, j) } if err := rows.Err(); err != nil { return nil, nil, fmt.Errorf("Parsing db responses: %v", err), http.StatusInternalServerError, nil } return returnable, nil, nil, http.StatusOK, &maxTime } // Used by POST requests to `/jobs`, creates a new content invalidation job // from the provided request body. func CreateV40(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } defer inf.Close() job := tc.InvalidationJobCreateV4{} if err := json.NewDecoder(r.Body).Decode(&job); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("Unable to parse Invalidation Job"), fmt.Errorf("parsing jobs/ POST: %v", err)) return } // Check if request object is valid w.Header().Set(rfc.ContentType, rfc.ApplicationJSON) if err := validateJobCreateV4(job, inf.Tx.Tx); err != nil { response := tc.Alerts{ Alerts: []tc.Alert{ { Text: err.Error(), Level: tc.ErrorLevel.String(), }, }, } resp, err := json.Marshal(response) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("encoding bad request response: %v", err)) return } w.WriteHeader(http.StatusBadRequest) api.WriteAndLogErr(w, r, append(resp, '\n')) return } // Check if authorized if ok, err := IsUserAuthorizedToModifyDSXMLID(inf, job.DeliveryService); err != nil { sysErr = fmt.Errorf("failed checking current user permissions for DS #%s: %v", job.DeliveryService, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } else if !ok { userErr = fmt.Errorf("failed to authorize based on tenancy") errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } // DS existence was already verified in the Validate() function dsid, exists, err := dbhelpers.GetDSIDFromXMLID(inf.Tx.Tx, job.DeliveryService) if err != nil { sysErr = fmt.Errorf("failed to match XML ID to int ID for Delivery Service %s: %v", job.DeliveryService, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } if !exists { userErr = fmt.Errorf("delivery service \"%v\" does not exist", job.DeliveryService) errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } _, cdnName, _, err := dbhelpers.GetDSNameAndCDNFromID(inf.Tx.Tx, int(dsid)) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting delivery service and CDN name from ID: "+err.Error())) return } userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, string(cdnName), inf.User.UserName) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr) return } row := inf.Tx.Tx.QueryRow(insertQueryV4, job.TTLHours, dsid, // Used in inner select for deliveryservice job.Regex, job.StartTime, time.Now(), inf.User.ID, dsid, job.InvalidationType) // Defaults for all api versions below 4.0 result := tc.InvalidationJobV4{} err = row.Scan( &result.ID, &result.AssetURL, &result.CreatedBy, &result.DeliveryService, &result.TTLHours, &result.InvalidationType, &result.StartTime) if err != nil { userErr, sysErr, errCode = api.ParseDBError(err) api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } if err := setRevalFlags(uint(dsid), inf.Tx.Tx); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("setting reval flags: %v", err)) return } conflicts := tc.ValidateJobUniqueness(inf.Tx.Tx, uint(dsid), result.StartTime, result.AssetURL, result.TTLHours) response := apiResponseV4{ make([]tc.Alert, len(conflicts)+1), result, } for i, conflict := range conflicts { response.Alerts[i] = tc.Alert{ Text: conflict, Level: tc.WarnLevel.String(), } } response.Alerts[len(conflicts)] = tc.Alert{ Text: fmt.Sprintf("Invalidation (%s) request created for %v, start:%v end %v", result.InvalidationType, result.AssetURL, result.StartTime, result.StartTime.Add(time.Hour*time.Duration(job.TTLHours))), Level: tc.SuccessLevel.String(), } resp, err := json.Marshal(response) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("Marshaling JSON: %v", err)) return } if inf.Version == nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("nil API version")) return } w.Header().Set(http.CanonicalHeaderKey("location"), fmt.Sprintf("%s://%s/api/%d.%d/jobs?id=%d", inf.Config.URL.Scheme, r.Host, inf.Version.Major, inf.Version.Minor, result.ID)) w.WriteHeader(http.StatusOK) api.WriteAndLogErr(w, r, append(resp, '\n')) duplicate := "" if len(conflicts) > 0 { duplicate = "(duplicate) " } changeLogMsg := fmt.Sprintf("%s content invalidation job %s- ID: %d DSXMLID: %s ASSET_URL: '%s' TTLHRs: %d INVALIDATION: %s", api.Created, duplicate, result.ID, result.DeliveryService, result.AssetURL, result.TTLHours, result.InvalidationType, ) api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, inf.Tx.Tx) } // Used by POST requests to `/jobs`, creates a new content invalidation job // from the provided request body. // // Deprecated. To be used only with versions less than 4.0 func Create(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } defer inf.Close() job := tc.InvalidationJobInput{} if err := json.NewDecoder(r.Body).Decode(&job); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("Unable to parse Invalidation Job"), fmt.Errorf("parsing jobs/ POST: %v", err)) return } w.Header().Set(rfc.ContentType, rfc.ApplicationJSON) if err := job.Validate(inf.Tx.Tx); err != nil { response := tc.Alerts{ Alerts: []tc.Alert{ tc.Alert{ Text: err.Error(), Level: tc.ErrorLevel.String(), }, }, } resp, err := json.Marshal(response) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("Encoding bad request response: %v", err)) return } w.WriteHeader(http.StatusBadRequest) api.WriteAndLogErr(w, r, append(resp, '\n')) return } // Validate() would have already checked for deliveryservice existence and // parsed the ttl, so if either of these throws an error now, something // weird has happened dsid, err := job.DSID(nil) if err != nil { sysErr = fmt.Errorf("retrieving parsed DSID: %v", err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } var ttl uint if ttl, err = job.TTLHours(); err != nil { sysErr = fmt.Errorf("retrieving parsed TTL: %v", err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } if ok, err := IsUserAuthorizedToModifyDSID(inf, dsid); err != nil { sysErr = fmt.Errorf("Checking current user permissions for DS #%d: %v", dsid, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } else if !ok { userErr = fmt.Errorf("No such Delivery Service!") errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } _, cdnName, _, err := dbhelpers.GetDSNameAndCDNFromID(inf.Tx.Tx, int(dsid)) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting delivery service and CDN name from ID: "+err.Error())) return } userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, string(cdnName), inf.User.UserName) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr) return } row := inf.Tx.Tx.QueryRow(insertQuery, ttl, dsid, // Used in inner select for deliveryservice *job.Regex, (*job.StartTime).Time, time.Now(), inf.User.ID, dsid, tc.REFRESH) // Defaults for all api versions below 4.0 result := tc.InvalidationJob{} err = row.Scan(&result.AssetURL, &result.DeliveryService, &result.ID, &result.CreatedBy, &result.Keyword, &result.Parameters, &result.StartTime) if err != nil { userErr, sysErr, errCode = api.ParseDBError(err) api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } if err := setRevalFlags(dsid, inf.Tx.Tx); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("setting reval flags: %v", err)) return } conflicts := tc.ValidateJobUniqueness(inf.Tx.Tx, dsid, job.StartTime.Time, *result.AssetURL, ttl) response := apiResponse{ make([]tc.Alert, len(conflicts)+1), result, } for i, conflict := range conflicts { response.Alerts[i] = tc.Alert{ Text: conflict, Level: tc.WarnLevel.String(), } } response.Alerts[len(conflicts)] = tc.Alert{ Text: fmt.Sprintf("Invalidation request created for %v, start:%v end %v", *result.AssetURL, job.StartTime.Time, job.StartTime.Add(time.Hour*time.Duration(ttl))), Level: tc.SuccessLevel.String(), } resp, err := json.Marshal(response) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("Marshaling JSON: %v", err)) return } if inf.Version == nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("nil API version")) return } w.Header().Set(http.CanonicalHeaderKey("location"), fmt.Sprintf("%s://%s/api/%s/jobs?id=%d", inf.Config.URL.Scheme, r.Host, inf.Version, *result.ID)) w.WriteHeader(http.StatusOK) api.WriteAndLogErr(w, r, append(resp, '\n')) duplicate := "" if len(conflicts) > 0 { duplicate = "(duplicate) " } api.CreateChangeLogRawTx(api.ApiChange, api.Created+" content invalidation job "+duplicate+"- ID: "+ strconv.FormatUint(*result.ID, 10)+" DS: "+*result.DeliveryService+" URL: '"+*result.AssetURL+ "' Params: '"+*result.Parameters+"'", inf.User, inf.Tx.Tx) } // Used by PUT requests to `/jobs`, replaces an existing content invalidation job // with the provided request body. func UpdateV40(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"}) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } defer inf.Close() var oFQDN string var dsid uint var uid uint job := tc.InvalidationJobV4{} row := inf.Tx.Tx.QueryRow(putInfoQueryV4, inf.Params["id"]) err := row.Scan(&job.ID, &job.CreatedBy, &uid, &dsid, &job.DeliveryService, &job.AssetURL, &job.TTLHours, &job.StartTime, &job.InvalidationType, &oFQDN) if err != nil { if err == sql.ErrNoRows { userErr = fmt.Errorf("No job by id '%s'!", inf.Params["id"]) errCode = http.StatusNotFound } else { sysErr = fmt.Errorf("fetching job update info: %v", err) errCode = http.StatusInternalServerError } api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } if ok, err := IsUserAuthorizedToModifyDSID(inf, dsid); err != nil { sysErr = fmt.Errorf("Checking user permissions on DS #%d: %v", dsid, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } else if !ok { userErr = errors.New("No such Delivery Service!") errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if ok, err := IsUserAuthorizedToModifyJobsMadeByUsername(inf, job.CreatedBy); err != nil { sysErr = fmt.Errorf("Checking user permissions against user %s: %v", job.CreatedBy, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } else if !ok { userErr = fmt.Errorf("No job by id '%s'!", inf.Params["id"]) errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } input := tc.InvalidationJobV4{} if err := json.NewDecoder(r.Body).Decode(&input); err != nil { userErr = fmt.Errorf("Unable to parse input: %v", err) sysErr = fmt.Errorf("parsing input to PUT jobs?id=%s: %v", inf.Params["id"], err) errCode = http.StatusBadRequest api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } if err := validateInvalidationJobV4(input); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil) return } if !strings.HasPrefix(input.AssetURL, oFQDN) { userErr = fmt.Errorf("Cannot set asset URL that does not start with Delivery Service origin URL: %s", oFQDN) errCode = http.StatusBadRequest api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if job.StartTime.Before(time.Now()) { userErr = errors.New("Cannot modify a job that has already started!") errCode = http.StatusMethodNotAllowed w.Header().Set(http.CanonicalHeaderKey("allow"), "GET,HEAD,DELETE") api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if job.DeliveryService != input.DeliveryService { userErr = errors.New("Cannot change 'deliveryService' of existing invalidation job!") errCode = http.StatusConflict api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if job.CreatedBy != input.CreatedBy { userErr = errors.New("Cannot change 'createdBy' of existing invalidation jobs!") errCode = http.StatusConflict api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if job.ID != input.ID { userErr = errors.New("Cannot change an invalidation job 'id'!") errCode = http.StatusConflict api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if job.InvalidationType != input.InvalidationType { if input.InvalidationType == tc.REFETCH && !refetchAllowed(inf.Tx.Tx) { userErr = errors.New("Invalidation Type REFETCH is disallowed") errCode = http.StatusBadRequest api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } } row = inf.Tx.Tx.QueryRow(updateQueryV4, input.AssetURL, input.TTLHours, input.StartTime, input.InvalidationType, job.ID) err = row.Scan(&job.AssetURL, &job.CreatedBy, &job.DeliveryService, &job.ID, &job.TTLHours, &job.StartTime, &job.InvalidationType) if err != nil { sysErr = fmt.Errorf("Updating a job: %v", err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } if err = setRevalFlags(job.DeliveryService, inf.Tx.Tx); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("Setting reval flags: %v", err)) return } conflicts := tc.ValidateJobUniqueness(inf.Tx.Tx, dsid, input.StartTime, input.AssetURL, input.TTLHours) response := apiResponseV4{ make([]tc.Alert, len(conflicts)+1), job, } for i, conflict := range conflicts { response.Alerts[i] = tc.Alert{ Text: conflict, Level: tc.WarnLevel.String(), } } response.Alerts[len(conflicts)] = tc.Alert{ Text: fmt.Sprintf("Invalidation request created for %s, start: %v end: %v invalidation type: %v", job.AssetURL, job.StartTime, job.StartTime.Add(time.Hour*time.Duration(job.TTLHours)), job.InvalidationType), Level: tc.SuccessLevel.String(), } resp, err := json.Marshal(response) if err != nil { sysErr = fmt.Errorf("encoding response: %v", err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } w.Header().Set(http.CanonicalHeaderKey("content-type"), rfc.ApplicationJSON) api.WriteAndLogErr(w, r, append(resp, '\n')) changeLogMsg := fmt.Sprintf("%s content invalidation job - ID: %d DSXMLID: %s ASSET_URL: '%s' TTLHRs: %d INVALIDATION: %s", api.Updated, input.ID, input.DeliveryService, input.AssetURL, input.TTLHours, input.InvalidationType, ) api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, inf.Tx.Tx) } // Used by PUT requests to `/jobs`, replaces an existing content invalidation job // with the provided request body. // // Deprecated. To be used only with versions less than 4.0 func Update(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } defer inf.Close() var oFQDN string var dsid uint var uid uint job := tc.InvalidationJob{} row := inf.Tx.Tx.QueryRow(putInfoQuery, inf.Params["id"]) err := row.Scan(&job.ID, &job.CreatedBy, &uid, &dsid, &job.DeliveryService, &job.AssetURL, &job.Parameters, &job.StartTime, &oFQDN) if err != nil { if err == sql.ErrNoRows { userErr = fmt.Errorf("No job by id '%s'!", inf.Params["id"]) errCode = http.StatusNotFound } else { sysErr = fmt.Errorf("fetching job update info: %v", err) errCode = http.StatusInternalServerError } api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } if ok, err := IsUserAuthorizedToModifyDSID(inf, dsid); err != nil { sysErr = fmt.Errorf("Checking user permissions on DS #%d: %v", dsid, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } else if !ok { userErr = errors.New("No such Delivery Service!") errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if ok, err := IsUserAuthorizedToModifyJobsMadeByUsername(inf, *job.CreatedBy); err != nil { sysErr = fmt.Errorf("Checking user permissions against user %s: %v", *job.CreatedBy, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } else if !ok { userErr = fmt.Errorf("No job by id '%s'!", inf.Params["id"]) errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } input := tc.InvalidationJob{} if err := json.NewDecoder(r.Body).Decode(&input); err != nil { userErr = fmt.Errorf("Unable to parse input: %v", err) sysErr = fmt.Errorf("parsing input to PUT jobs?id=%s: %v", inf.Params["id"], err) errCode = http.StatusBadRequest api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } if err := input.Validate(); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, err, nil) return } if !strings.HasPrefix(*input.AssetURL, oFQDN) { userErr = fmt.Errorf("Cannot set asset URL that does not start with Delivery Service origin URL: %s", oFQDN) errCode = http.StatusBadRequest api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if job.StartTime.Before(time.Now()) { userErr = errors.New("Cannot modify a job that has already started!") errCode = http.StatusMethodNotAllowed w.Header().Set(http.CanonicalHeaderKey("allow"), "GET,HEAD,DELETE") api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if *job.DeliveryService != *input.DeliveryService { userErr = errors.New("Cannot change 'deliveryService' of existing invalidation job!") errCode = http.StatusConflict api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if *job.CreatedBy != *input.CreatedBy { userErr = errors.New("Cannot change 'createdBy' of existing invalidation jobs!") errCode = http.StatusConflict api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if *job.ID != *input.ID { userErr = errors.New("Cannot change an invalidation job 'id'!") errCode = http.StatusConflict api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } _, cdnName, _, err := dbhelpers.GetDSNameAndCDNFromID(inf.Tx.Tx, int(dsid)) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting delivery service and CDN name from ID: "+err.Error())) return } userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, string(cdnName), inf.User.UserName) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr) return } row = inf.Tx.Tx.QueryRow(updateQuery, input.AssetURL, strings.TrimSuffix(strings.TrimPrefix(*input.Parameters, "TTL:"), "h"), // Strip TTL: and h from 'TTL:##h' input.StartTime.Time, *job.ID) err = row.Scan(&job.AssetURL, &job.CreatedBy, &job.DeliveryService, &job.ID, &job.Keyword, &job.Parameters, &job.StartTime) if err != nil { sysErr = fmt.Errorf("Updating a job: %v", err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } if err = setRevalFlags(*job.DeliveryService, inf.Tx.Tx); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("Setting reval flags: %v", err)) return } ttlHours := input.TTLHours() conflicts := tc.ValidateJobUniqueness(inf.Tx.Tx, dsid, input.StartTime.Time, *input.AssetURL, ttlHours) response := apiResponse{ make([]tc.Alert, len(conflicts)+1), job, } for i, conflict := range conflicts { response.Alerts[i] = tc.Alert{ Text: conflict, Level: tc.WarnLevel.String(), } } response.Alerts[len(conflicts)] = tc.Alert{ Text: fmt.Sprintf("Invalidation request created for %v, start:%v end %v", *job.AssetURL, job.StartTime.Time, job.StartTime.Add(time.Hour*time.Duration(ttlHours))), Level: tc.SuccessLevel.String(), } resp, err := json.Marshal(response) if err != nil { sysErr = fmt.Errorf("encoding response: %v", err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } w.Header().Set(http.CanonicalHeaderKey("content-type"), rfc.ApplicationJSON) api.WriteAndLogErr(w, r, append(resp, '\n')) api.CreateChangeLogRawTx(api.ApiChange, api.Updated+" content invalidation job - ID: "+strconv.FormatUint(*job.ID, 10)+" DS: "+*job.DeliveryService+" URL: '"+*job.AssetURL+"' Params: '"+*job.Parameters+"'", inf.User, inf.Tx.Tx) } // Used by DELETE requests to `/jobs`, deletes an existing content invalidation job func DeleteV40(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"}) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } defer inf.Close() var dsid uint var createdBy uint row := inf.Tx.Tx.QueryRow(`SELECT job_deliveryservice, job_user FROM job WHERE id=$1`, inf.Params["id"]) if err := row.Scan(&dsid, &createdBy); err != nil { if err == sql.ErrNoRows { userErr = fmt.Errorf("No job by id '%s'!", inf.Params["id"]) errCode = http.StatusNotFound } else { sysErr = fmt.Errorf("Getting info for job #%s: %v", inf.Params["id"], err) errCode = http.StatusInternalServerError } api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } if ok, err := IsUserAuthorizedToModifyDSID(inf, dsid); err != nil { sysErr = fmt.Errorf("Checking user permissions on DS #%d: %v", dsid, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } else if !ok { userErr = errors.New("No such Delivery Service!") errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if ok, err := IsUserAuthorizedToModifyJobsMadeByUserID(inf, createdBy); err != nil { sysErr = fmt.Errorf("Checking user permissions against user %v: %v", createdBy, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } else if !ok { userErr = fmt.Errorf("No job by id '%s'!", inf.Params["id"]) errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } result := tc.InvalidationJobV4{} row = inf.Tx.Tx.QueryRow(deleteQueryV4, inf.Params["id"]) err := row.Scan( &result.ID, &result.AssetURL, &result.CreatedBy, &result.DeliveryService, &result.TTLHours, &result.InvalidationType, &result.StartTime) if err != nil { sysErr = fmt.Errorf("deleting job #%s: %v", inf.Params["id"], err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } if err = setRevalFlags(dsid, inf.Tx.Tx); err != nil { sysErr = fmt.Errorf("setting reval_pending after deleting job #%s: %v", inf.Params["id"], err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } response := apiResponseV4{[]tc.Alert{ {Text: "Content invalidation job was deleted", Level: tc.SuccessLevel.String()}, }, result, } resp, err := json.Marshal(response) if err != nil { sysErr = fmt.Errorf("encoding response: %v", err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } w.Header().Set(http.CanonicalHeaderKey("content-type"), rfc.ApplicationJSON) api.WriteAndLogErr(w, r, append(resp, '\n')) changeLogMsg := fmt.Sprintf("%s content invalidation job - ID: %d DSXMLID: %s ASSET_URL: '%s' TTLHRs: %d INVALIDATION: %s", api.Deleted, result.ID, result.DeliveryService, result.AssetURL, result.TTLHours, result.InvalidationType, ) api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, inf.Tx.Tx) } // Used by DELETE requests to `/jobs`, deletes an existing content invalidation job // // Deprecated. To be used only with versions less than 4.0 func Delete(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"}) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } defer inf.Close() var dsid uint var createdBy uint row := inf.Tx.Tx.QueryRow(`SELECT job_deliveryservice, job_user FROM job WHERE id=$1`, inf.Params["id"]) if err := row.Scan(&dsid, &createdBy); err != nil { if err == sql.ErrNoRows { userErr = fmt.Errorf("No job by id '%s'!", inf.Params["id"]) errCode = http.StatusNotFound } else { sysErr = fmt.Errorf("Getting info for job #%s: %v", inf.Params["id"], err) errCode = http.StatusInternalServerError } api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } if ok, err := IsUserAuthorizedToModifyDSID(inf, dsid); err != nil { sysErr = fmt.Errorf("Checking user permissions on DS #%d: %v", dsid, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } else if !ok { userErr = errors.New("No such Delivery Service!") errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } if ok, err := IsUserAuthorizedToModifyJobsMadeByUserID(inf, createdBy); err != nil { sysErr = fmt.Errorf("Checking user permissions against user %v: %v", createdBy, err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } else if !ok { userErr = fmt.Errorf("No job by id '%s'!", inf.Params["id"]) errCode = http.StatusNotFound api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, nil) return } _, cdnName, _, err := dbhelpers.GetDSNameAndCDNFromID(inf.Tx.Tx, int(dsid)) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting delivery service and CDN name from ID: "+err.Error())) return } userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, string(cdnName), inf.User.UserName) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr) return } result := tc.InvalidationJob{} row = inf.Tx.Tx.QueryRow(deleteQuery, inf.Params["id"]) err = row.Scan(&result.AssetURL, &result.CreatedBy, &result.DeliveryService, &result.ID, &result.Keyword, &result.Parameters, &result.StartTime) if err != nil { sysErr = fmt.Errorf("deleting job #%s: %v", inf.Params["id"], err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } if err = setRevalFlags(dsid, inf.Tx.Tx); err != nil { sysErr = fmt.Errorf("setting reval_pending after deleting job #%s: %v", inf.Params["id"], err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } response := apiResponse{[]tc.Alert{tc.Alert{Text: "Content invalidation job was deleted", Level: tc.SuccessLevel.String()}}, result} resp, err := json.Marshal(response) if err != nil { sysErr = fmt.Errorf("encoding response: %v", err) errCode = http.StatusInternalServerError api.HandleErr(w, r, inf.Tx.Tx, errCode, nil, sysErr) return } w.Header().Set(http.CanonicalHeaderKey("content-type"), rfc.ApplicationJSON) api.WriteAndLogErr(w, r, append(resp, '\n')) api.CreateChangeLogRawTx(api.ApiChange, api.Deleted+" content invalidation job - ID: "+strconv.FormatUint(*result.ID, 10)+" DS: "+*result.DeliveryService+" URL: '"+*result.AssetURL+"' Params: '"+*result.Parameters+"'", inf.User, inf.Tx.Tx) } // Validates the fields submitted for an InvalidationJobCreateV40. These errors // are ultimately returned to the user func validateJobCreateV4(job tc.InvalidationJobCreateV4, tx *sql.Tx) error { errs := []string{} err := validation.ValidateStruct(&job, validation.Field(&job.DeliveryService, validation.Required), validation.Field(&job.Regex, validation.Required, validation.NewStringRule(func(s string) bool { return strings.HasPrefix(s, `\/`) || strings.HasPrefix(s, "/") }, `must start with '/' (or '\/')`)), validation.Field(&job.StartTime, validation.Required), validation.Field(&job.TTLHours, validation.Required), validation.Field(&job.InvalidationType, validation.Required, validation.NewStringRule(func(s string) bool { return s == tc.REFRESH || s == tc.REFETCH }, fmt.Sprintf("must be either %s or %s (case sensitive)", tc.REFRESH, tc.REFETCH))), ) if err != nil { errs = append(errs, err.Error()) } if _, _, err := dbhelpers.GetDSIDFromXMLID(tx, job.DeliveryService); err != nil { errs = append(errs, "Delivery Service is invalid: "+err.Error()) } if _, err := regexp.Compile(job.Regex); err != nil { errs = append(errs, "regex: is not a valid Regular Expression: "+err.Error()) } if job.StartTime.Before(time.Now()) { errs = append(errs, "startTime: must be in the future") } if valid, err := validateTLLHours(job.TTLHours, tx); !valid { if err != nil { errs = append(errs, "TTL is invalid: "+err.Error()) } else { errs = append(errs, "TTL is invalid") } } if job.InvalidationType == tc.REFETCH && !refetchAllowed(tx) { errs = append(errs, "invalidationType is not allowed since 'refetch_enabled' parameter doesn't exists or the value is not set to a case-insensitive 'true'") } if len(errs) > 0 { return errors.New(strings.Join(errs, ", ")) } return nil } // validateInvalidationJobV4 checks that the InvalidationJob is valid, by ensuring all of its fields are well-defined. // This returns an error describing any and all problematic fields encountered during validation. func validateInvalidationJobV4(job tc.InvalidationJobV4) error { errs := []string{} err := validation.ValidateStruct(&job, validation.Field(&job.DeliveryService, validation.Required), validation.Field(&job.AssetURL, validation.Required, is.URL), validation.Field(&job.CreatedBy, validation.Required), validation.Field(&job.ID, validation.Required), validation.Field(&job.TTLHours, validation.Required), validation.Field(&job.StartTime, validation.Required), validation.Field(&job.InvalidationType, validation.Required, validation.NewStringRule(func(s string) bool { return s == tc.REFRESH || s == tc.REFETCH }, fmt.Sprintf("must be either %s or %s (case sensitive)", tc.REFRESH, tc.REFETCH))), ) if err != nil { errs = append(errs, err.Error()) } if job.StartTime.After(time.Now().Add(time.Hour * 48)) { errs = append(errs, "startTime: must be within two days from now") } if job.StartTime.Before(time.Now()) { errs = append(errs, "startTime: cannot be in the past") } if len(errs) > 0 { return errors.New(strings.Join(errs, ", ")) } return nil } // validateTLLHours ensures the supplied TTL hours is within acceptable limits func validateTLLHours(ttlHours uint32, tx *sql.Tx) (bool, error) { var maxDays uint err := tx.QueryRow(`SELECT value FROM parameter WHERE name='maxRevalDurationDays' AND config_file='regex_revalidate.config'`).Scan(&maxDays) maxHours := maxDays * 24 if err != nil { log.Errorf("error querying \"maxRevalDurationDays\" parameter: %v", err) return false, nil // sent to the user, hide server error } if err == nil && uint(ttlHours) > maxHours { return false, fmt.Errorf("cannot exceed %s", strconv.FormatUint(uint64(maxHours), 10)) } return true, nil } // refetchAllowed checks whether Refetch is allowed and enabled in the parameter table func refetchAllowed(tx *sql.Tx) bool { refetchEnabled := false err := tx.QueryRow(`SELECT 'true' = lower(trim(p.value)) FROM "parameter" p WHERE p.name=$1 AND p.config_file=$2`, tc.RefetchEnabled, tc.GlobalConfigFileName).Scan(&refetchEnabled) if err != nil { log.Errorf("error querying \"refetch_enabled\" from parameter: %v", err) return refetchEnabled // sent to the user, hide server error } return refetchEnabled } // API versions below 4.0 allowed for either the Delivery Service ID (uint) OR Delivery Service XML-ID (string). // This can be refactored once api versions below 4.0 are removed to take a Delivery Service XML-ID (string), rather // than an empty interface {}. func setRevalFlags(d interface{}, tx *sql.Tx) error { var useReval string row := tx.QueryRow(`SELECT value FROM parameter WHERE name=$1 AND config_file=$2`, tc.UseRevalPendingParameterName, tc.GlobalConfigFileName) if err := row.Scan(&useReval); err != nil { if err != sql.ErrNoRows { return err } useReval = "0" } column := "revalidate_update_time" if useReval == "0" { column = "config_update_time" } var q string switch t := d.(type) { case uint: q = fmt.Sprintf(queueUpdateOrRevalQuery, column, "id") case string: q = fmt.Sprintf(queueUpdateOrRevalQuery, column, "xml_id") default: return fmt.Errorf("invalid type passed to 'setRevalFlags': %v", t) } row = tx.QueryRow(q, d) if err := row.Scan(); err != nil && err != sql.ErrNoRows { return err } return nil } // Checks if the current user's (identified in the api.Info) tenant has permissions to // edit a Delivery Service. `ds` is expected to be the integral, unique identifer of the // Delivery Service in question. // // This returns, in order, a boolean that indicates whether or not the current user // has the required tenancy to modify the indicated Delivery Service, and an error // indicating what, if anything, went wrong during the check. // returned errors is not nil, otherwise its value is undefined. // // Note: If no such delivery service exists, the return values shall indicate that the // user isn't authorized. func IsUserAuthorizedToModifyDSID(inf *api.Info, ds uint) (bool, error) { var t uint row := inf.Tx.Tx.QueryRow(`SELECT tenant_id FROM deliveryservice WHERE id=$1`, ds) if err := row.Scan(&t); err != nil { if err == sql.ErrNoRows { return false, nil //I do this to conceal the existence of DSes for which the user has no permission to see } return false, err } return tenant.IsResourceAuthorizedToUserTx(int(t), inf.User, inf.Tx.Tx) } // Checks if the current user's (identified in the api.Info) tenant has permissions to // edit a Delivery Service. `ds` is expected to be the "xml_id" of the // Delivery Service in question. // // This returns, in order, a boolean that indicates whether or not the current user // has the required tenancy to modify the indicated Delivery Service, and an error // indicating what, if anything, went wrong during the check. // returned errors is not nil, otherwise its value is undefined. // // Note: If no such delivery service exists, the return values shall indicate that the // user isn't authorized. func IsUserAuthorizedToModifyDSXMLID(inf *api.Info, ds string) (bool, error) { var t uint row := inf.Tx.Tx.QueryRow(`SELECT tenant_id FROM deliveryservice WHERE xml_id=$1`, ds) if err := row.Scan(&t); err != nil { if err == sql.ErrNoRows { return false, nil //I do this to conceal the existence of DSes for which the user has no permission to see } return false, err } return tenant.IsResourceAuthorizedToUserTx(int(t), inf.User, inf.Tx.Tx) } // Checks if the current user's (identified in the api.Info) tenant has permissions to // edit on par with the user identified by `u`. `u` is expected to be the integral, // unique identifer of the user in question (not the current, requesting user). // // This returns, in order, a boolean that indicates whether or not the current user // has the required tenancy to modify the indicated Delivery Service, and an error // indicating what, if anything, went wrong during the check. // returned errors is not nil, otherwise its value is undefined. // // Note: If no such delivery service exists, the return values shall indicate that the // user isn't authorized. func IsUserAuthorizedToModifyJobsMadeByUserID(inf *api.Info, u uint) (bool, error) { var t uint row := inf.Tx.Tx.QueryRow(`SELECT tenant_id FROM tm_user WHERE id=$1`, u) if err := row.Scan(&t); err != nil { if err == sql.ErrNoRows { return false, nil //I do this to conceal the existence of DSes for which the user has no permission to see } return false, err } return tenant.IsResourceAuthorizedToUserTx(int(t), inf.User, inf.Tx.Tx) } // Checks if the current user's (identified in the api.Info) tenant has permissions to // edit on par with the user identified by `u`. `u` is expected to be the username of // the user in question (not the current, requesting user). // // This returns, in order, a boolean that indicates whether or not the current user // has the required tenancy to modify the indicated Delivery Service, and an error // indicating what, if anything, went wrong during the check. // returned errors is not nil, otherwise its value is undefined. // // Note: If no such delivery service exists, the return values shall indicate that the // user isn't authorized. func IsUserAuthorizedToModifyJobsMadeByUsername(inf *api.Info, u string) (bool, error) { var t uint row := inf.Tx.Tx.QueryRow(`SELECT tenant_id FROM tm_user WHERE username=$1`, u) if err := row.Scan(&t); err != nil { if err == sql.ErrNoRows { return false, nil //I do this to conceal the existence of DSes for which the user has no permission to see } return false, err } return tenant.IsResourceAuthorizedToUserTx(int(t), inf.User, inf.Tx.Tx) }