You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ra...@apache.org on 2021/05/04 15:55:01 UTC

[trafficcontrol] branch master updated: URL Sig Keys in Postgres and DELETE methods (#5801)

This is an automated email from the ASF dual-hosted git repository.

rawlin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 15101a5  URL Sig Keys in Postgres and DELETE methods (#5801)
15101a5 is described below

commit 15101a5bb7e7edf19f1bfc8c30848444c44d1fde
Author: mattjackson220 <33...@users.noreply.github.com>
AuthorDate: Tue May 4 09:54:41 2021 -0600

    URL Sig Keys in Postgres and DELETE methods (#5801)
    
    * URL Sig Keys in Postgres and DELETE methods
    
    * updated per comments
    
    * return error for delete
    
    * added API doc for new DELETE
    
    * fixed copy error
    
    * added Changelog entry for URL sign key delete
---
 CHANGELOG.md                                       |   1 +
 docs/source/api/v4/deliveryservices_id_urlkeys.rst |  33 +++++++
 .../v4/deliveryservices_xmlid_xmlid_urlkeys.rst    |  33 +++++++
 .../testing/api/v4/deliveryservices_test.go        |  59 +++++++++++
 .../traffic_ops_golang/deliveryservice/urlkey.go   | 109 +++++++++++++++++++++
 traffic_ops/traffic_ops_golang/routing/routes.go   |   2 +
 .../trafficvault/backends/disabled/disabled.go     |   4 +
 .../trafficvault/backends/postgres/postgres.go     |  25 ++++-
 .../trafficvault/backends/postgres/url_sig_keys.go |  82 ++++++++++++++++
 .../trafficvault/backends/riaksvc/dsutil.go        |  12 +++
 .../trafficvault/backends/riaksvc/riak.go          |   4 +
 .../trafficvault/trafficvault.go                   |   3 +
 traffic_ops/v4-client/deliveryservice.go           |  22 +++++
 13 files changed, 387 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d38e85d..e82b7ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Add a Federation to the Ansible Dataset Loader
 - Added asynchronous status to ACME certificate generation.
 - Added headers to Traffic Portal, Traffic Ops, and Traffic Monitor to opt out of tracking users via Google FLoC.
+- `DELETE` request method for `deliveryservices/xmlId/{name}/urlkeys` and `deliveryservices/{id}/urlkeys`.
 
 ### Fixed
 - [#5690](https://github.com/apache/trafficcontrol/issues/5690) - Fixed github action for added/modified db migration file.
diff --git a/docs/source/api/v4/deliveryservices_id_urlkeys.rst b/docs/source/api/v4/deliveryservices_id_urlkeys.rst
index 3fa07ea..e5e42b4 100644
--- a/docs/source/api/v4/deliveryservices_id_urlkeys.rst
+++ b/docs/source/api/v4/deliveryservices_id_urlkeys.rst
@@ -91,3 +91,36 @@ Response Structure
 			"key15": "...",
 		}
 	}
+
+
+``DELETE``
+==========
+.. seealso:: :ref:`to-api-deliveryservices-xmlid-xmlid-urlkeys`
+
+Deletes URL signing keys for a :term:`Delivery Service`.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Response Type:  Object
+
+Request Structure
+-----------------
+.. table:: Request Path Parameters
+
+	+------+----------------------------------------------------------------------------------------+
+	| Name | Description                                                                            |
+	+======+========================================================================================+
+	| id   | Filter for the :term:`Delivery Service` identified by this integral, unique identifier |
+	+------+----------------------------------------------------------------------------------------+
+
+Response Structure
+------------------
+.. code-block:: json
+	:caption: Response Example
+
+	{
+		"alerts": [{
+			"level": "success",
+			"text": "Successfully deleted URL Sig keys from Traffic Vault"
+		}]
+	}
diff --git a/docs/source/api/v4/deliveryservices_xmlid_xmlid_urlkeys.rst b/docs/source/api/v4/deliveryservices_xmlid_xmlid_urlkeys.rst
index f50aee9..03dca83 100644
--- a/docs/source/api/v4/deliveryservices_xmlid_xmlid_urlkeys.rst
+++ b/docs/source/api/v4/deliveryservices_xmlid_xmlid_urlkeys.rst
@@ -66,3 +66,36 @@ Response Structure
 		"key14":"DtXsu8nsw04YhT0kNoKBhu2G3P9WRpQJ",
 		"key7":"cmKoIIxXGAxUMdCsWvnGLoIMGmNiuT5I"
 	}}
