traffic_ops/traffic_ops_golang/cdnfederation/cdnfederations.go (573 lines of code) (raw):
// Package cdnfederation is one of many, many packages that contain logic
// pertaining to federations of CDNs and/or parts of CDNs.
package cdnfederation
/*
* 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"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/apache/trafficcontrol/v8/lib/go-log"
"github.com/apache/trafficcontrol/v8/lib/go-tc"
"github.com/apache/trafficcontrol/v8/lib/go-tc/tovalidate"
"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"
"github.com/asaskevich/govalidator"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/go-ozzo/ozzo-validation/is"
"github.com/lib/pq"
)
// we need a type alias to define functions on
type TOCDNFederation struct {
api.APIInfoImpl `json:"-"`
tc.CDNFederation
TenantID *int `json:"-" db:"tenant_id"`
}
func (v *TOCDNFederation) GetLastUpdated() (*time.Time, bool, error) {
return api.GetLastUpdated(v.APIInfo().Tx, *v.ID, "federation")
}
func selectMaxLastUpdatedQuery(where, orderBy, pagination string) string {
return `
SELECT max(t)
FROM (
(
SELECT federation.last_updated AS t
FROM federation
JOIN federation_deliveryservice fds
ON fds.federation = federation.id
JOIN deliveryservice ds
ON ds.id = fds.deliveryservice
JOIN cdn c
ON c.id = ds.cdn_id ` + where + orderBy + pagination +
`)
UNION ALL
(
SELECT max(last_updated) AS t
FROM last_deleted l
WHERE l.table_name='federation'
)
) AS res`
}
func (v *TOCDNFederation) SetLastUpdated(t tc.TimeNoMod) { v.LastUpdated = &t }
func (*TOCDNFederation) InsertQuery() string {
return `
INSERT INTO federation (
cname,
ttl,
description
) VALUES (
:cname,
:ttl,
:description
) RETURNING id, last_updated`
}
func (v *TOCDNFederation) SelectMaxLastUpdatedQuery(where, orderBy, pagination, _ string) string {
return selectMaxLastUpdatedQuery(where, orderBy, pagination)
}
func (v *TOCDNFederation) NewReadObj() interface{} { return &TOCDNFederation{} }
func (v *TOCDNFederation) SelectQuery() string {
return selectByCDNName()
}
func paramColumnInfo(v api.Version) map[string]dbhelpers.WhereColumnInfo {
if v.GreaterThanOrEqualTo(&api.Version{Major: 5}) {
return map[string]dbhelpers.WhereColumnInfo{
"id": {
Column: "federation.id",
Checker: api.IsInt,
},
"name": {
Column: "c.name",
},
"cname": {
Column: "federation.cname",
},
"xmlID": {
Column: "ds.xml_id",
},
"dsID": {
Column: "ds.id",
Checker: api.IsInt,
},
}
}
return map[string]dbhelpers.WhereColumnInfo{
"id": {
Column: "federation.id",
Checker: api.IsInt,
},
"name": {
Column: "c.name",
Checker: nil,
},
"cname": {
Column: "federation.cname",
Checker: nil,
},
}
}
func (v *TOCDNFederation) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
return paramColumnInfo(*v.ReqInfo.Version)
}
func (*TOCDNFederation) DeleteQuery() string { return `DELETE FROM federation WHERE id = :id` }
func (*TOCDNFederation) UpdateQuery() string {
return `
UPDATE federation SET
cname = :cname,
ttl = :ttl,
description = :description
WHERE
id=:id
RETURNING last_updated`
}
// Fufills `Identifier' interface
func (fed TOCDNFederation) GetKeyFieldsInfo() []api.KeyFieldInfo {
return []api.KeyFieldInfo{{Field: "id", Func: api.GetIntKey}}
}
// Fufills `Identifier' interface
func (fed TOCDNFederation) GetKeys() (map[string]interface{}, bool) {
if fed.ID == nil {
return map[string]interface{}{"id": 0}, false
}
return map[string]interface{}{"id": *fed.ID}, true
}
// Fufills `Identifier' interface
func (fed TOCDNFederation) GetAuditName() string {
if fed.CName != nil {
return *fed.CName
}
if fed.ID != nil {
return strconv.Itoa(*fed.ID)
}
return "unknown"
}
// Fufills `Identifier' interface
func (fed TOCDNFederation) GetType() string {
return "cdnfederation"
}
// Fufills `Create' interface
func (fed *TOCDNFederation) SetKeys(keys map[string]interface{}) {
i, _ := keys["id"].(int) // non-panicking type assertion
fed.ID = &i
}
// Fulfills `Validate' interface
func (fed *TOCDNFederation) Validate() (error, error) {
isDNSName := validation.NewStringRule(govalidator.IsDNSName, "must be a valid hostname")
endsWithDot := validation.NewStringRule(
func(str string) bool {
return strings.HasSuffix(str, ".")
}, "must end with a period")
// cname regex: (^\S*\.$), ttl regex: (^\d+$)
validateErrs := validation.Errors{
"cname": validation.Validate(fed.CName, validation.Required, endsWithDot, isDNSName),
"ttl": validation.Validate(fed.TTL, validation.Required, validation.Min(0)),
}
return util.JoinErrs(tovalidate.ToErrors(validateErrs)), nil
}
func (fed *TOCDNFederation) CheckIfCDNAndFederationMatch(cdnName string) (error, error, int) {
var cdnFromDS string
var err error
if fed.DeliveryServiceIDs != nil {
if fed.DsId != nil {
cdnNames, err := dbhelpers.GetCDNNamesFromDSIds(fed.APIInfo().Tx.Tx, []int{*fed.DsId})
if err != nil {
return nil, fmt.Errorf("getting CDN names from DS IDs: %w", err), http.StatusInternalServerError
}
if len(cdnNames) != 1 {
return fmt.Errorf("%d CDNs returned for one DS ID", len(cdnNames)), nil, http.StatusBadRequest
}
cdnFromDS = cdnNames[0]
} else if fed.XmlId != nil {
cdnFromDS, err = dbhelpers.GetCDNNameFromDSXMLID(fed.APIInfo().Tx.Tx, *fed.XmlId)
if err != nil {
return nil, fmt.Errorf("getting CDN name from DS XMLID: %w", err), http.StatusInternalServerError
}
}
}
if cdnFromDS != "" && cdnFromDS != cdnName {
return errors.New("cdn names in request path and payload do not match"), nil, http.StatusBadRequest
}
return nil, nil, http.StatusOK
}
// fedAPIInfo.Params["name"] is not used on creation, rather the cdn name
// is connected when the federations/:id/deliveryservice links a federation
// However, we use fedAPIInfo.Params["name"] to check whether or not another user has a hard lock on the CDN.
// Note: cdns and deliveryservies have a 1-1 relationship
func (fed *TOCDNFederation) Create() (error, error, int) {
if cdn, ok := fed.APIInfo().Params["name"]; ok {
if ok, err := dbhelpers.CDNExists(fed.APIInfo().Params["name"], fed.APIInfo().Tx.Tx); err != nil {
return nil, errors.New("verifying CDN exists: " + err.Error()), http.StatusInternalServerError
} else if !ok {
return errors.New("cdn not found"), nil, http.StatusNotFound
}
userErr, sysErr, errCode := fed.CheckIfCDNAndFederationMatch(cdn)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
userErr, sysErr, errCode = dbhelpers.CheckIfCurrentUserCanModifyCDN(fed.APIInfo().Tx.Tx, cdn, fed.APIInfo().User.UserName)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
}
// Deliveryservice IDs should not be included on create.
if fed.DeliveryServiceIDs != nil {
fed.DsId = nil
fed.XmlId = nil
fed.DeliveryServiceIDs = nil
}
return api.GenericCreate(fed)
}
// returning true indicates the data related to the given tenantID should be visible
// `tenantIDs` is presumed to be unsorted, and a nil tenantID is viewable by everyone
func checkTenancy(tenantID *int, tenantIDs []int) bool {
if tenantID == nil {
return true
}
for _, id := range tenantIDs {
if id == *tenantID {
return true
}
}
return false
}
func (fed *TOCDNFederation) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
if idstr, ok := fed.APIInfo().Params["id"]; ok {
id, err := strconv.Atoi(idstr)
if err != nil {
return nil, errors.New("id must be an integer"), nil, http.StatusBadRequest, nil
}
fed.ID = util.IntPtr(id)
}
tenantIDs, err := tenant.GetUserTenantIDListTx(fed.APIInfo().Tx.Tx, fed.APIInfo().User.TenantID)
if err != nil {
return nil, nil, errors.New("getting tenant list for user: " + err.Error()), http.StatusInternalServerError, nil
}
api.DefaultSort(fed.APIInfo(), "cname")
if ok, err := dbhelpers.CDNExists(fed.APIInfo().Params["name"], fed.APIInfo().Tx.Tx); err != nil {
return nil, nil, errors.New("verifying CDN exists: " + err.Error()), http.StatusInternalServerError, nil
} else if !ok {
return nil, errors.New("cdn not found"), nil, http.StatusNotFound, nil
}
federations, userErr, sysErr, errCode, maxTime := api.GenericRead(h, fed, useIMS)
if userErr != nil || sysErr != nil {
return nil, userErr, sysErr, errCode, nil
}
if errCode == http.StatusNotModified {
return []interface{}{}, nil, nil, http.StatusNotModified, maxTime
}
filteredFederations := []interface{}{}
for _, ifederation := range federations {
federation := ifederation.(*TOCDNFederation)
if !checkTenancy(federation.TenantID, tenantIDs) {
return nil, errors.New("user not authorized for requested federation"), nil, http.StatusForbidden, nil
}
filteredFederations = append(filteredFederations, federation.CDNFederation)
}
if len(filteredFederations) == 0 {
if fed.ID != nil {
return nil, errors.New("not found"), nil, http.StatusNotFound, nil
}
}
return filteredFederations, nil, nil, errCode, maxTime
}
func (fed *TOCDNFederation) Update(h http.Header) (error, error, int) {
userErr, sysErr, errCode := fed.isTenantAuthorized()
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
if cdn, ok := fed.APIInfo().Params["name"]; ok {
if ok, err := dbhelpers.CDNExists(fed.APIInfo().Params["name"], fed.APIInfo().Tx.Tx); err != nil {
return nil, errors.New("verifying CDN exists: " + err.Error()), http.StatusInternalServerError
} else if !ok {
return errors.New("cdn not found"), nil, http.StatusNotFound
}
userErr, sysErr, errCode := fed.CheckIfCDNAndFederationMatch(cdn)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
userErr, sysErr, errCode = dbhelpers.CheckIfCurrentUserCanModifyCDN(fed.APIInfo().Tx.Tx, cdn, fed.APIInfo().User.UserName)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
}
// Deliveryservice IDs should not be included on update.
if fed.DeliveryServiceIDs != nil {
fed.DsId = nil
fed.XmlId = nil
fed.DeliveryServiceIDs = nil
}
return api.GenericUpdate(h, fed)
}
// Delete implements the Deleter interface for TOCDNFederation.
func (fed *TOCDNFederation) Delete() (error, error, int) {
userErr, sysErr, errCode := fed.isTenantAuthorized()
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
if cdn, ok := fed.APIInfo().Params["name"]; ok {
if ok, err := dbhelpers.CDNExists(fed.APIInfo().Params["name"], fed.APIInfo().Tx.Tx); err != nil {
return nil, errors.New("verifying CDN exists: " + err.Error()), http.StatusInternalServerError
} else if !ok {
return errors.New("cdn not found"), nil, http.StatusNotFound
}
userErr, sysErr, errCode := fed.CheckIfCDNAndFederationMatch(cdn)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
userErr, sysErr, errCode = dbhelpers.CheckIfCurrentUserCanModifyCDN(fed.APIInfo().Tx.Tx, cdn, fed.APIInfo().User.UserName)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
}
return api.GenericDelete(fed)
}
func (fed TOCDNFederation) isTenantAuthorized() (error, error, int) {
tenantID, err := getTenantIDFromFedID(*fed.ID, fed.APIInfo().Tx.Tx)
if err != nil {
// If nobody has claimed a tenant, that federation is publicly visible.
// This logically follows /federations/:id/deliveryservices
if err == sql.ErrNoRows {
return nil, nil, http.StatusOK
}
return nil, errors.New("getting tenant id from federation: " + err.Error()), http.StatusInternalServerError
}
// TODO: use IsResourceAuthorizedToUserTx instead
list, err := tenant.GetUserTenantIDListTx(fed.APIInfo().Tx.Tx, fed.APIInfo().User.TenantID)
if err != nil {
return nil, errors.New("getting federation tenant list: " + err.Error()), http.StatusInternalServerError
}
for _, id := range list {
if id == tenantID {
return nil, nil, http.StatusOK
}
}
return errors.New("unauthorized for tenant"), nil, http.StatusForbidden
}
func getTenantIDFromFedID(id int, tx *sql.Tx) (int, error) {
tenantID := 0
query := `
SELECT ds.tenant_id FROM federation AS f
JOIN federation_deliveryservice AS fd ON f.id = fd.federation
JOIN deliveryservice AS ds ON ds.id = fd.deliveryservice
WHERE f.id = $1`
err := tx.QueryRow(query, id).Scan(&tenantID)
return tenantID, err
}
func selectByID() string {
return `
SELECT
ds.tenant_id,
federation.id AS id,
federation.cname,
federation.ttl,
federation.description,
federation.last_updated,
ds.id AS ds_id,
ds.xml_id
FROM federation
LEFT JOIN federation_deliveryservice AS fd ON federation.id = fd.federation
LEFT JOIN deliveryservice AS ds ON ds.id = fd.deliveryservice`
// WHERE federation.id = :id (determined by dbhelper)
}
const selectQuery = `
SELECT
ds.tenant_id,
federation.id AS id,
federation.cname,
federation.ttl,
federation.description,
federation.last_updated,
ds.id AS ds_id,
ds.xml_id
FROM federation
JOIN
federation_deliveryservice AS fd
ON federation.id = fd.federation
JOIN
deliveryservice AS ds
ON ds.id = fd.deliveryservice
JOIN
cdn AS c
ON c.id = ds.cdn_id
`
const insertQuery = `
INSERT INTO federation (
cname,
ttl,
"description"
) VALUES (
$1,
$2,
$3
)
RETURNING
id,
last_updated
`
func selectByCDNName() string {
return selectQuery
}
const updateQuery = `
UPDATE federation SET
cname = $1,
ttl = $2,
"description" = $3
WHERE
id = $4
RETURNING
last_updated
`
const deleteQuery = `
DELETE FROM federation
WHERE id = $1
RETURNING
cname,
"description",
id,
last_updated,
ttl
`
func addTenancyStmt(where string) string {
if where == "" {
where = "WHERE "
} else {
where += " AND "
}
where += "ds.tenant_id = ANY(:tenantIDs)"
return where
}
func getCDNFederations(inf *api.Info) ([]tc.CDNFederationV5, time.Time, int, error, error) {
tenantList, err := tenant.GetUserTenantIDListTx(inf.Tx.Tx, inf.User.TenantID)
if err != nil {
return nil, time.Time{}, http.StatusInternalServerError, nil, fmt.Errorf("getting tenant list for user: %w", err)
}
cols := paramColumnInfo(*inf.Version)
where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, cols)
if len(errs) > 0 {
return nil, time.Time{}, http.StatusBadRequest, util.JoinErrs(errs), nil
}
queryValues["tenantIDs"] = pq.Array(tenantList)
where = addTenancyStmt(where)
if inf.UseIMS() {
query := selectMaxLastUpdatedQuery(where, orderBy, pagination)
cont, max := ims.TryIfModifiedSinceQuery(inf.Tx, inf.RequestHeaders(), queryValues, query)
if !cont {
log.Debugln("IMS HIT")
return nil, max, http.StatusNotModified, nil, nil
}
log.Debugln("IMS MISS")
} else {
log.Debugln("Non IMS request")
}
query := selectQuery + where + orderBy + pagination
rows, err := inf.Tx.NamedQuery(query, queryValues)
if err != nil {
userErr, sysErr, code := api.ParseDBError(err)
return nil, time.Time{}, code, userErr, sysErr
}
defer log.Close(rows, "closing CDNFederation rows")
ret := []tc.CDNFederationV5{}
for rows.Next() {
fed := tc.CDNFederationV5{
DeliveryService: new(tc.CDNFederationDeliveryService),
}
var tenantID int
err := rows.Scan(
&tenantID,
&fed.ID,
&fed.CName,
&fed.TTL,
&fed.Description,
&fed.LastUpdated,
&fed.DeliveryService.ID,
&fed.DeliveryService.XMLID,
)
if err != nil {
return nil, time.Time{}, http.StatusInternalServerError, nil, fmt.Errorf("scanning a CDN Federation: %w", err)
}
ret = append(ret, fed)
}
return ret, time.Time{}, http.StatusOK, nil, nil
}
// Read handles GET requests to `cdns/{{name}}/federations`.
func Read(inf *api.Info) (int, error, error) {
api.DefaultSort(inf, "cname")
feds, max, code, userErr, sysErr := getCDNFederations(inf)
if userErr != nil || sysErr != nil {
return code, userErr, sysErr
}
if feds == nil {
return inf.WriteNotModifiedResponse(max)
}
return inf.WriteOKResponse(feds)
}
// ReadID handles GET requests to `cdns/{{name}}/federations/{{ID}}`.
func ReadID(inf *api.Info) (int, error, error) {
feds, max, code, userErr, sysErr := getCDNFederations(inf)
if userErr != nil || sysErr != nil {
return code, userErr, sysErr
}
if feds == nil {
return inf.WriteNotModifiedResponse(max)
}
id := inf.IntParams["id"]
if len(feds) == 0 {
return http.StatusNotFound, fmt.Errorf("no such Federation #%d in CDN %s", id, inf.Params["name"]), nil
}
if len(feds) > 1 {
return http.StatusInternalServerError, nil, fmt.Errorf("%d CDN federations found by ID: %d", len(feds), id)
}
return inf.WriteOKResponse(feds[0])
}
func validate(fed tc.CDNFederationV5) error {
endsWithDot := validation.NewStringRule(
func(str string) bool {
return strings.HasSuffix(str, ".")
},
"must end with a period",
)
validateErrs := validation.Errors{
"cname": validation.Validate(fed.CName, validation.Required, is.DNSName, endsWithDot),
"ttl": validation.Validate(fed.TTL, validation.Required, validation.Min(60)),
}
return util.JoinErrs(tovalidate.ToErrors(validateErrs))
}
// Create handles POST requests to `cdns/{{name}}/federations`.
func Create(inf *api.Info) (int, error, error) {
var fed tc.CDNFederationV5
if err := inf.DecodeBody(&fed); err != nil {
return http.StatusBadRequest, fmt.Errorf("parsing request body: %w", err), nil
}
// You can't set this at creation time, but if it was in the request it
// would be shown in the response - we're supposed to ignore extra fields.
// This doesn't do that exactly, but it helps.
fed.DeliveryService = nil
err := validate(fed)
if err != nil {
return http.StatusBadRequest, err, nil
}
err = inf.Tx.Tx.QueryRow(insertQuery, fed.CName, fed.TTL, fed.Description).Scan(&fed.ID, &fed.LastUpdated)
if err != nil {
userErr, sysErr, code := api.ParseDBError(err)
return code, userErr, fmt.Errorf("inserting a CDN Federation: %w", sysErr)
}
changeLogMsg := fmt.Sprintf("CDNFEDERATION: %s, ID:%d, ACTION: Created cdnFederation", fed.CName, fed.ID)
api.CreateChangeLogRawTx(api.Created, changeLogMsg, inf.User, inf.Tx.Tx)
return inf.WriteCreatedResponse(fed, "Federation was created", "federations/"+strconv.Itoa(fed.ID))
}
// Update handles PUT requests to `cdns/{{name}}/federations/{{id}}`.
func Update(inf *api.Info) (int, error, error) {
var fed tc.CDNFederationV5
if err := inf.DecodeBody(&fed); err != nil {
return http.StatusBadRequest, fmt.Errorf("parsing request body: %w", err), nil
}
id := inf.IntParams["id"]
var lastModified time.Time
err := inf.Tx.QueryRow("SELECT last_updated FROM federation WHERE id = $1", id).Scan(&lastModified)
if err != nil {
return http.StatusInternalServerError, nil, fmt.Errorf("getting last modified time for Federation #%d: %w", id, err)
}
if !api.IsUnmodified(inf.RequestHeaders(), lastModified) {
return http.StatusPreconditionFailed, api.ResourceModifiedError, nil
}
// You can't set this via a PUT request, but if it was in the request it
// would be shown in the response - we're supposed to ignore extra fields.
// This doesn't do that exactly, but it helps.
fed.DeliveryService = nil
fed.ID = id
err = validate(fed)
if err != nil {
return http.StatusBadRequest, err, nil
}
err = inf.Tx.Tx.QueryRow(updateQuery, fed.CName, fed.TTL, fed.Description, id).Scan(&fed.LastUpdated)
if err != nil {
userErr, sysErr, code := api.ParseDBError(err)
return code, userErr, sysErr
}
changeLogMsg := fmt.Sprintf("CDNFEDERATION: %s, ID:%d, ACTION: Updated cdnFederation", fed.CName, id)
api.CreateChangeLogRawTx(api.Updated, changeLogMsg, inf.User, inf.Tx.Tx)
return inf.WriteSuccessResponse(fed, "Federation was updated")
}
// Delete handles DELETE requests to `cdns/{{name}}/federations/{{id}}`.
func Delete(inf *api.Info) (int, error, error) {
id := inf.IntParams["id"]
var fed tc.CDNFederationV5
err := inf.Tx.QueryRow(deleteQuery, id).Scan(&fed.CName, &fed.Description, &fed.ID, &fed.LastUpdated, &fed.TTL)
if err != nil {
userErr, sysErr, code := api.ParseDBError(err)
return code, userErr, sysErr
}
changeLogMsg := fmt.Sprintf("CDNFEDERATION:%s, ID:%d, ACTION: Deleted cdnFederation", fed.CName, fed.ID)
api.CreateChangeLogRawTx(api.Deleted, changeLogMsg, inf.User, inf.Tx.Tx)
return inf.WriteSuccessResponse(fed, "Federation was deleted")
}