Repository: apache/trafficcontrol (go)

Issue:
/server_server_capabilities in TO API uses non-RFC3339 date/time strings
## Parent Issue: #5911

Breaking down the parent issue https://github.com/apache/trafficcontrol/issues/5911 in smaller issues to raise separate, reviewable PRs for each of the services.

## Traffic Control components affected:
- Traffic Ops
- Documentation


## Current behavior:
Statuses Using Deprecated TimeNoMod & TimeLayout

## Expected behavior:
Status  to Use RFC3339 Format

## Steps to reproduce:
`GET /api/5.0/server_server_capabilities`

Pull request:
Use RFC 3339 for lastUpdated timestamp in /server_server_capabilities
<!--
Thank you for contributing! Please be sure to read our contribution guidelines: https://github.com/apache/trafficcontrol/blob/master/CONTRIBUTING.md
If this closes or relates to an existing issue, please reference it using one of the following:

Closes: #ISSUE
Related: #ISSUE

If this PR fixes a security vulnerability, DO NOT submit! Instead, contact
the Apache Traffic Control Security Team at security@trafficcontrol.apache.org and follow the
guidelines at https://apache.org/security regarding vulnerability disclosure.
-->

This PR resolves #7743 by using the timestamp format specified in RFC 3339 for the `lastUpdated` field of the `/api/5.0/server_server_capabilities` endpoint.
<!-- **^ Add meaningful description above** --><hr/>

## Which Traffic Control components are affected by this PR?
<!-- Please delete all components from this list that are NOT affected by this PR.
Feel free to add the name of a tool or script that is affected but not on the list.
-->
- Traffic Ops

## What is the best way to verify this PR?
<!-- Please include here ALL the steps necessary to test your PR.
If your PR has tests (and most should), provide the steps needed to run the tests.
If not, please provide step-by-step instructions to test the PR manually and explain why your PR does not need tests. -->
Run the server_server_capabilities API tests