+
+
+``DELETE``
+==========
+.. seealso:: :ref:`to-api-deliveryservices-id-urlkeys`
+
+Deletes URL signing keys for a :term:`Delivery Service`.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Response Type:  Object
+
+Request Structure
+-----------------
+.. table:: Request Path Parameters
+
+	+-------+------------------------------------------------------+
+	|  Name |              Description                             |
+	+=======+======================================================+
+	| xmlid | The 'xml_id' of the desired :term:`Delivery Service` |
+	+-------+------------------------------------------------------+
+
+Response Structure
+------------------
+.. code-block:: json
+	:caption: Response Example
+
+	{
+		"alerts": [{
+			"level": "success",
+			"text": "Successfully deleted URL Sig keys from Traffic Vault"
+		}]
+	}
diff --git a/traffic_ops/testing/api/v4/deliveryservices_test.go b/traffic_ops/testing/api/v4/deliveryservices_test.go
index 80ced6f..439e7fc 100644
--- a/traffic_ops/testing/api/v4/deliveryservices_test.go
+++ b/traffic_ops/testing/api/v4/deliveryservices_test.go
@@ -44,7 +44,9 @@ func TestDeliveryServices(t *testing.T) {
 
 		if includeSystemTests {
 			SSLDeliveryServiceCDNUpdateTest(t)
+			CreateTestDeliveryServicesURLSigKeys(t)
 			GetTestDeliveryServicesURLSigKeys(t)
+			DeleteTestDeliveryServicesURLSigKeys(t)
 		}
 
 		GetTestDeliveryServicesIMS(t)
@@ -1374,6 +1376,63 @@ func GetTestDeliveryServicesURLSigKeys(t *testing.T) {
 	}
 }
 
