traffic_ops/traffic_ops_golang/deliveryservice/request/requests.go (996 lines of code) (raw):

// Package request contains logic and handlers for API routes dealing with // Delivery Service Requests (DSRs). package request /* * 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" "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/deliveryservice" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/routing/middleware" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/tenant" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/util/ims" "github.com/jmoiron/sqlx" "github.com/lib/pq" ) const selectQuery = ` SELECT a.username AS author, e.username AS lastEditedBy, s.username AS assignee, r.assignee_id, r.author_id, r.change_type, r.created_at, r.id, r.last_edited_by_id, r.last_updated, r.deliveryservice, r.original, r.status FROM deliveryservice_request r JOIN tm_user a ON r.author_id = a.id LEFT OUTER JOIN tm_user s ON r.assignee_id = s.id LEFT OUTER JOIN tm_user e ON r.last_edited_by_id = e.id ` const insertQuery = ` INSERT INTO deliveryservice_request ( assignee_id, author_id, change_type, last_edited_by_id, deliveryservice, original, status ) VALUES ( $1, $2, $3, $2, NULLIF($4, 'null'::jsonb), NULLIF($5, 'null'::jsonb), $6 ) RETURNING id, last_updated, created_at ` const updateQuery = ` UPDATE deliveryservice_request SET assignee_id = $1, change_type = $2, last_edited_by_id = $3, deliveryservice = NULLIF($4, 'null'::jsonb), original = NULLIF($5, 'null'::jsonb), status = $6 WHERE id = $7 RETURNING last_updated, created_at ` const deleteQuery = ` DELETE FROM deliveryservice_request WHERE id=$1 ` // TODO: figure out how to modify 'AddTenancyCheck' so this isn't necessary. const customTenancyCheck = `( CASE r.change_type WHEN 'delete' THEN CAST(r.original->>'tenantId' AS BIGINT) = ANY(CAST(:accessibleTenants AS BIGINT[])) ELSE CAST(r.deliveryservice->>'tenantId' AS BIGINT) = ANY(CAST(:accessibleTenants AS BIGINT[])) END )` const originalsQuery = deliveryservice.SelectDeliveryServicesQuery + ` WHERE ds.id = ANY(:ids) ` func selectMaxLastUpdatedQuery(where string) string { return `SELECT max(t) FROM ( SELECT max(r.last_updated) as t FROM deliveryservice_request r JOIN tm_user a ON r.author_id = a.id LEFT OUTER JOIN tm_user s ON r.assignee_id = s.id LEFT OUTER JOIN tm_user e ON r.last_edited_by_id = e.id ` + where + ` UNION ALL SELECT MAX(last_updated) AS t FROM last_deleted l WHERE l.table_name='deliveryservice_request') AS res` } // getOriginals fetches the Delivery Services identified in 'ids' and sets // them as originals on the Delivery Services to which each ID maps in // needOriginals. It returns a response code to use if an error occurred, in // which case it also returns a user error and a system error. func getOriginals(ids []int, tx *sqlx.Tx, needOriginals map[int][]*tc.DeliveryServiceRequestV5) (int, error, error) { if len(ids) > 0 { originals, userErr, sysErr, errCode := deliveryservice.GetDeliveryServices(originalsQuery, map[string]interface{}{"ids": pq.Array(ids)}, tx) if userErr != nil || sysErr != nil { return errCode, userErr, sysErr } for _, ds := range originals { if original := ds.DS; original.ID == nil { log.Warnf("Trying to fill in originals: found Delivery Service with no ID") } else if need, ok := needOriginals[*original.ID]; ok { for _, n := range need { n.Original = new(tc.DeliveryServiceV5) *n.Original = original } } else { log.Warnf("Trying to fill in originals: found Delivery Service that wasn't identified by a DSR (#%d)", *original.ID) } } } return http.StatusOK, nil, nil } // Get is the GET handler for /deliveryservice_requests. func Get(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) tx := inf.Tx.Tx if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } defer inf.Close() // Middleware should've already handled this, so idk why this is a pointer at all tbh version := inf.Version if version == nil { middleware.NotImplementedHandler().ServeHTTP(w, r) return } queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{ "assignee": {Column: "s.username"}, "assigneeId": {Column: "r.assignee_id", Checker: api.IsInt}, "author": {Column: "a.username"}, "authorId": {Column: "r.author_id", Checker: api.IsInt}, "changeType": {Column: "r.change_type"}, "createdAt": {Column: "r.created_at"}, "id": {Column: "r.id", Checker: api.IsInt}, "status": {Column: "r.status"}, } if _, ok := inf.Params["orderby"]; !ok { inf.Params["orderby"] = "xmlId" } where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols) if len(errs) > 0 { api.HandleErr(w, r, tx, http.StatusBadRequest, util.JoinErrs(errs), nil) return } // TODO: add this functionality to the query builder in dbhelpers if xmlID, ok := inf.Params["xmlId"]; ok { where = dbhelpers.AppendWhere(where, "((r.deliveryservice->>'xmlId' = :xmlId) OR (r.original->>'xmlId' = :xmlId))") queryValues["xmlId"] = xmlID } var maxTime *time.Time if inf.UseIMS() { maxTime = new(time.Time) var runSecond bool runSecond, *maxTime = ims.TryIfModifiedSinceQuery(inf.Tx, r.Header, queryValues, selectMaxLastUpdatedQuery(where)) if !runSecond { log.Debugln("IMS HIT") api.WriteIMSHitResp(w, r, *maxTime) return } log.Debugln("IMS MISS") } else { log.Debugln("Non IMS request") } tenantIDs, err := tenant.GetUserTenantIDListTx(tx, inf.User.TenantID) if err != nil { sysErr = fmt.Errorf("dsr getting tenant list: %w", err) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } where = dbhelpers.AppendWhere(where, customTenancyCheck) queryValues["accessibleTenants"] = pq.Array(tenantIDs) query := selectQuery + where + orderBy + pagination log.Debugln("Query is ", query) rows, err := inf.Tx.NamedQuery(query, queryValues) if err != nil { sysErr = fmt.Errorf("dsr querying: %w", err) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } defer log.Close(rows, "getting DSRs") dsrs := []tc.DeliveryServiceRequestV5{} needOriginals := map[int][]*tc.DeliveryServiceRequestV5{} var originalIDs []int for rows.Next() { var dsr tc.DeliveryServiceRequestV5 if err = rows.StructScan(&dsr); err != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("dsr scanning: %w", err)) return } dsrs = append(dsrs, dsr) if dsr.IsOpen() && dsr.ChangeType != tc.DSRChangeTypeCreate { if dsr.ChangeType == tc.DSRChangeTypeUpdate && dsr.Requested != nil && dsr.Requested.ID != nil { id := *dsr.Requested.ID if _, ok := needOriginals[id]; !ok { needOriginals[id] = []*tc.DeliveryServiceRequestV5{&dsrs[len(dsrs)-1]} } else { needOriginals[id] = append(needOriginals[id], &dsrs[len(dsrs)-1]) } originalIDs = append(originalIDs, id) } else if dsr.ChangeType == tc.DSRChangeTypeDelete && dsr.Original != nil && dsr.Original.ID != nil { id := *dsr.Original.ID if _, ok := needOriginals[id]; !ok { needOriginals[id] = []*tc.DeliveryServiceRequestV5{&dsrs[len(dsrs)-1]} } else { needOriginals[id] = append(needOriginals[id], &dsrs[len(dsrs)-1]) } originalIDs = append(originalIDs, id) } } } if maxTime != nil { w.Header().Set(rfc.LastModified, maxTime.Format(rfc.LastModifiedFormat)) } if version.Major >= 4 { errCode, userErr, sysErr = getOriginals(originalIDs, inf.Tx, needOriginals) if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } if version.Major >= 5 { api.WriteResp(w, r, dsrs) return } if version.Minor >= 1 { downgraded := make([]tc.DeliveryServiceRequestV4, 0, len(dsrs)) for _, dsr := range dsrs { downgraded = append(downgraded, dsr.Downgrade()) } api.WriteResp(w, r, downgraded) return } downgraded := make([]tc.DeliveryServiceRequestV40, 0, len(dsrs)) for _, dsr := range dsrs { downgraded = append(downgraded, dsr.Downgrade().Downgrade()) } api.WriteResp(w, r, downgraded) return } downgraded := make([]tc.DeliveryServiceRequestNullable, 0, len(dsrs)) for _, dsr := range dsrs { downgraded = append(downgraded, dsr.Downgrade().Downgrade().Downgrade()) } api.WriteResp(w, r, downgraded) } // isTenantAuthorized ensures the user is authorized on the DSR's // DeliveryService's Tenant, as appropriate to the change type. func isTenantAuthorized(dsr tc.DeliveryServiceRequestV5, inf *api.Info) (bool, error) { if dsr.Requested != nil && (dsr.ChangeType == tc.DSRChangeTypeUpdate || dsr.ChangeType == tc.DSRChangeTypeCreate) { ok, err := tenant.IsResourceAuthorizedToUserTx(dsr.Requested.TenantID, inf.User, inf.Tx.Tx) if err != nil { err = fmt.Errorf("requested: %w", err) } if !ok || err != nil { return ok, err } } ds := dsr.Original if ds == nil || dsr.ChangeType == tc.DSRChangeTypeCreate { // No deliveryservice applied yet or change type doesn't require an original return true, nil } ok, err := tenant.IsResourceAuthorizedToUserTx(ds.TenantID, inf.User, inf.Tx.Tx) if err != nil { err = fmt.Errorf("original: %w", err) } return ok, err } // Warning: this assumes inf isn't nil, and neither is dsr, inf.Tx or inf.User or inf.Tx.Tx. func insert(dsr *tc.DeliveryServiceRequestV5, inf *api.Info) (int, error, error) { dsr.Author = inf.User.UserName dsr.LastEditedBy = inf.User.UserName if dsr.ChangeType != tc.DSRChangeTypeDelete { dsr.Original = nil } else { dsr.Requested = nil } dsr.ID = new(int) if err := inf.Tx.Tx.QueryRow(insertQuery, dsr.AssigneeID, inf.User.ID, dsr.ChangeType, dsr.Requested, dsr.Original, dsr.Status).Scan(dsr.ID, &dsr.LastUpdated, &dsr.CreatedAt); err != nil { userErr, sysErr, errCode := api.ParseDBError(err) return errCode, userErr, sysErr } if dsr.ChangeType == tc.DSRChangeTypeUpdate { query := deliveryservice.SelectDeliveryServicesQuery + `WHERE xml_id=:XMLID` originals, userErr, sysErr, errCode := deliveryservice.GetDeliveryServices(query, map[string]interface{}{"XMLID": dsr.XMLID}, inf.Tx) if userErr != nil || sysErr != nil { return errCode, userErr, sysErr } if len(originals) < 1 { userErr = fmt.Errorf("cannot update non-existent Delivery Service '%s'", dsr.XMLID) return http.StatusBadRequest, userErr, nil } if len(originals) > 1 { sysErr = fmt.Errorf("too many Delivery Services with XMLID '%s'; want: 1, got: %d", dsr.XMLID, len(originals)) return http.StatusInternalServerError, nil, sysErr } dsr.Original = new(tc.DeliveryServiceV5) *dsr.Original = originals[0].DS } return http.StatusOK, nil, nil } // dsrManipulationResult encodes the result of manipulating a DSR. type dsrManipulationResult struct { // Action is the action performed to manipulate the DSR. Action string // Assignee is a pointer to the name of the user assigned to a DSR - or nil // if there isn't one. Assignee *string // ChangeType is the DSR's change type. ChangeType tc.DSRChangeType // Successful is whether or not the manipulation encountered no errors. Successful bool // XMLID is the XMLID of the Delivery Service affected by the DSR. XMLID string } // String constructs a changelog message for the result. // Unsuccessful results do not have a changelog message. func (d dsrManipulationResult) String() string { if !d.Successful { return "" } var builder strings.Builder builder.WriteString(d.Action) builder.Write([]byte(" Delivery Service Request of type ")) builder.WriteString(d.ChangeType.String()) builder.Write([]byte(" for Delivery Service '")) builder.WriteString(d.XMLID) builder.WriteRune('\'') if d.Assignee != nil { builder.Write([]byte(" (assigned to user ")) builder.WriteString(*d.Assignee) builder.WriteRune(')') } return builder.String() } func createV5(w http.ResponseWriter, r *http.Request, inf *api.Info) (result dsrManipulationResult) { tx := inf.Tx.Tx var dsr tc.DeliveryServiceRequestV5 if err := json.NewDecoder(r.Body).Decode(&dsr); err != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("decoding: %w", err), nil) return } if userErr, sysErr := validateV5(dsr, tx); userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, sysErr) return } if dsr.Status != tc.RequestStatusDraft && dsr.Status != tc.RequestStatusSubmitted { userErr := fmt.Errorf("invalid initial request status '%s' - must be '%s' or '%s'", dsr.Status, tc.RequestStatusDraft, tc.RequestStatusSubmitted) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } ok, err := isTenantAuthorized(dsr, inf) if err != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) return } if !ok { api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil) return } dsr.SetXMLID() if ok, err = dbhelpers.DSRExistsWithXMLID(dsr.XMLID, tx); err != nil { err = fmt.Errorf("checking for existence of DSR with xmlid '%s'", dsr.XMLID) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) return } else if ok { userErr := fmt.Errorf("an open Delivery Service Request for XMLID '%s' already exists", dsr.XMLID) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } if dsr.Original != nil { if len(dsr.Original.TLSVersions) < 1 { dsr.Original.TLSVersions = nil } } if dsr.Requested != nil { if len(dsr.Requested.TLSVersions) < 1 { dsr.Requested.TLSVersions = nil } } errCode, userErr, sysErr := insert(&dsr, inf) if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } w.Header().Set(rfc.Location, fmt.Sprintf("/api/%s/deliveryservice_requests/%d", inf.Version, *dsr.ID)) w.WriteHeader(http.StatusCreated) api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service request created", dsr) result.Successful = true result.Assignee = dsr.Assignee result.XMLID = dsr.XMLID result.ChangeType = dsr.ChangeType result.Action = api.Created return } func createV4(w http.ResponseWriter, r *http.Request, inf *api.Info) (result dsrManipulationResult) { tx := inf.Tx.Tx var dsr tc.DeliveryServiceRequestV4 if err := json.NewDecoder(r.Body).Decode(&dsr); err != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("decoding: %w", err), nil) return } if userErr, sysErr := validateV4(dsr, tx); userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, sysErr) return } if dsr.Status != tc.RequestStatusDraft && dsr.Status != tc.RequestStatusSubmitted { userErr := fmt.Errorf("invalid initial request status '%s' - must be '%s' or '%s'", dsr.Status, tc.RequestStatusDraft, tc.RequestStatusSubmitted) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } upgraded := dsr.Upgrade() ok, err := isTenantAuthorized(upgraded, inf) if err != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) return } if !ok { api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil) return } upgraded.SetXMLID() if ok, err = dbhelpers.DSRExistsWithXMLID(upgraded.XMLID, tx); err != nil { err = fmt.Errorf("checking for existence of DSR with xmlid '%s'", upgraded.XMLID) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) return } else if ok { userErr := fmt.Errorf("an open Delivery Service Request for XMLID '%s' already exists", upgraded.XMLID) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } if upgraded.Original != nil { if len(upgraded.Original.TLSVersions) < 1 { upgraded.Original.TLSVersions = nil } } if upgraded.Requested != nil { if len(upgraded.Requested.TLSVersions) < 1 { upgraded.Requested.TLSVersions = nil } } errCode, userErr, sysErr := insert(&upgraded, inf) if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } dsr = upgraded.Downgrade() w.Header().Set(rfc.Location, fmt.Sprintf("/api/%s/deliveryservice_requests/%d", inf.Version, *dsr.ID)) w.WriteHeader(http.StatusCreated) if inf.Version.Minor >= 1 { api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service request created", dsr) } else { api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service request created", dsr.Downgrade()) } result.Successful = true result.Assignee = dsr.Assignee result.XMLID = dsr.XMLID result.ChangeType = dsr.ChangeType result.Action = api.Created return } func createLegacy(w http.ResponseWriter, r *http.Request, inf *api.Info) (result dsrManipulationResult) { tx := inf.Tx.Tx var dsr tc.DeliveryServiceRequestNullable if err := json.NewDecoder(r.Body).Decode(&dsr); err != nil { userErr := fmt.Errorf("decoding: %w", err) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } userErr, sysErr := validateLegacy(dsr, tx) if sysErr != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } if userErr != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } upgraded := dsr.Upgrade().Upgrade().Upgrade() authorized, err := isTenantAuthorized(upgraded, inf) if err != nil { sysErr := fmt.Errorf("checking tenant authorized: %w", err) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } if !authorized { userErr := errors.New("not authorized on this tenant") api.HandleErr(w, r, tx, http.StatusForbidden, userErr, nil) return } if *dsr.Status != tc.RequestStatusDraft && *dsr.Status != tc.RequestStatusSubmitted { userErr := fmt.Errorf("invalid initial request status '%v'. Must be '%v' or '%v'", *dsr.Status, tc.RequestStatusDraft, tc.RequestStatusSubmitted) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } // first, ensure there's not an active request with this XMLID ds := dsr.DeliveryService if ds == nil { userErr := errors.New("no delivery service associated with this request") api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } if ds.XMLID == nil { userErr := errors.New("no XMLID associated with this request") api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } XMLID := *ds.XMLID active, err := isActiveRequest(inf.Tx, XMLID) if err != nil { sysErr := fmt.Errorf("checking request active: %w", err) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } if active { userErr := fmt.Errorf("an active request exists for Delivery Service '%s'", XMLID) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } errCode, userErr, sysErr := insert(&upgraded, inf) if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Delivery Service request created", upgraded.Downgrade().Downgrade().Downgrade()) result.Successful = true result.Assignee = dsr.Assignee result.XMLID = upgraded.XMLID result.ChangeType = upgraded.ChangeType result.Action = api.Created return result } // Post is the handler for POST requests to /deliveryservice_requests. func Post(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() // Middleware should've already handled this, so idk why this is a pointer at all tbh version := inf.Version if version == nil { middleware.NotImplementedHandler().ServeHTTP(w, r) return } if inf.User == nil { sysErr = errors.New("no user in API Info") api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, sysErr) return } var result dsrManipulationResult switch version.Major { default: fallthrough case 5: result = createV5(w, r, inf) case 4: result = createV4(w, r, inf) case 3: result = createLegacy(w, r, inf) } if result.Successful { inf.CreateChangeLog(result.String()) } } // Delete is the handler for DELETE requests to /deliveryservice_requests. func Delete(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"}) tx := inf.Tx.Tx if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } defer inf.Close() // Middleware should've already handled this, so idk why this is a pointer at all tbh if inf.Version == nil { middleware.NotImplementedHandler().ServeHTTP(w, r) return } if inf.User == nil { sysErr = errors.New("no user in API Info") api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, sysErr) return } var dsr tc.DeliveryServiceRequestV5 if err := inf.Tx.QueryRowx(selectQuery+"WHERE r.id=$1", inf.IntParams["id"]).StructScan(&dsr); err != nil { if errors.Is(err, sql.ErrNoRows) { errCode = http.StatusNotFound userErr = fmt.Errorf("no such Delivery Service Request: #%d", inf.IntParams["id"]) sysErr = nil } else { userErr, sysErr, errCode = api.ParseDBError(err) } api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } dsr.SetXMLID() authorized, err := isTenantAuthorized(dsr, inf) if err != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) return } if !authorized { api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil) return } result, err := tx.Exec(deleteQuery, inf.IntParams["id"]) if err != nil { sysErr = fmt.Errorf("deleting DSR #%d: %w", inf.IntParams["id"], err) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } if affected, err := result.RowsAffected(); err != nil { sysErr = fmt.Errorf("checking affected rows: %w", err) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } else if affected != 1 { sysErr = fmt.Errorf("incorrect number of rows affected by delete: %d", affected) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } if dsr.IsOpen() && dsr.ChangeType != tc.DSRChangeTypeCreate { if dsr.ChangeType == tc.DSRChangeTypeDelete && dsr.Original != nil && dsr.Original.ID != nil { errCode, userErr, sysErr = getOriginals([]int{*dsr.Original.ID}, inf.Tx, map[int][]*tc.DeliveryServiceRequestV5{*dsr.Original.ID: {&dsr}}) } else if dsr.ChangeType == tc.DSRChangeTypeUpdate && dsr.Requested != nil && dsr.Requested.ID != nil { errCode, userErr, sysErr = getOriginals([]int{*dsr.Requested.ID}, inf.Tx, map[int][]*tc.DeliveryServiceRequestV5{*dsr.Requested.ID: {&dsr}}) } if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } } var resp interface{} switch inf.Version.Major { default: fallthrough case 5: resp = dsr case 4: if inf.Version.Minor >= 1 { resp = dsr.Downgrade() } else { resp = dsr.Downgrade().Downgrade() } case 3: resp = dsr.Downgrade().Downgrade().Downgrade() } api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Delivery Service Request #%d deleted", inf.IntParams["id"]), resp) res := dsrManipulationResult{ Successful: true, XMLID: dsr.XMLID, Action: api.Deleted, Assignee: dsr.Assignee, ChangeType: dsr.ChangeType, } inf.CreateChangeLog(res.String()) } func putV50(w http.ResponseWriter, r *http.Request, inf *api.Info) (result dsrManipulationResult) { tx := inf.Tx.Tx var dsr tc.DeliveryServiceRequestV5 if err := json.NewDecoder(r.Body).Decode(&dsr); err != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("decoding: %w", err), nil) return } if userErr, sysErr := validateV5(dsr, tx); userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, sysErr) return } if dsr.Status != tc.RequestStatusDraft && dsr.Status != tc.RequestStatusSubmitted { userErr := fmt.Errorf("cannot change DeliveryServiceRequest status to '%s'", dsr.Status) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } if dsr.ChangeType != tc.DSRChangeTypeDelete { dsr.Original = nil } else { dsr.Requested = nil } authorized, err := isTenantAuthorized(dsr, inf) if err != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) return } if !authorized { api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil) return } dsr.LastEditedBy = inf.User.UserName dsr.LastEditedByID = new(int) *dsr.LastEditedByID = inf.User.ID if dsr.Requested != nil && len(dsr.Requested.TLSVersions) < 1 { dsr.Requested.TLSVersions = nil } if dsr.Original != nil && len(dsr.Original.TLSVersions) < 1 { dsr.Original.TLSVersions = nil } args := []interface{}{ dsr.AssigneeID, dsr.ChangeType, inf.User.ID, dsr.Requested, dsr.Original, dsr.Status, inf.IntParams["id"], } if err := tx.QueryRow(updateQuery, args...).Scan(&dsr.CreatedAt, &dsr.LastUpdated); err != nil { var userErr, sysErr error var errCode int if errors.Is(err, sql.ErrNoRows) { userErr = fmt.Errorf("no such Delivery Service Request: #%d", inf.IntParams["id"]) errCode = http.StatusNotFound sysErr = fmt.Errorf("running update query for Delivery Service Requests: %w", err) } else { userErr, sysErr, errCode = api.ParseDBError(err) } api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } dsr.SetXMLID() if dsr.ChangeType == tc.DSRChangeTypeUpdate { query := deliveryservice.SelectDeliveryServicesQuery + `WHERE xml_id=:XMLID` originals, userErr, sysErr, errCode := deliveryservice.GetDeliveryServices(query, map[string]interface{}{"XMLID": dsr.XMLID}, inf.Tx) if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } if len(originals) < 1 { userErr = fmt.Errorf("cannot update non-existent Delivery Service '%s'", dsr.XMLID) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } if len(originals) > 1 { sysErr = fmt.Errorf("too many Delivery Services with XMLID '%s'; want: 1, got: %d", dsr.XMLID, len(originals)) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } dsr.Original = new(tc.DeliveryServiceV5) *dsr.Original = originals[0].DS } api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Delivery Service Request #%d updated", inf.IntParams["id"]), dsr) result.Successful = true result.Action = "Updated" result.Assignee = dsr.Assignee result.ChangeType = dsr.ChangeType result.XMLID = dsr.XMLID return } func putV4(w http.ResponseWriter, r *http.Request, inf *api.Info) (result dsrManipulationResult) { tx := inf.Tx.Tx var dsr tc.DeliveryServiceRequestV4 var dsrV40 tc.DeliveryServiceRequestV40 if inf.Version.Minor == 0 { if err := json.NewDecoder(r.Body).Decode(&dsrV40); err != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("decoding: %v", err), nil) return } dsr = dsrV40.Upgrade() } else { if err := json.NewDecoder(r.Body).Decode(&dsr); err != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("decoding: %v", err), nil) return } dsrV40 = dsr.Downgrade() } if userErr, sysErr := validateV4(dsr, tx); userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, sysErr) return } if dsr.Status != tc.RequestStatusDraft && dsr.Status != tc.RequestStatusSubmitted { userErr := fmt.Errorf("cannot change DeliveryServiceRequest status to '%s'", dsr.Status) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } if dsr.ChangeType != tc.DSRChangeTypeDelete { dsr.Original = nil } else { dsr.Requested = nil } upgraded := dsr.Upgrade() authorized, err := isTenantAuthorized(upgraded, inf) if err != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) return } if !authorized { api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil) return } upgraded.LastEditedBy = inf.User.UserName upgraded.LastEditedByID = new(int) *upgraded.LastEditedByID = inf.User.ID if upgraded.Requested != nil && len(upgraded.Requested.TLSVersions) < 1 { upgraded.Requested.TLSVersions = nil } if upgraded.Original != nil && len(upgraded.Original.TLSVersions) < 1 { upgraded.Original.TLSVersions = nil } args := []interface{}{ upgraded.AssigneeID, upgraded.ChangeType, inf.User.ID, upgraded.Requested, upgraded.Original, upgraded.Status, inf.IntParams["id"], } if dsr.Original != nil { if dsr.Original.LongDesc1 != nil || dsr.Original.LongDesc2 != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("the longDesc1 and longDesc2 fields are no longer supported in API 4.0 onwards"), nil) return } } if dsr.Requested != nil { if dsr.Requested.LongDesc1 != nil || dsr.Requested.LongDesc2 != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("the longDesc1 and longDesc2 fields are no longer supported in API 4.0 onwards"), nil) return } } if err := tx.QueryRow(updateQuery, args...).Scan(&upgraded.CreatedAt, &upgraded.LastUpdated); err != nil { var userErr, sysErr error var errCode int if errors.Is(err, sql.ErrNoRows) { userErr = fmt.Errorf("no such Delivery Service Request: #%d", inf.IntParams["id"]) errCode = http.StatusNotFound sysErr = fmt.Errorf("running update query for Delivery Service Requests: %w", err) } else { userErr, sysErr, errCode = api.ParseDBError(err) } api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } upgraded.SetXMLID() dsr.XMLID = upgraded.XMLID if dsr.ChangeType == tc.DSRChangeTypeUpdate { query := deliveryservice.SelectDeliveryServicesQuery + `WHERE xml_id=:XMLID` originals, userErr, sysErr, errCode := deliveryservice.GetDeliveryServices(query, map[string]interface{}{"XMLID": dsr.XMLID}, inf.Tx) if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } if len(originals) < 1 { userErr = fmt.Errorf("cannot update non-existent Delivery Service '%s'", dsr.XMLID) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } if len(originals) > 1 { sysErr = fmt.Errorf("too many Delivery Services with XMLID '%s'; want: 1, got: %d", dsr.XMLID, len(originals)) api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } dsr.Original = new(tc.DeliveryServiceV4) *dsr.Original = originals[0].DS.Downgrade() *dsr.Original = dsr.Original.RemoveLD1AndLD2() if dsr.Requested != nil { *dsr.Requested = dsr.Requested.RemoveLD1AndLD2() } } api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Delivery Service Request #%d updated", inf.IntParams["id"]), dsr) result.Successful = true result.Action = "Updated" result.Assignee = dsr.Assignee result.ChangeType = dsr.ChangeType result.XMLID = dsr.XMLID return } func putLegacy(w http.ResponseWriter, r *http.Request, inf *api.Info) (result dsrManipulationResult) { tx := inf.Tx.Tx var dsr tc.DeliveryServiceRequestNullable if err := json.NewDecoder(r.Body).Decode(&dsr); err != nil { userErr := fmt.Errorf("decoding: %w", err) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } userErr, sysErr := validateLegacy(dsr, tx) if sysErr != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } if userErr != nil { api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } if *dsr.Status != tc.RequestStatusDraft && *dsr.Status != tc.RequestStatusSubmitted { userErr := fmt.Errorf("cannot change DeliveryServiceRequest status to '%s'", dsr.Status) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } dsr.LastEditedBy = new(string) *dsr.LastEditedBy = inf.User.UserName dsr.LastEditedByID = new(tc.IDNoMod) *dsr.LastEditedByID = tc.IDNoMod(inf.User.ID) upgraded := dsr.Upgrade().Upgrade().Upgrade() authorized, err := isTenantAuthorized(upgraded, inf) if err != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) return } if !authorized { api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil) return } args := []interface{}{ upgraded.AssigneeID, upgraded.ChangeType, inf.User.ID, upgraded.Requested, upgraded.Original, upgraded.Status, inf.IntParams["id"], } if err := tx.QueryRow(updateQuery, args...).Scan(&dsr.CreatedAt, &dsr.LastUpdated); err != nil { var errCode int var userErr, sysErr error if errors.Is(err, sql.ErrNoRows) { errCode = http.StatusNotFound userErr = fmt.Errorf("no such Delivery Service Request: #%d", inf.IntParams["id"]) sysErr = nil } else { userErr, sysErr, errCode = api.ParseDBError(err) } api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } upgraded.SetXMLID() dsr.XMLID = &upgraded.XMLID api.WriteRespAlertObj(w, r, tc.SuccessLevel, fmt.Sprintf("Delivery Service Request #%d updated", inf.IntParams["id"]), dsr) result.Action = api.Updated result.Assignee = dsr.Assignee result.ChangeType = upgraded.ChangeType result.Successful = true result.XMLID = upgraded.XMLID return } // Put is the handler for PUT requests to /deliveryservice_requests. func Put(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"}) tx := inf.Tx.Tx if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } defer inf.Close() // Middleware should've already handled this, so idk why this is a pointer at all tbh if inf.Version == nil { middleware.NotImplementedHandler().ServeHTTP(w, r) return } if inf.User == nil { sysErr = errors.New("no user in API Info") api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr) return } id := inf.IntParams["id"] errCode, userErr, sysErr = inf.CheckPrecondition(selectMaxLastUpdatedQuery("WHERE r.id = $1"), id) if userErr != nil || sysErr != nil { api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } var current tc.DeliveryServiceRequestV5 if err := inf.Tx.QueryRowx(selectQuery+"WHERE r.id=$1", id).StructScan(&current); err != nil { if errors.Is(err, sql.ErrNoRows) { errCode = http.StatusNotFound userErr = fmt.Errorf("no such Delivery Service Request: #%d", inf.IntParams["id"]) sysErr = nil } else { userErr, sysErr, errCode = api.ParseDBError(err) } api.HandleErr(w, r, tx, errCode, userErr, sysErr) return } authorized, err := isTenantAuthorized(current, inf) if err != nil { api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) return } if !authorized { api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil) return } if !current.IsOpen() { userErr = fmt.Errorf("cannot change DeliveryServiceRequest in '%s' status", current.Status) api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) return } var result dsrManipulationResult switch inf.Version.Major { default: fallthrough case 5: result = putV50(w, r, inf) case 4: result = putV4(w, r, inf) case 3: result = putLegacy(w, r, inf) } if result.Successful { inf.CreateChangeLog(result.String()) } } // isActiveRequest returns true if a request using this XMLID is currently in an active state. func isActiveRequest(tx *sqlx.Tx, xmlID string) (bool, error) { qry := `SELECT EXISTS(SELECT 1 FROM deliveryservice_request WHERE deliveryservice->>'xmlId' = $1 AND status IN ('draft', 'submitted', 'pending'))` active := false if err := tx.QueryRow(qry, xmlID).Scan(&active); err != nil { return false, err } return active, nil }