## PR submission checklist
- [x] This PR has tests <!-- If not, please delete this text and explain why this PR does not need tests. -->
- [x] This PR has documentation <!-- If not, please delete this text and explain why this PR does not need documentation. -->
- [x] This PR has a CHANGELOG.md entry <!-- A fix for a bug from an ATC release, an improvement, or a new feature should have a changelog entry. -->
- [x] This PR **DOES NOT FIX A SERIOUS SECURITY VULNERABILITY** (see [the Apache Software Foundation's security guidelines](https://apache.org/security) for details)

<!--
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.
-->

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6dd1d9e64..a435a14d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -100,6 +100,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
     - [#7691](https://github.com/apache/trafficcontrol/pull/7691) *Traffic Ops* Fixed `/topologies` v5 APIs to respond with `RFC3339` timestamps.
     - [#7413](https://github.com/apache/trafficcontrol/issues/7413) *Traffic Ops* Fixed `/service_category` v5 APIs to respond with `RFC3339` timestamps.
     - [#7413](https://github.com/apache/trafficcontrol/issues/7706) *Traffic Ops* Fixed `/statuses` v5 APIs to respond with `RFC3339` timestamps.
+- [#7743](https://github.com/apache/trafficcontrol/issues/7743) *Traffic Ops* Fixes /server_server_capabilities apis to respond with RFC3339 date/time format
 - [#7730](https://github.com/apache/trafficcontrol/pull/7730) *Traffic Monitor* Fixed the panic seen in TM when `plugin.system_stats.timestamp_ms` appears as float and not string.
 - [#4393](https://github.com/apache/trafficcontrol/issues/4393) *Traffic Ops* Fixed the error code and alert structure when TO is queried for a delivery service with no ssl keys.
 - [#7623](https://github.com/apache/trafficcontrol/pull/7623) *Traffic Ops* Removed TryIfModifiedSinceQuery from servicecategories.go and reused from ims.go
diff --git a/docs/source/api/v5/server_server_capabilities.rst b/docs/source/api/v5/server_server_capabilities.rst
index 07af380ae..9a626d96a 100644
--- a/docs/source/api/v5/server_server_capabilities.rst
+++ b/docs/source/api/v5/server_server_capabilities.rst
@@ -66,7 +66,7 @@ Response Structure
 ------------------
 :serverHostName:   The server's host name
 :serverId:         The server's integral, unique identifier
-:lastUpdated:      The date and time at which this association between the server and the :term:`Server Capability` was last updated, in :ref:`non-rfc-datetime`
+:lastUpdated:      The date and time at which this association between the server and the :term:`Server Capability` was last updated, in :rfc:`3339` format
 :serverCapability: The :term:`Server Capability`'s name
 
 .. code-block:: http
@@ -87,13 +87,13 @@ Response Structure
 	{
 		"response": [
 			{
-				"lastUpdated": "2019-10-07 22:05:31+00",
+				"lastUpdated": "2023-08-09T14:25:11.017999Z",
 				"serverHostName": "atlanta-org-1",
 				"serverId": 260,
 				"serverCapability": "ram"
 			},
 			{
-				"lastUpdated": "2019-10-07 22:05:31+00",
+				"lastUpdated": "2023-08-09T14:25:11.017999Z",
 				"serverHostName": "atlanta-org-2",
 				"serverId": 261,
 				"serverCapability": "disk"
@@ -136,7 +136,7 @@ Request Structure
 Response Structure
 ------------------
 :serverId:         The integral, unique identifier of the newly associated server
-:lastUpdated:      The date and time at which this association between the server and the :term:`Server Capability` was last updated, in :ref:`non-rfc-datetime`
+:lastUpdated:      The date and time at which this association between the server and the :term:`Server Capability` was last updated, in :rfc:`3339` format
 :serverCapability: The :term:`Server Capability`'s name
 
 .. code-block:: http
@@ -162,7 +162,7 @@ Response Structure
 			}
 		],
 		"response": {
-			"lastUpdated": "2019-10-07 22:15:11+00",
+			"lastUpdated": "2023-08-09T14:25:11.017999Z",
 			"serverId": 1,
 			"serverCapability": "disk"
 		}
diff --git a/lib/go-tc/server_server_capability.go b/lib/go-tc/server_server_capability.go
index 88bb755b8..42a4d7b25 100644
--- a/lib/go-tc/server_server_capability.go
+++ b/lib/go-tc/server_server_capability.go
@@ -19,6 +19,20 @@ package tc
  * under the License.
  */
 
+import "time"
+
+// ServerServerCapabilityV5 is a ServerServerCapability as it appears in version 5 of the
+// Traffic Ops API - it always points to the highest minor version in APIv5.
+type ServerServerCapabilityV5 = ServerServerCapabilityV50
+
+// ServerServerCapabilityV50 represents an association between a server capability and a server.
+type ServerServerCapabilityV50 struct {
+	LastUpdated      *time.Time `json:"lastUpdated" db:"last_updated"`
+	Server           *string    `json:"serverHostName,omitempty" db:"host_name"`
+	ServerID         *int       `json:"serverId" db:"server"`
+	ServerCapability *string    `json:"serverCapability" db:"server_capability"`
+}
+
 // ServerServerCapability represents an association between a server capability and a server.
 type ServerServerCapability struct {
 	LastUpdated      *TimeNoMod `json:"lastUpdated" db:"last_updated"`
@@ -35,6 +49,18 @@ type MultipleServersCapabilities struct {
 	PageType           string   `json:"pageType"`
 }
 
+// ServerServerCapabilitiesResponseV5 is the type of a response from the
+// /api/5.x/server_server_capabilities Traffic Ops endpoint.
+// It always points to the type for the latest minor version of APIv5.
+type ServerServerCapabilitiesResponseV5 = ServerServerCapabilitiesResponseV50
+
+// ServerServerCapabilitiesResponseV50 is the type of a response from Traffic
+// Ops to a request made to its /api/5.0/server_server_capabilities.
+type ServerServerCapabilitiesResponseV50 struct {
+	Response []ServerServerCapabilityV5 `json:"response"`
+	Alerts
+}
+
 // ServerServerCapabilitiesResponse is the type of a response from Traffic
 // Ops to a request made to its /server_server_capabilities.
 type ServerServerCapabilitiesResponse struct {
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 0355a2216..eec686a68 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -331,9 +331,9 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `multiple_servers_capabilities/?$`, Handler: server.DeleteMultipleServersCapabilities, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:READ", "SERVER:DELETE", "SERVER-CAPABILITY:READ", "SERVER-CAPABILITY:DELETE"}, Authenticated: Authenticated, Middlewares: nil, ID: 407924192781},
 
 		//Server Server Capabilities: CRUD
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `server_server_capabilities/?$`, Handler: api.ReadHandler(&server.TOServerServerCapability{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 480023188931},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `server_server_capabilities/?$`, Handler: api.CreateHandler(&server.TOServerServerCapability{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 429316683431},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `server_server_capabilities/?$`, Handler: api.DeleteHandler(&server.TOServerServerCapability{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 405871405831},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `server_server_capabilities/?$`, Handler: api.ReadHandler(&server.TOServerServerCapabilityV5{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 480023188931},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `server_server_capabilities/?$`, Handler: api.CreateHandler(&server.TOServerServerCapabilityV5{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 429316683431},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `server_server_capabilities/?$`, Handler: api.DeleteHandler(&server.TOServerServerCapabilityV5{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 405871405831},
 
 		//Status: CRUD
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `statuses/?$`, Handler: api.ReadHandler(&status.TOStatusV5{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"STATUS:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 424490565631},
diff --git a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
index b1a5ee56a..5c8d0c507 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go
@@ -48,6 +48,288 @@ const (
 	ServerHostNameQueryParam   = "serverHostName"
 )
 
+type TOServerServerCapabilityV5 struct {
+	api.APIInfoImpl `json:"-"`
+	tc.ServerServerCapabilityV5
+}
+
+func (ssc *TOServerServerCapabilityV5) SetLastUpdated(t tc.TimeNoMod) { ssc.LastUpdated = &t.Time }
+func (ssc *TOServerServerCapabilityV5) NewReadObj() interface{} {
+	return &tc.ServerServerCapabilityV5{}
+}
+func (ssc *TOServerServerCapabilityV5) SelectQuery() string { return scSelectQuery() }
+func (ssc *TOServerServerCapabilityV5) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
+	return map[string]dbhelpers.WhereColumnInfo{
+		ServerCapabilityQueryParam: dbhelpers.WhereColumnInfo{Column: "sc.server_capability"},
+		ServerQueryParam:           dbhelpers.WhereColumnInfo{Column: "s.id", Checker: api.IsInt},
+		ServerHostNameQueryParam:   dbhelpers.WhereColumnInfo{Column: "s.host_name"},
+	}
+
+}
+func (ssc *TOServerServerCapabilityV5) DeleteQuery() string { return scDeleteQuery() }
+func (ssc TOServerServerCapabilityV5) GetKeyFieldsInfo() []api.KeyFieldInfo {
+	return []api.KeyFieldInfo{
+		{Field: ServerQueryParam, Func: api.GetIntKey},
+		{Field: ServerCapabilityQueryParam, Func: api.GetStringKey},
+	}
+}
+
+// Need to satisfy Identifier interface but is a no-op as path does not have Update
+func (ssc TOServerServerCapabilityV5) GetKeys() (map[string]interface{}, bool) {
+	if ssc.ServerID == nil {
+		return map[string]interface{}{ServerQueryParam: 0}, false
+	}
+	if ssc.ServerCapability == nil {
+		return map[string]interface{}{ServerCapabilityQueryParam: 0}, false
+	}
+	return map[string]interface{}{
+		ServerQueryParam:           *ssc.ServerID,
+		ServerCapabilityQueryParam: *ssc.ServerCapability,
+	}, true
+}
+
+func (ssc *TOServerServerCapabilityV5) SetKeys(keys map[string]interface{}) {
+	sID, _ := keys[ServerQueryParam].(int)
+	ssc.ServerID = &sID
+
+	sc, _ := keys[ServerCapabilityQueryParam].(string)
+	ssc.ServerCapability = &sc
+}
+
+func (ssc *TOServerServerCapabilityV5) GetAuditName() string {
+	if ssc.ServerCapability != nil {
+		return *ssc.ServerCapability
+	}
+	return "unknown"
+}
+
+func (ssc *TOServerServerCapabilityV5) GetType() string {
+	return "server server_capability"
+}
+
+// Validate fulfills the api.Validator interface.
+func (ssc TOServerServerCapabilityV5) Validate() (error, error) {
+	errs := validation.Errors{
+		ServerQueryParam:           validation.Validate(ssc.ServerID, validation.Required),
+		ServerCapabilityQueryParam: validation.Validate(ssc.ServerCapability, validation.Required),
+	}
+
+	return util.JoinErrs(tovalidate.ToErrors(errs)), nil
+}
+
+func (ssc *TOServerServerCapabilityV5) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
+	api.DefaultSort(ssc.APIInfo(), "serverHostName")
+	return api.GenericRead(h, ssc, useIMS)
+}
+func (v *TOServerServerCapabilityV5) SelectMaxLastUpdatedQuery(where, orderBy, pagination, tableName string) string {
+	return `SELECT max(t) from (
+		SELECT max(sc.last_updated) as t from server_server_capability sc
+JOIN server s ON sc.server = s.id ` + where + orderBy + pagination +
+		` UNION ALL
+	select max(last_updated) as t from last_deleted l where l.table_name='server_server_capability') as res`
+}
+
+func (ssc *TOServerServerCapabilityV5) Delete() (error, error, int) {
+	tenantIDs, err := tenant.GetUserTenantIDListTx(ssc.APIInfo().Tx.Tx, ssc.APIInfo().User.TenantID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting servers_server_capability: %v", err), http.StatusInternalServerError
+	}
+	accessibleTenants := make(map[int]struct{}, len(tenantIDs))
+	for _, id := range tenantIDs {
+		accessibleTenants[id] = struct{}{}
+	}
+	userErr, sysErr, status := checkTopologyBasedDSRequiredCapabilitiesV5(ssc, accessibleTenants)
+	if userErr != nil || sysErr != nil {
+		return userErr, sysErr, status
+	}
+
+	userErr, sysErr, status = checkDSRequiredCapabilitiesV5(ssc, accessibleTenants)
+	if userErr != nil || sysErr != nil {
+		return userErr, sysErr, status
+	}
+
+	if ssc.ServerID != nil {
+		cdnName, err := dbhelpers.GetCDNNameFromServerID(ssc.APIInfo().Tx.Tx, int64(*ssc.ServerID))
+		if err != nil {
+			return nil, err, http.StatusInternalServerError
+		}
+		userErr, sysErr, errCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(ssc.APIInfo().Tx.Tx, string(cdnName), ssc.APIInfo().User.UserName)
+		if userErr != nil || sysErr != nil {
+			return userErr, sysErr, errCode
+		}
+	}
+	return api.GenericDelete(ssc)
+}
+
+func checkTopologyBasedDSRequiredCapabilitiesV5(ssc *TOServerServerCapabilityV5, accessibleTenants map[int]struct{}) (error, error, int) {
+	dsRows, err := ssc.APIInfo().Tx.Tx.Query(getTopologyBasedDSesReqCapQuery(), ssc.ServerID, ssc.ServerCapability)
+	if err != nil {
+		return nil, fmt.Errorf("querying topology-based DSes with the required capability %s: %v", *ssc.ServerCapability, err), http.StatusInternalServerError
+	}
+	defer log.Close(dsRows, "closing dsRows in checkTopologyBasedDSRequiredCapabilitiesV5")
+
+	xmlidToTopology := make(map[string]string)
+	xmlidToTenantID := make(map[string]int)
+	xmlidToReqCaps := make(map[string][]string)
+	for dsRows.Next() {
+		xmlID := ""
+		topology := ""
+		tenantID := 0
+		reqCaps := []string{}
+		if err := dsRows.Scan(&xmlID, &topology, &tenantID, pq.Array(&reqCaps)); err != nil {
+			return nil, fmt.Errorf("scanning dsRows in checkTopologyBasedDSRequiredCapabilitiesV5: %v", err), http.StatusInternalServerError
+		}
+		xmlidToTenantID[xmlID] = tenantID
+		xmlidToTopology[xmlID] = topology
+		xmlidToReqCaps[xmlID] = reqCaps
+	}
+	if len(xmlidToTopology) == 0 {
+		return nil, nil, http.StatusOK
+	}
+
+	serverRows, err := ssc.APIInfo().Tx.Tx.Query(getServerCapabilitiesOfCachegoupQuery(), ssc.ServerID, ssc.ServerCapability)
+	if err != nil {
+		return nil, fmt.Errorf("querying server capabilitites of server %d's cachegroup: %v", *ssc.ServerID, err), http.StatusInternalServerError
+	}
+	defer log.Close(serverRows, "closing serverRows in checkTopologyBasedDSRequiredCapabilitiesV5")
+
+	serverIDToCapabilities := make(map[int]map[string]struct{})
+	for serverRows.Next() {
+		serverID := 0
+		capabilities := []string{}
+		if err := serverRows.Scan(&serverID, pq.Array(&capabilities)); err != nil {
+			return nil, fmt.Errorf("scanning serverRows in checkTopologyBasedDSRequiredCapabilitiesV5: %v", err), http.StatusInternalServerError
+		}
+		serverIDToCapabilities[serverID] = make(map[string]struct{})
+		for _, c := range capabilities {
+			serverIDToCapabilities[serverID][c] = struct{}{}
+		}
+	}
+
+	unsatisfiedDSes := []string{}
+	for ds, dsReqCaps := range xmlidToReqCaps {
+		dsIsSatisfied := false
+		for _, serverCaps := range serverIDToCapabilities {
+			serverHasCapabilities := true
+			for _, dsReqCap := range dsReqCaps {
+				if _, ok := serverCaps[dsReqCap]; !ok {
+					serverHasCapabilities = false
+					break
+				}
+			}
+			if serverHasCapabilities {
+				dsIsSatisfied = true
+				break
+			}
+		}
+		if !dsIsSatisfied {
+			unsatisfiedDSes = append(unsatisfiedDSes, ds)
+		}
+	}
+	if len(unsatisfiedDSes) == 0 {
+		return nil, nil, http.StatusOK
+	}
+
+	dsStrings := make([]string, 0, len(unsatisfiedDSes))
+	for _, ds := range unsatisfiedDSes {
+		if _, ok := accessibleTenants[xmlidToTenantID[ds]]; ok {
+			dsStrings = append(dsStrings, "(xml_id = "+ds+", topology = "+xmlidToTopology[ds]+")")
+		}
+	}
+	return fmt.Errorf("this capability is required by delivery services, but there are no other servers in this server's cachegroup to satisfy them %s", strings.Join(dsStrings, ", ")), nil, http.StatusBadRequest
+}
+
+func checkDSRequiredCapabilitiesV5(ssc *TOServerServerCapabilityV5, accessibleTenants map[int]struct{}) (error, error, int) {
+	// Ensure that the user is not removing a server capability from the server
+	// that is required by the delivery services the server is assigned to (if applicable)
+	dsIDs := []int64{}
+	if err := ssc.APIInfo().Tx.Tx.QueryRow(checkDSReqCapQuery(), ssc.ServerID, ssc.ServerCapability).Scan(pq.Array(&dsIDs)); err != nil {
+		return nil, fmt.Errorf("checking removing server server capability would still suffice delivery service requried capabilites: %v", err), http.StatusInternalServerError
+	}
+
+	if len(dsIDs) > 0 {
+		return ssc.buildDSReqCapError(dsIDs, accessibleTenants)
+	}
+	return nil, nil, http.StatusOK
+}
+
+func (ssc *TOServerServerCapabilityV5) buildDSReqCapError(dsIDs []int64, accessibleTenants map[int]struct{}) (error, error, int) {
+
+	dsTenantIDs, err := getDSTenantIDsByIDs(ssc.APIInfo().Tx, dsIDs)
+	if err != nil {
+		return nil, err, http.StatusInternalServerError
+	}
+
+	authDSIDs := []string{}
+
+	for _, dsTenantID := range dsTenantIDs {
+		if _, ok := accessibleTenants[dsTenantID.TenantID]; ok {
+			if ok {
+				authDSIDs = append(authDSIDs, strconv.Itoa(dsTenantID.ID))
+			}
+			continue
+		}
+	}
+
+	dsStr := "delivery services"
+	if len(authDSIDs) > 0 {
+		dsStr = fmt.Sprintf("the delivery services %v", strings.Join(authDSIDs, ","))
+	}
+	return fmt.Errorf("cannot remove the capability %v from the server %v as the server is assigned to %v that require it", *ssc.ServerCapability, *ssc.ServerID, dsStr), nil, http.StatusBadRequest
+}
+
+func (ssc *TOServerServerCapabilityV5) Create() (error, error, int) {
+	tx := ssc.APIInfo().Tx
+
+	// Check existence prior to checking type
+	_, exists, err := dbhelpers.GetServerNameFromID(tx.Tx, int64(*ssc.ServerID))
+	if err != nil {
+		return nil, err, http.StatusInternalServerError
+	}
+	if !exists {
+		return fmt.Errorf("server %v does not exist", *ssc.ServerID), nil, http.StatusNotFound
+	}
+
+	// Ensure type is correct
+	var sidList []int64
+	sidList = append(sidList, int64(*ssc.ServerID))
+	errCode, userErr, sysErr := checkServerType(tx.Tx, sidList)
+	if userErr != nil || sysErr != nil {
+		return userErr, sysErr, errCode
+
+	}
+
+	cdnName, err := dbhelpers.GetCDNNameFromServerID(tx.Tx, int64(*ssc.ServerID))
+	if err != nil {
+		return nil, err, http.StatusInternalServerError
+	}
+	userErr, sysErr, errCode = dbhelpers.CheckIfCurrentUserCanModifyCDN(tx.Tx, string(cdnName), ssc.APIInfo().User.UserName)
+	if userErr != nil || sysErr != nil {
+		return userErr, sysErr, errCode
+	}
+
+	resultRows, err := tx.NamedQuery(scInsertQuery(), ssc)
+	if err != nil {
+		return api.ParseDBError(err)
+	}
+	defer resultRows.Close()
+
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err := resultRows.StructScan(&ssc); err != nil {
+			return nil, errors.New(ssc.GetType() + " create scanning: " + err.Error()), http.StatusInternalServerError
+		}
+	}
+	if rowsAffected == 0 {
+		return nil, errors.New(ssc.GetType() + " create: no " + ssc.GetType() + " was inserted, no rows was returned"), http.StatusInternalServerError
+	} else if rowsAffected > 1 {
+		return nil, errors.New("too many rows returned from " + ssc.GetType() + " insert"), http.StatusInternalServerError
+	}
+
+	return nil, nil, http.StatusOK
+}
+
 type (
 	TOServerServerCapability struct {
 		api.APIInfoImpl `json:"-"`
diff --git a/traffic_ops/v5-client/server_server_capabilities.go b/traffic_ops/v5-client/server_server_capabilities.go
index 15bd1efbc..d0d125e44 100644
--- a/traffic_ops/v5-client/server_server_capabilities.go
+++ b/traffic_ops/v5-client/server_server_capabilities.go
@@ -32,7 +32,7 @@ const apiServerServerCapabilities = "/server_server_capabilities"
 const apiMultipleServersCapabilities = "/multiple_servers_capabilities"
 
 // CreateServerServerCapability assigns a Server Capability to a Server.
-func (to *Session) CreateServerServerCapability(ssc tc.ServerServerCapability, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) CreateServerServerCapability(ssc tc.ServerServerCapabilityV5, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	var alerts tc.Alerts
 	reqInf, err := to.post(apiServerServerCapabilities, opts, ssc, &alerts)
 	return alerts, reqInf, err
@@ -52,8 +52,8 @@ func (to *Session) DeleteServerServerCapability(serverID int, serverCapability s
 
 // GetServerServerCapabilities retrieves a list of Server Capabilities that are
 // assigned to Servers.
-func (to *Session) GetServerServerCapabilities(opts RequestOptions) (tc.ServerServerCapabilitiesResponse, toclientlib.ReqInf, error) {
-	var resp tc.ServerServerCapabilitiesResponse
+func (to *Session) GetServerServerCapabilities(opts RequestOptions) (tc.ServerServerCapabilitiesResponseV5, toclientlib.ReqInf, error) {
+	var resp tc.ServerServerCapabilitiesResponseV5
 	reqInf, err := to.get(apiServerServerCapabilities, opts, &resp)
 	return resp, reqInf, err
 }


Test patch:
diff --git a/traffic_ops/testing/api/v5/server_server_capabilities_test.go b/traffic_ops/testing/api/v5/server_server_capabilities_test.go
index dcab7cf02..f112d34b6 100644
--- a/traffic_ops/testing/api/v5/server_server_capabilities_test.go
+++ b/traffic_ops/testing/api/v5/server_server_capabilities_test.go
@@ -38,7 +38,7 @@ func TestServerServerCapabilities(t *testing.T) {
 		currentTime := time.Now().UTC().Add(-15 * time.Second)
 		tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
 
-		methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.ServerServerCapability]{
+		methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.ServerServerCapabilityV5]{
 			"GET": {
 				"NOT MODIFIED when NO CHANGES made": {
 					ClientSession: TOSession,
@@ -102,7 +102,7 @@ func TestServerServerCapabilities(t *testing.T) {
 			"POST": {
 				"BAD REQUEST when ALREADY EXISTS": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						ServerID:         util.Ptr(GetServerID(t, "dtrc-mid-01")()),
 						ServerCapability: util.Ptr("disk"),
 					},
@@ -110,21 +110,21 @@ func TestServerServerCapabilities(t *testing.T) {
 				},
 				"BAD REQUEST when MISSING SERVER ID": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						Server: util.Ptr("disk"),
 					},
 					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
 				},
 				"BAD REQUEST when MISSING SERVER CAPABILITY": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						ServerID: util.Ptr(GetServerID(t, "dtrc-mid-01")()),
 					},
 					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
 				},
 				"NOT FOUND when SERVER CAPABILITY DOESNT EXIST": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						ServerID:         util.Ptr(GetServerID(t, "dtrc-mid-01")()),
 						ServerCapability: util.Ptr("bogus"),
 					},
@@ -132,7 +132,7 @@ func TestServerServerCapabilities(t *testing.T) {
 				},
 				"NOT FOUND when SERVER DOESNT EXIST": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						ServerID:         util.Ptr(99999999),
 						ServerCapability: util.Ptr("bogus"),
 					},
@@ -140,7 +140,7 @@ func TestServerServerCapabilities(t *testing.T) {
 				},
 				"BAD REQUEST when SERVER TYPE NOT EDGE or MID": {
 					ClientSession: TOSession,
-					RequestBody: tc.ServerServerCapability{
+					RequestBody: tc.ServerServerCapabilityV5{
 						ServerID:         util.Ptr(GetServerID(t, "trafficvault")()),
 						ServerCapability: util.Ptr("bogus"),
 					},
@@ -225,7 +225,7 @@ func TestServerServerCapabilities(t *testing.T) {
 func validateServerServerCapabilitiesFields(expectedResp map[string]interface{}) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected Server Server Capabilities response to not be nil.")
-		serverServerCapabilityResponse := resp.([]tc.ServerServerCapability)
+		serverServerCapabilityResponse := resp.([]tc.ServerServerCapabilityV5)
 		for field, expected := range expectedResp {
 			for _, serverServerCapability := range serverServerCapabilityResponse {
 				switch field {
@@ -261,7 +261,7 @@ func validateServerServerCapabilitiesSort() utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected Server Server Capabilities response to not be nil.")
 		var serverNames []string
-		serverServerCapabilityResponse := resp.([]tc.ServerServerCapability)
+		serverServerCapabilityResponse := resp.([]tc.ServerServerCapabilityV5)
 		for _, serverServerCapability := range serverServerCapabilityResponse {
 			assert.RequireNotNil(t, serverServerCapability.Server, "Expected Server to not be nil.")
 			serverNames = append(serverNames, *serverServerCapability.Server)
@@ -272,7 +272,7 @@ func validateServerServerCapabilitiesSort() utils.CkReqFunc {
 
 func validateServerServerCapabilitiesPagination(paginationParam string) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
-		paginationResp := resp.([]tc.ServerServerCapability)
+		paginationResp := resp.([]tc.ServerServerCapabilityV5)
 
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("orderby", "serverId")
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go b/traffic_ops/testing/api/v5/traffic_control_test.go
index 4154bb7f5..7d9547fa3 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -46,7 +46,7 @@ type TrafficControl struct {
 	Regions                                           []tc.RegionV5                           `json:"regions"`
 	Roles                                             []tc.RoleV4                             `json:"roles"`
 	Servers                                           []tc.ServerV5                           `json:"servers"`
-	ServerServerCapabilities                          []tc.ServerServerCapability             `json:"serverServerCapabilities"`
+	ServerServerCapabilities                          []tc.ServerServerCapabilityV5           `json:"serverServerCapabilities"`
 	ServerCapabilities                                []tc.ServerCapabilityV5                 `json:"serverCapabilities"`
 	ServiceCategories                                 []tc.ServiceCategoryV5                  `json:"serviceCategories"`
 	Statuses                                          []tc.StatusV5                           `json:"statuses"`
diff --git a/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go b/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go
index a6ee9e46b..89c7dc34a 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_server_capability_test.go
@@ -35,6 +35,185 @@ import (
 	"gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
 
+func getTestSSCsV5() []tc.ServerServerCapabilityV5 {
+	sscs := []tc.ServerServerCapabilityV5{}
+	testSSC := tc.ServerServerCapabilityV5{
+		LastUpdated:      util.Ptr(time.Now()),
+		Server:           util.StrPtr("test"),
+		ServerID:         util.IntPtr(1),
+		ServerCapability: util.StrPtr("test"),
+	}
+	sscs = append(sscs, testSSC)
+
+	testSSC1 := testSSC
+	testSSC1.ServerCapability = util.Ptr("blah")
+	sscs = append(sscs, testSSC1)
+
+	return sscs
+}
+
+func TestReadSCsV5(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("an error '%v' was not expected when opening a stub database connection", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	testSCs := getTestSSCsV5()
+	rows := sqlmock.NewRows([]string{"server_capability", "server", "last_updated"})
+
+	for _, ts := range testSCs {
+		rows = rows.AddRow(
+			ts.ServerCapability,
+			ts.ServerID,
+			ts.LastUpdated)
+	}
+	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT").WillReturnRows(rows)
+	mock.ExpectCommit()
+
+	reqInfo := api.APIInfo{Tx: db.MustBegin(), Params: map[string]string{"serverId": "1"}}
+	obj := TOServerServerCapabilityV5{
+		api.APIInfoImpl{ReqInfo: &reqInfo},
+		tc.ServerServerCapabilityV5{},
+	}
+	sscs, userErr, sysErr, _, _ := obj.Read(nil, false)
+	if userErr != nil || sysErr != nil {
+		t.Errorf("Read expected: no errors, actual: %v %v", userErr, sysErr)
+	}
+
+	if len(sscs) != 2 {
+		t.Errorf("ServerServerCapabilityV5.Read expected: len(scs) == 1, actual: %v", len(sscs))
+	}
+}
+
+func TestInterfacesV5(t *testing.T) {
+	var i interface{}
+	i = &TOServerServerCapabilityV5{}
+
+	if _, ok := i.(api.Creator); !ok {
+		t.Errorf("ServerServerCapabilityV5 must be Creator")
+	}
+	if _, ok := i.(api.Reader); !ok {
+		t.Errorf("ServerServerCapabilityV5 must be Reader")
+	}
+	if _, ok := i.(api.Deleter); !ok {
+		t.Errorf("ServerServerCapabilityV5 must be Deleter")
+	}
+	if _, ok := i.(api.Identifier); !ok {
+		t.Errorf("ServerServerCapabilityV5 must be Identifier")
+	}
+}
+
+func TestFuncsV5(t *testing.T) {
+	if strings.Index(scSelectQuery(), "SELECT") != 0 {
+		t.Errorf("expected selectQuery to start with SELECT")
+	}
+	if strings.Index(scInsertQuery(), "INSERT") != 0 {
+		t.Errorf("expected insertQuery to start with INSERT")
+	}
+	if strings.Index(scDeleteQuery(), "DELETE") != 0 {
+		t.Errorf("expected deleteQuery to start with DELETE")
+	}
+}
+
+func TestValidateV5(t *testing.T) {
+	testSSC := tc.ServerServerCapabilityV5{
+		LastUpdated:      util.Ptr(time.Now()),
+		Server:           util.StrPtr("test1"),
+		ServerID:         util.IntPtr(1),
+		ServerCapability: util.StrPtr("abc"),
+	}
+	testTOSSC := TOServerServerCapabilityV5{
+		ServerServerCapabilityV5: testSSC,
+	}
+
+	err, _ := testTOSSC.Validate()
+	errs := test.SortErrors(test.SplitErrors(err))
+
+	if len(errs) > 0 {
+		t.Errorf(`expected no errors,  got %v`, errs)
+	}
+}
+
+func TestCheckExistingServerV5(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	mock.ExpectBegin()
+	rows := sqlmock.NewRows([]string{"host_name"})
+	rows.AddRow("test")
+	mock.ExpectQuery("SELECT host_name").WithArgs(1).WillReturnRows(rows)
+
+	rows1 := sqlmock.NewRows([]string{"name"})
+	rows1.AddRow("ALL")
+	mock.ExpectQuery("SELECT name").WithArgs(1).WillReturnRows(rows1)
+
+	rows2 := sqlmock.NewRows([]string{"username", "soft", "shared_usernames"})
+	rows2.AddRow("user1", false, []byte("{}"))
+	mock.ExpectQuery("SELECT c.username, c.soft").WithArgs("ALL").WillReturnRows(rows2)
+	mock.ExpectCommit()
+
+	testSCCs := getTestSSCsV5()
+	var sids []int64
+	sids = append(sids, int64(*testSCCs[0].ServerID))
+	code, usrErr, sysErr := checkExistingServer(db.MustBegin().Tx, sids, "user1")
+	if usrErr != nil {
+		t.Errorf("server not found, error:%v", usrErr)
+	}
+	if sysErr != nil {
+		t.Errorf("unable to check if server exists, error:%v", sysErr)
+	}
+	if code != http.StatusOK {
+		t.Errorf("existing server check failed, expected:%d, got:%d", http.StatusOK, code)
+	}
+}
+
+func TestCheckServerTypeV5(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	testSCCs := getTestSSCsV5()
+	testSCCs[1].ServerID = util.Ptr(2)
+	testSCCs[1].Server = util.Ptr("foo")
+
+	mock.ExpectBegin()
+	rows := sqlmock.NewRows([]string{"array_agg"})
+	var sids []int64
+	for i, _ := range testSCCs {
+		sids = append(sids, int64(*testSCCs[i].ServerID))
+	}
+	rows.AddRow([]byte("{1,2}"))
+	mock.ExpectQuery("SELECT array_agg").WithArgs(pq.Array(sids)).WillReturnRows(rows)
+	mock.ExpectCommit()
+
+	code, usrErr, sysErr := checkServerType(db.MustBegin().Tx, sids)
+	if usrErr != nil {
+		t.Errorf("mismatch in server type, error:%v", usrErr)
+	}
+	if sysErr != nil {
+		t.Errorf("unable to check if server type exists, error:%v", sysErr)
+	}
+	if code != http.StatusOK {
+		t.Errorf("server type check failed, expected:%d, got:%d", http.StatusOK, code)
+	}
+}
+
 func getTestSSCs() []tc.ServerServerCapability {
 	sscs := []tc.ServerServerCapability{}
 	testSSC := tc.ServerServerCapability{