+func CreateTestDeliveryServicesURLSigKeys(t *testing.T) {
+	if len(testData.DeliveryServices) == 0 {
+		t.Fatal("couldn't get the xml ID of test DS")
+	}
+	firstDS := testData.DeliveryServices[0]
+	if firstDS.XMLID == nil {
+		t.Fatal("couldn't get the xml ID of test DS")
+	}
+
+	_, _, err := TOSession.CreateDeliveryServiceURLSigKeys(*firstDS.XMLID, nil)
+	if err != nil {
+		t.Error("failed to create url sig keys: " + err.Error())
+	}
+
+	firstKeys, _, err := TOSession.GetDeliveryServiceURLSigKeys(*firstDS.XMLID, nil)
+	if err != nil {
+		t.Error("failed to get url sig keys: " + err.Error())
+	}
+	if len(firstKeys) == 0 {
+		t.Errorf("failed to create url sig keys")
+	}
+
+	// Create new keys again and check that they are different
+	_, _, err = TOSession.CreateDeliveryServiceURLSigKeys(*firstDS.XMLID, nil)
+	if err != nil {
+		t.Error("failed to create url sig keys: " + err.Error())
+	}
+
+	secondKeys, _, err := TOSession.GetDeliveryServiceURLSigKeys(*firstDS.XMLID, nil)
+	if err != nil {
+		t.Error("failed to get url sig keys: " + err.Error())
+	}
+	if len(secondKeys) == 0 {
+		t.Errorf("failed to create url sig keys")
+	}
+
+	if secondKeys["key0"] == firstKeys["key0"] {
+		t.Errorf("second create did not generate new url sig keys")
+	}
+}
+
+func DeleteTestDeliveryServicesURLSigKeys(t *testing.T) {
+	if len(testData.DeliveryServices) == 0 {
+		t.Fatal("couldn't get the xml ID of test DS")
+	}
+	firstDS := testData.DeliveryServices[0]
+	if firstDS.XMLID == nil {
+		t.Fatal("couldn't get the xml ID of test DS")
+	}
+
+	_, _, err := TOSession.DeleteDeliveryServiceURLSigKeys(*firstDS.XMLID, nil)
+	if err != nil {
+		t.Error("failed to delete url sig keys: " + err.Error())
+	}
+
+}
+
 func GetDeliveryServiceByLogsEnabled(t *testing.T) {
 	if len(testData.DeliveryServices) > 0 {
 		firstDS := testData.DeliveryServices[0]
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/urlkey.go b/traffic_ops/traffic_ops_golang/deliveryservice/urlkey.go
index 974d05a..bea441b 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/urlkey.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/urlkey.go
@@ -33,6 +33,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
 )
 
+// GetURLKeysByID returns the URL sig keys for a delivery service identified by the id in the path parameter.
 func GetURLKeysByID(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
 	if userErr != nil || sysErr != nil {
@@ -85,6 +86,7 @@ func GetURLKeysByID(w http.ResponseWriter, r *http.Request) {
 	api.WriteResp(w, r, keys)
 }
 
+// GetURLKeysByName returns the URL sig keys for a delivery service identified by the xmlId in the path parameter.
 func GetURLKeysByName(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
 	if userErr != nil || sysErr != nil {
@@ -129,6 +131,8 @@ func GetURLKeysByName(w http.ResponseWriter, r *http.Request) {
 	api.WriteResp(w, r, keys)
 }
 
+// CopyURLKeys copies the URL sig keys from a delivery service in the 'copy-name' path parameter
+// to the delivery service in the 'name' path parameter.
 func CopyURLKeys(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name", "copy-name"}, nil)
 	if userErr != nil || sysErr != nil {
@@ -208,6 +212,7 @@ func CopyURLKeys(w http.ResponseWriter, r *http.Request) {
 	api.WriteRespAlert(w, r, tc.SuccessLevel, "Successfully copied and stored keys")
 }
 
+// GenerateURLKeys generates new URL sig keys for the delivery service identified by the xmlId in the path parameter.
 func GenerateURLKeys(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
 	if userErr != nil || sysErr != nil {
@@ -263,6 +268,7 @@ func GenerateURLKeys(w http.ResponseWriter, r *http.Request) {
 	api.WriteRespAlert(w, r, tc.SuccessLevel, "Successfully generated and stored keys")
 }
 
+// GenerateURLSigKeys generates new URL sig keys.
 func GenerateURLSigKeys() (tc.URLSigKeys, error) {
 	chars := `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_`
 	numKeys := 16
@@ -289,3 +295,106 @@ func GenerateURLSigKeys() (tc.URLSigKeys, error) {
 	}
 	return keys, nil
 }
+
+// DeleteURLKeysByID deletes the URL sig keys for the delivery service identified by the id in the path parameter.
+func DeleteURLKeysByID(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()
+
+	if !inf.Config.TrafficVaultEnabled {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, userErr, errors.New("deliveryservice.DeleteURLKeysByID: Traffic Vault is not configured"))
+		return
+	}
+
+	dsId := inf.IntParams["id"]
+	ds, ok, err := dbhelpers.GetDSNameFromID(inf.Tx.Tx, dsId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting delivery service name from ID: "+err.Error()))
+		return
+	}
+	if !ok {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("delivery service "+inf.Params["id"]+" not found"), nil)
+		return
+	}
+
+	dsTenantID, ok, err := getDSTenantIDByID(inf.Tx.Tx, inf.IntParams["id"])
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
+		return
+	}
+	if !ok {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("delivery service "+inf.Params["id"]+" not found"), nil)
+		return
+	}
+	if authorized, err := tenant.IsResourceAuthorizedToUserTx(*dsTenantID, inf.User, inf.Tx.Tx); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
+		return
+	} else if !authorized {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
+	}
+
+	err = inf.Vault.DeleteURLSigKeys(string(ds), inf.Tx.Tx, r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deleting URL Sig keys from Traffic Vault: "+err.Error()))
+		return
+	}
+	api.CreateChangeLogRawTx(api.ApiChange, "DS: "+string(ds)+", ID: "+strconv.Itoa(dsId)+", ACTION: Deleted URL sig keys", inf.User, inf.Tx.Tx)
+	api.WriteRespAlert(w, r, tc.SuccessLevel, "Successfully deleted URL Sig keys from Traffic Vault")
+}
+
+// DeleteURLKeysByName deletes the URL sig keys for the delivery service identified by the xmlId in the path parameter.
+func DeleteURLKeysByName(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	if !inf.Config.TrafficVaultEnabled {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, userErr, errors.New("deliveryservice.DeleteURLKeysByName: Traffic Vault is not configured"))
+		return
+	}
+
+	ds := tc.DeliveryServiceName(inf.Params["name"])
+
+	dsTenantID, ok, err := getDSTenantIDByName(inf.Tx.Tx, ds)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
+		return
+	}
+	if !ok {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("delivery service "+string(ds)+" not found"), nil)
+		return
+	}
+	if authorized, err := tenant.IsResourceAuthorizedToUserTx(*dsTenantID, inf.User, inf.Tx.Tx); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
+		return
+	} else if !authorized {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
+	}
+
+	dsId, ok, err := getDSIDFromName(inf.Tx.Tx, string(ds))
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deliveryservice.DeleteURLKeysByName: getting DS ID from name "+err.Error()))
+		return
+	} else if !ok {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("no DS with name "+string(ds)), nil)
+		return
+	}
+
+	err = inf.Vault.DeleteURLSigKeys(string(ds), inf.Tx.Tx, r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deleting URL Sig keys from Traffic Vault: "+err.Error()))
+		return
+	}
+
+	api.CreateChangeLogRawTx(api.ApiChange, "DS: "+string(ds)+", ID: "+strconv.Itoa(dsId)+", ACTION: Deleted URL sig keys", inf.User, inf.Tx.Tx)
+	api.WriteRespAlert(w, r, tc.SuccessLevel, "Successfully deleted URL Sig keys from Traffic Vault")
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index aaf2bb5..7c44a66 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -500,7 +500,9 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{api.Version{Major: 4, Minor: 0}, http.MethodPost, `deliveryservices/xmlId/{name}/urlkeys/copyFromXmlId/{copy-name}/?$`, deliveryservice.CopyURLKeys, auth.PrivLevelOperations, Authenticated, nil, 42625010763},
 		{api.Version{Major: 4, Minor: 0}, http.MethodPost, `deliveryservices/xmlId/{name}/urlkeys/generate/?$`, deliveryservice.GenerateURLKeys, auth.PrivLevelOperations, Authenticated, nil, 45304828243},
 		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `deliveryservices/xmlId/{name}/urlkeys/?$`, deliveryservice.GetURLKeysByName, auth.PrivLevelReadOnly, Authenticated, nil, 42027192113},
+		{api.Version{Major: 4, Minor: 0}, http.MethodDelete, `deliveryservices/xmlId/{name}/urlkeys/?$`, deliveryservice.DeleteURLKeysByName, auth.PrivLevelOperations, Authenticated, nil, 42027192114},
 		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `deliveryservices/{id}/urlkeys/?$`, deliveryservice.GetURLKeysByID, auth.PrivLevelReadOnly, Authenticated, nil, 4931971143},
+		{api.Version{Major: 4, Minor: 0}, http.MethodDelete, `deliveryservices/{id}/urlkeys/?$`, deliveryservice.DeleteURLKeysByID, auth.PrivLevelOperations, Authenticated, nil, 4931971144},
 
 		//Delivery service LetsEncrypt
 		{api.Version{Major: 4, Minor: 0}, http.MethodPost, `deliveryservices/sslkeys/generate/letsencrypt/?$`, deliveryservice.GenerateLetsEncryptCertificates, auth.PrivLevelOperations, Authenticated, nil, 4534390523},
diff --git a/traffic_ops/traffic_ops_golang/trafficvault/backends/disabled/disabled.go b/traffic_ops/traffic_ops_golang/trafficvault/backends/disabled/disabled.go
index 81dda37..d18d68b 100644
--- a/traffic_ops/traffic_ops_golang/trafficvault/backends/disabled/disabled.go
+++ b/traffic_ops/traffic_ops_golang/trafficvault/backends/disabled/disabled.go
@@ -81,6 +81,10 @@ func (d *Disabled) PutURLSigKeys(xmlID string, keys tc.URLSigKeys, tx *sql.Tx, c
 	return disabledErr
 }
 
+func (d *Disabled) DeleteURLSigKeys(xmlID string, tx *sql.Tx, ctx context.Context) error {
+	return disabledErr
+}
+
 func (d *Disabled) GetURISigningKeys(xmlID string, tx *sql.Tx, ctx context.Context) ([]byte, bool, error) {
 	return nil, false, disabledErr
 }
diff --git a/traffic_ops/traffic_ops_golang/trafficvault/backends/postgres/postgres.go b/traffic_ops/traffic_ops_golang/trafficvault/backends/postgres/postgres.go
index 6dea910..9b1a554 100644
--- a/traffic_ops/traffic_ops_golang/trafficvault/backends/postgres/postgres.go
+++ b/traffic_ops/traffic_ops_golang/trafficvault/backends/postgres/postgres.go
@@ -133,11 +133,32 @@ func (p *Postgres) DeleteDNSSECKeys(cdnName string, tx *sql.Tx, ctx context.Cont
 }
 
 func (p *Postgres) GetURLSigKeys(xmlID string, tx *sql.Tx, ctx context.Context) (tc.URLSigKeys, bool, error) {
-	return tc.URLSigKeys{}, false, notImplementedErr
+	tvTx, dbCtx, cancelFunc, err := p.beginTransaction(ctx)
+	if err != nil {
+		return tc.URLSigKeys{}, false, err
+	}
+	defer p.commitTransaction(tvTx, dbCtx, cancelFunc)
+	return getURLSigKeys(xmlID, tvTx, ctx)
 }
 
 func (p *Postgres) PutURLSigKeys(xmlID string, keys tc.URLSigKeys, tx *sql.Tx, ctx context.Context) error {
-	return notImplementedErr
+	tvTx, dbCtx, cancelFunc, err := p.beginTransaction(ctx)
+	if err != nil {
+		return err
+	}
+	defer p.commitTransaction(tvTx, dbCtx, cancelFunc)
+
+	return putURLSigKeys(xmlID, tvTx, keys, ctx)
+}
+
+func (p *Postgres) DeleteURLSigKeys(xmlID string, tx *sql.Tx, ctx context.Context) error {
+	tvTx, dbCtx, cancelFunc, err := p.beginTransaction(ctx)
+	if err != nil {
+		return err
+	}
+	defer p.commitTransaction(tvTx, dbCtx, cancelFunc)
+
+	return deleteURLSigKeys(xmlID, tvTx, ctx)
 }
 
 func (p *Postgres) GetURISigningKeys(xmlID string, tx *sql.Tx, ctx context.Context) ([]byte, bool, error) {
diff --git a/traffic_ops/traffic_ops_golang/trafficvault/backends/postgres/url_sig_keys.go b/traffic_ops/traffic_ops_golang/trafficvault/backends/postgres/url_sig_keys.go
new file mode 100644
index 0000000..93d88cc
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/trafficvault/backends/postgres/url_sig_keys.go
@@ -0,0 +1,82 @@
+package postgres
+
+/*
+ * 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 (
+	"context"
+	"database/sql"
+	"encoding/json"
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+
+	"github.com/jmoiron/sqlx"
+)
+
+func getURLSigKeys(xmlID string, tvTx *sqlx.Tx, ctx context.Context) (tc.URLSigKeys, bool, error) {
+	var jsonUrlKeys string
+	if err := tvTx.QueryRow("SELECT data FROM url_sig_key WHERE deliveryservice = $1", xmlID).Scan(&jsonUrlKeys); err != nil {
+		if err == sql.ErrNoRows {
+			return tc.URLSigKeys{}, false, nil
+		}
+		e := checkErrWithContext("Traffic Vault PostgreSQL: executing SELECT URL Sig Keys query", err, ctx.Err())
+		return tc.URLSigKeys{}, false, e
+	}
+
+	urlSignKey := tc.URLSigKeys{}
+	err := json.Unmarshal([]byte(jsonUrlKeys), &urlSignKey)
+	if err != nil {
+		return tc.URLSigKeys{}, false, errors.New("unmarshalling keys: " + err.Error())
+	}
+
+	return urlSignKey, true, nil
+}
+
+func putURLSigKeys(xmlID string, tvTx *sqlx.Tx, keys tc.URLSigKeys, ctx context.Context) error {
+	keyJSON, err := json.Marshal(&keys)
+	if err != nil {
+		return errors.New("marshalling keys: " + err.Error())
+	}
+
+	// Delete old keys first if they exist
+	if err = deleteURLSigKeys(xmlID, tvTx, ctx); err != nil {
+		return err
+	}
+
+	res, err := tvTx.Exec("INSERT INTO url_sig_key (deliveryservice, data) VALUES ($1, $2)", xmlID, keyJSON)
+	if err != nil {
+		e := checkErrWithContext("Traffic Vault PostgreSQL: executing INSERT URL Sig Keys query", err, ctx.Err())
+		return e
+	}
+	if rowsAffected, err := res.RowsAffected(); err != nil {
+		return err
+	} else if rowsAffected == 0 {
+		return errors.New("URL Sign Keys: no keys were inserted")
+	}
+	return nil
+}
+
+func deleteURLSigKeys(xmlID string, tvTx *sqlx.Tx, ctx context.Context) error {
+	if _, err := tvTx.Exec("DELETE FROM url_sig_key WHERE deliveryservice = $1", xmlID); err != nil {
+		e := checkErrWithContext("Traffic Vault PostgreSQL: executing DELETE URL Sig Keys query", err, ctx.Err())
+		return e
+	}
+	return nil
+}
diff --git a/traffic_ops/traffic_ops_golang/trafficvault/backends/riaksvc/dsutil.go b/traffic_ops/traffic_ops_golang/trafficvault/backends/riaksvc/dsutil.go
index 5c951ac..203f9a7 100644
--- a/traffic_ops/traffic_ops_golang/trafficvault/backends/riaksvc/dsutil.go
+++ b/traffic_ops/traffic_ops_golang/trafficvault/backends/riaksvc/dsutil.go
@@ -330,6 +330,18 @@ func putURLSigKeys(tx *sql.Tx, authOpts *riak.AuthOptions, riakPort *uint, ds tc
 	return err
 }
 
+func deleteURLSigningKeys(tx *sql.Tx, authOpts *riak.AuthOptions, riakPort *uint, ds tc.DeliveryServiceName) error {
+	cluster, err := getPooledCluster(tx, authOpts, riakPort)
+	if err != nil {
+		return errors.New("getting pooled Riak cluster: " + err.Error())
+	}
+	key := getURLSigConfigFileName(ds)
+	if err := deleteObject(key, urlSigKeysBucket, cluster); err != nil {
+		return errors.New("deleting object: " + err.Error())
+	}
+	return nil
+}
+
 const sslKeysIndex = "sslkeys"
 const cdnSSLKeysLimit = 1000 // TODO: emulates Perl; reevaluate?
 
diff --git a/traffic_ops/traffic_ops_golang/trafficvault/backends/riaksvc/riak.go b/traffic_ops/traffic_ops_golang/trafficvault/backends/riaksvc/riak.go
index 06069b4..e7fb923 100644
--- a/traffic_ops/traffic_ops_golang/trafficvault/backends/riaksvc/riak.go
+++ b/traffic_ops/traffic_ops_golang/trafficvault/backends/riaksvc/riak.go
@@ -81,6 +81,10 @@ func (r *Riak) PutURLSigKeys(xmlID string, keys tc.URLSigKeys, tx *sql.Tx, ctx c
 	return putURLSigKeys(tx, &r.cfg.AuthOptions, &r.cfg.Port, tc.DeliveryServiceName(xmlID), keys)
 }
 
+func (r *Riak) DeleteURLSigKeys(xmlID string, tx *sql.Tx, ctx context.Context) error {
+	return deleteURLSigningKeys(tx, &r.cfg.AuthOptions, &r.cfg.Port, tc.DeliveryServiceName(xmlID))
+}
+
 func (r *Riak) GetURISigningKeys(xmlID string, tx *sql.Tx, ctx context.Context) ([]byte, bool, error) {
 	return getURISigningKeys(tx, &r.cfg.AuthOptions, &r.cfg.Port, xmlID)
 }
diff --git a/traffic_ops/traffic_ops_golang/trafficvault/trafficvault.go b/traffic_ops/traffic_ops_golang/trafficvault/trafficvault.go
index fb63067..08031d8 100644
--- a/traffic_ops/traffic_ops_golang/trafficvault/trafficvault.go
+++ b/traffic_ops/traffic_ops_golang/trafficvault/trafficvault.go
@@ -69,6 +69,9 @@ type TrafficVault interface {
 	// PutURLSigKeys stores the given URL sig keys for the delivery service identified by
 	// the given xmlID.
 	PutURLSigKeys(xmlID string, keys tc.URLSigKeys, tx *sql.Tx, ctx context.Context) error
+	// DeleteURLSigKeys deletes the URL sig keys for the delivery service identified
+	// by the given xmlID.
+	DeleteURLSigKeys(xmlID string, tx *sql.Tx, ctx context.Context) error
 	// GetURISigningKeys retrieves the URI signing keys (as raw JSON bytes) for the delivery
 	// service identified by the given xmlID.
 	GetURISigningKeys(xmlID string, tx *sql.Tx, ctx context.Context) ([]byte, bool, error)
diff --git a/traffic_ops/v4-client/deliveryservice.go b/traffic_ops/v4-client/deliveryservice.go
index ae6daee..e5887a1 100644
--- a/traffic_ops/v4-client/deliveryservice.go
+++ b/traffic_ops/v4-client/deliveryservice.go
@@ -85,6 +85,12 @@ const (
 	// (namely the XMLID of the Delivery Service of interest).
 	APIDeliveryServicesURLSigKeys = APIDeliveryServices + "/xmlId/%s/urlkeys"
 
+	// APIDeliveryServicesURLSigKeysGenerate is the API path on which Traffic Ops provides
+	// functionality to generate new URL-signing keys used by a Delivery Service identified
+	// by its XMLID. It is intended to be used with fmt.Sprintf to insert its required path parameter
+	// (namely the XMLID of the Delivery Service of interest).
+	APIDeliveryServicesURLSigKeysGenerate = APIDeliveryServices + "/xmlId/%s/urlkeys/generate"
+
 	// APIDeliveryServicesRegexes is the API path on which Traffic Ops serves Delivery Service
 	// 'regex' (Regular Expression) information.
 	APIDeliveryServicesRegexes = "/deliveryservices_regexes"
@@ -329,6 +335,22 @@ func (to *Session) GetDeliveryServiceURLSigKeys(dsName string, header http.Heade
 	return data.Response, reqInf, nil
 }
 
+// CreateDeliveryServiceURLSigKeys creates new URL-signing keys used by the Delivery Service
+// identified by the XMLID 'dsName'
+func (to *Session) CreateDeliveryServiceURLSigKeys(dsName string, header http.Header) (tc.Alerts, toclientlib.ReqInf, error) {
+	var alerts tc.Alerts
+	reqInf, err := to.post(fmt.Sprintf(APIDeliveryServicesURLSigKeysGenerate, dsName), nil, header, &alerts)
+	return alerts, reqInf, err
+}
+
+// DeleteDeliveryServiceURLSigKeys deletes the URL-signing keys used by the Delivery Service
+// identified by the XMLID 'dsName'
+func (to *Session) DeleteDeliveryServiceURLSigKeys(dsName string, header http.Header) (tc.Alerts, toclientlib.ReqInf, error) {
+	var alerts tc.Alerts
+	reqInf, err := to.del(fmt.Sprintf(APIDeliveryServicesURLSigKeys, dsName), header, &alerts)
+	return alerts, reqInf, err
+}
+
 // GetDeliveryServiceURISigningKeys returns the URI-signing keys used by the Delivery Service
 // identified by the XMLID 'dsName'. The result is not parsed.
 func (to *Session) GetDeliveryServiceURISigningKeys(dsName string, header http.Header) ([]byte, toclientlib.ReqInf, error) {