Repository: apache/trafficcontrol
Pull request #7695 (2023-08-03 19:53:13)

Added Api contract test case for Service categories endpoint
<!--
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.
-->

Added Api contract test case for Service categories 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 Test


## What is the best way to verify this PR?

Step 1 : Follow steps from README file(traffic_ops/testing/api_contract) to run api-contract-tests.

Step 2 : To execute API Contract Testcase , navigate to traffic_ops/testing/api_contract/v4
pytest --to-user Username --to-password Password --to-url URL --config Config Path --request-template Request template --response-template Response template 

Note: Config, Request template and response template paths are optional. 

## 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 **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 df9d6e6fa..cf9ab9393 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -145,6 +145,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - [#7628](https://github.com/apache/trafficcontrol/pull/7628) *Traffic Ops* Fixes an issue where certificate chain validation failed based on leading or trailing whitespace.
 - [#7596](https://github.com/apache/trafficcontrol/pull/7596) *Traffic Ops* Fixes `federation_resolvers` v5 apis to respond with `RFC3339` date/time Format.
 - [#7660](https://github.com/apache/trafficcontrol/pull/7660) *Traffic Ops* Fixes `deliveryServices` v5 apis to respond with `RFC3339` date/time Format.
+- [#7698](https://github.com/apache/trafficcontrol/pull/7698) *Traffic Ops* Fixes `region` v5 apis to respond with `RFC3339` date/time Format.
+- [#7686](https://github.com/apache/trafficcontrol/pull/7686) *Traffic Ops* Fixes secured parameters being visible when role has proper permissions.
+- [#7697](https://github.com/apache/trafficcontrol/pull/7697) *Traffic Ops* Fixes `iloPassword` and `xmppPassword` checking for priv-level instead of using permissions.
 
 ### Removed
 - [#7271](https://github.com/apache/trafficcontrol/pull/7271) Remove components in `infrastructre/docker/`, not in use as cdn-in-a-box performs the same functionality.
diff --git a/docs/source/api/v5/regions.rst b/docs/source/api/v5/regions.rst
index 6f1323487..13a898416 100644
--- a/docs/source/api/v5/regions.rst
+++ b/docs/source/api/v5/regions.rst
@@ -41,8 +41,7 @@ Request Structure
 	+-----------+----------+---------------------------------------------------------------------------------------------------------------+
 	| name      | no       | Filter :term:`Regions` by name                                                                                |
 	+-----------+----------+---------------------------------------------------------------------------------------------------------------+
-	| orderby   | no       | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response`` |
-	|           |          | array                                                                                                         |
+	| orderby   | no       | Choose the ordering of the results - either one of them "division", "id", "name"                              |
 	+-----------+----------+---------------------------------------------------------------------------------------------------------------+
 	| sortOrder | no       | Changes the order of sorting. Either ascending (default or "asc") or descending ("desc")                      |
 	+-----------+----------+---------------------------------------------------------------------------------------------------------------+
@@ -67,9 +66,9 @@ Request Structure
 Response Structure
 -------------------
 :divisionName: The name of the division which contains this region
-:divisionId:   The integral, unique identifier of the division which contains this region
+:division:   The integral, unique identifier of the division which contains this region
 :id:           An integral, unique identifier for this region
-:lastUpdated:  The date and time at which this region was last updated, in :ref:`non-rfc-datetime`
+:lastUpdated:  The date and time at which this region was last updated in :rfc:`3339` Format
 :name:         The region name
 
 .. code-block:: http
@@ -92,7 +91,7 @@ Response Structure
 			"divisionName": "Quebec",
 			"division": 1,
 			"id": 2,
-			"lastUpdated": "2018-12-05 17:50:58+00",
+			"lastUpdated": "2023-05-25T15:59:33.7096-06:00",
 			"name": "Montreal"
 		}
 	]}
@@ -110,8 +109,7 @@ Creates a new region
 
 Request Structure
 -----------------
-:division:     The integral, unique identifier of the division which shall contain the new region\ [1]_
-:divisionName: The name of the division which shall contain the new region\ [1]_
+:division:     The integral, unique identifier of the division which shall contain the new region
 :name:         The name of the region
 
 .. code-block:: http
@@ -126,19 +124,16 @@ Request Structure
 	Content-Type: application/json
 
 	{
-		"name": "Manchester",
-		"division": "4",
-		"divisionName": "England"
+		"division": 4,
+		"name": "Manchester"
 	}
 
-.. [1] The only "division" key that actually matters in the request body is ``division``; ``divisionName`` is not validated and has no effect - particularly not the effect of re-naming the division - beyond changing the name in the API response to this request. Subsequent requests will reveal the true name of the division. Note that if ``divisionName`` is not present in the request body it will be ``null`` in the response, but again further requests will show the true division name (provided it has been assigned to a division).
-
 Response Structure
 ------------------
 :divisionName: The name of the division which contains this region
-:divisionId:   The integral, unique identifier of the division which contains this region
+:division:   The integral, unique identifier of the division which contains this region
 :id:           An integral, unique identifier for this region
-:lastUpdated:  The date and time at which this region was last updated, in :ref:`non-rfc-datetime`
+:lastUpdated:  The date and time at which this region was last updated in :rfc:`3339` Format
 :name:         The region name
 
 .. code-block:: http
@@ -166,7 +161,7 @@ Response Structure
 		"divisionName": "England",
 		"division": 3,
 		"id": 5,
-		"lastUpdated": "2018-12-06 02:14:45+00",
+		"lastUpdated": "2023-05-25T15:59:33.7096-06:00",
 		"name": "Manchester"
 	}}
 
diff --git a/docs/source/api/v5/regions_id.rst b/docs/source/api/v5/regions_id.rst
index ed92a29d8..ca8e8a81d 100644
--- a/docs/source/api/v5/regions_id.rst
+++ b/docs/source/api/v5/regions_id.rst
@@ -38,8 +38,7 @@ Request Structure
 	|  ID  | The integral, unique identifier of the region to update |
 	+------+---------------------------------------------------------+
 
-:division:     The new integral, unique identifier of the division which shall contain the region\ [1]_
-:divisionName: The new name of the division which shall contain the region\ [1]_
+:division:     The new integral, unique identifier of the division which shall contain the region
 :name:         The new name of the region
 
 .. code-block:: http
@@ -54,20 +53,16 @@ Request Structure
 	Content-Type: application/json
 
 	{
-		"name": "Leeds",
 		"division": 3,
-		"divisionName": "England"
+		"name": "Leeds"
 	}
 
-.. [1] The only "division" key that actually matters in the request body is ``division``; ``divisionName`` is not validated and has no effect - particularly not the effect of re-naming the division - beyond changing the name in the API response to this request. Subsequent requests will reveal the true name of the division. Note that if ``divisionName`` is not present in the request body it will be ``null`` in the response, but again further requests will show the true division name (provided it has been assigned to a division).
-
-
 Response Structure
 ------------------
 :divisionName: The name of the division which contains this region
-:divisionId:   The integral, unique identifier of the division which contains this region
+:division:   The integral, unique identifier of the division which contains this region
 :id:           An integral, unique identifier for this region
-:lastUpdated:  The date and time at which this region was last updated, in :ref:`non-rfc-datetime`
+:lastUpdated:  The date and time at which this region was last updated in :rfc:`3339` Format
 :name:         The region name
 
 .. code-block:: http
@@ -95,6 +90,6 @@ Response Structure
 		"divisionName": "England",
 		"division": 3,
 		"id": 5,
-		"lastUpdated": "2018-12-06 02:23:40+00",
+		"lastUpdated": "2023-05-26T15:59:33.7096-06:00",
 		"name": "Leeds"
 	}}
diff --git a/docs/source/api/v5/servers.rst b/docs/source/api/v5/servers.rst
index ed9024fe6..a612c1add 100644
--- a/docs/source/api/v5/servers.rst
+++ b/docs/source/api/v5/servers.rst
@@ -98,7 +98,7 @@ Response Structure
 :iloIpAddress: The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :iloIpGateway: The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :iloIpNetmask: The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
-:iloPassword:  The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :term:`Role(s) <Role>`
+:iloPassword:  The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the SECURE-SERVER:READ permission.
 :iloUsername:  The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
 
@@ -158,7 +158,7 @@ Response Structure
 :typeId:     The integral, unique identifier of the 'type' of this server
 :updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (:term:`t3c`, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
-:xmppPasswd: The password used in XMPP communications with the server
+:xmppPasswd: The password used in XMPP communications with the server - displays as simply ``******`` if the currently logged-in user does not have the SECURE-SERVER:READ permission.
 
 .. code-block:: http
 	:caption: Response Example
@@ -254,7 +254,7 @@ Request Structure
 :iloIpAddress: An optional IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :iloIpGateway: An optional IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :iloIpNetmask: An optional IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
-:iloPassword:  An optional string containing the password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :term:`Role(s) <Role>`
+:iloPassword:  An optional string containing the password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the SECURE-SERVER:READ permission.
 :iloUsername:  An optional string containing the user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be necessary, but it is illegal for this set to be an empty collection.
 
@@ -302,7 +302,7 @@ Request Structure
 
 :typeId:     The integral, unique identifier of the 'type' of this server
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
-:xmppPasswd: An optional password used in XMPP communications with the server
+:xmppPasswd: An optional password used in XMPP communications with the server - displays as simply ``******`` if the currently logged-in user does not have the SECURE-SERVER:READ permission.
 
 .. code-block:: http
 	:caption: Request Example
@@ -385,7 +385,7 @@ Response Structure
 :iloIpAddress: The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :iloIpGateway: The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :iloIpNetmask: The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
-:iloPassword:  The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
+:iloPassword:  The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the SECURE-SERVER:READ permission.
 :iloUsername:  The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
 :interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
 
@@ -445,7 +445,7 @@ Response Structure
 :typeId:     The integral, unique identifier of the 'type' of this server
 :updPending: A boolean value which, if ``true``, indicates that the server has updates of some kind pending, typically to be acted upon by Traffic Control Cache Config (T3C, formerly ORT)
 :xmppId:     A system-generated UUID used to generate a server hashId for use in Traffic Router's consistent hashing algorithm. This value is set when a server is created and cannot be changed afterwards.
-:xmppPasswd: The password used in XMPP communications with the server
+:xmppPasswd: The password used in XMPP communications with the server - displays as simply ``******`` if the currently logged-in user does not have the SECURE-SERVER:READ permission.
 
 .. code-block:: http
 	:caption: Response Example
diff --git a/lib/go-tc/regions.go b/lib/go-tc/regions.go
index fd3808233..5d3a29050 100644
--- a/lib/go-tc/regions.go
+++ b/lib/go-tc/regions.go
@@ -21,6 +21,9 @@ package tc
 
 // RegionsResponse is the type of responses from Traffic Ops to GET requests
 // made to its /regions API endpoint.
+
+import "time"
+
 type RegionsResponse struct {
 	Response []Region `json:"response"`
 	Alerts
@@ -35,6 +38,27 @@ type Region struct {
 	Name         string    `json:"name" db:"name"`
 }
 
+// RegionsResponseV5 is an alias for the latest minor version of the major version 5.
+type RegionsResponseV5 = RegionsResponseV50
+
+// RegionsResponseV50 is response made from /regions API endpoint - in the latest minor version APIv50.
+type RegionsResponseV50 struct {
+	Response []RegionV5 `json:"response"`
+	Alerts
+}
+
+// RegionV5 is an alias for the latest minor version of the major version 5.
+type RegionV5 = RegionV50
+
+// RegionV50 is a named collection of Physical Locations within a Division in the latest minor version APIv50.
+type RegionV50 struct {
+	DivisionName string    `json:"divisionName"`
+	Division     int       `json:"division" db:"division"`
+	ID           int       `json:"id" db:"id"`
+	LastUpdated  time.Time `json:"lastUpdated" db:"last_updated"`
+	Name         string    `json:"name" db:"name"`
+}
+
 // RegionName is a response to a request to get a region by its name. It
 // includes the division that the region is in.
 type RegionName struct {
diff --git a/traffic_control/clients/python/trafficops/tosession.py b/traffic_control/clients/python/trafficops/tosession.py
index fbb5565e3..db97e460a 100644
--- a/traffic_control/clients/python/trafficops/tosession.py
+++ b/traffic_control/clients/python/trafficops/tosession.py
@@ -2028,6 +2028,49 @@ class TOSession(RestApiSession):
 		:rtype: Tuple[Dict[str, Any], requests.Response]
 		:raises: Union[LoginError, OperationError]
 		"""
+	
+	#
+	# Service categories
+	#
+	@api_request('get', 'service_categories', ('3.0', '4.0', '4.1', '5.0'))
+	def get_service_categories(self, query_params=None):
+		"""
+		Get service categories.
+		:ref:`to-api-service_categories`
+		:rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], requests.Response]
+		:raises: Union[LoginError, OperationError]
+		"""
+
+
+	@api_request('post', 'service_categories', ('3.0', '4.0', '4.1', '5.0'))
+	def create_service_category(self, data=None):
+		"""
+		Create a service category.
+		:param data: The update action. QueueUpdateRequest() can be used for this argument also.
+		:type data: Dict[str, Any]
+		:rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], requests.Response]
+		:raises: Union[LoginError, OperationError]
+		"""
+	
+	@api_request('put', 'service_categories/{service_category_name:s}', ('3.0', '4.0', '4.1', '5.0'))
+	def update_service_category(self, service_category_name=None, data=None):
+		"""
+		Update a service category.
+		:param service_category_name: The current name of the Service Category.
+		:param data: The update action. QueueUpdateRequest() can be used for this argument also.
+		:type data: Dict[str, Any]
+		:rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], requests.Response]
+		:raises: Union[LoginError, OperationError]
+		"""
+
+	@api_request('delete', 'service_categories/{service_category_name:s}', ('3.0', '4.0', '4.1', '5.0'))
+	def delete_service_category(self, service_category_name=None):
+		"""
+		Delete a service category.
+		:param service_category_name: The current name of the Service Category to be deleted.
+		:rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], requests.Response]
+		:raises: Union[LoginError, OperationError]
+		"""
 
 	#
 	# Static DNS Entries
diff --git a/traffic_ops/traffic_ops_golang/parameter/parameters.go b/traffic_ops/traffic_ops_golang/parameter/parameters.go
index 167019fbc..3595ced55 100644
--- a/traffic_ops/traffic_ops_golang/parameter/parameters.go
+++ b/traffic_ops/traffic_ops_golang/parameter/parameters.go
@@ -176,10 +176,18 @@ func (param *TOParameter) Read(h http.Header, useIMS bool) ([]interface{}, error
 			return nil, nil, errors.New("scanning " + param.GetType() + ": " + err.Error()), http.StatusInternalServerError, nil
 		}
 		if p.Secure != nil && *p.Secure {
-			if param.ReqInfo.Version.Major >= 4 &&
-				param.ReqInfo.Config.RoleBasedPermissions &&
-				!param.ReqInfo.User.Can("PARAMETER-SECURE:READ") {
-				p.Value = &HiddenField
+			if param.ReqInfo.Version.Major >= 5 {
+				if !param.ReqInfo.User.Can("PARAMETER-SECURE:READ") {
+					p.Value = &HiddenField
+				}
+			} else if param.ReqInfo.Version.Major == 4 {
+				if param.ReqInfo.Config.RoleBasedPermissions {
+					if !param.ReqInfo.User.Can("PARAMETER-SECURE:READ") {
+						p.Value = &HiddenField
+					}
+				} else if param.ReqInfo.User.PrivLevel < auth.PrivLevelAdmin {
+					p.Value = &HiddenField
+				}
 			} else if param.ReqInfo.User.PrivLevel < auth.PrivLevelAdmin {
 				p.Value = &HiddenField
 			}
diff --git a/traffic_ops/traffic_ops_golang/region/regions.go b/traffic_ops/traffic_ops_golang/region/regions.go
index 79fd9ccbe..5ce193057 100644
--- a/traffic_ops/traffic_ops_golang/region/regions.go
+++ b/traffic_ops/traffic_ops_golang/region/regions.go
@@ -20,14 +20,22 @@ package region
  */
 
 import (
+	"database/sql"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"net/http"
 	"time"
 
+	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
+	"github.com/apache/trafficcontrol/lib/go-util"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
+
+	validation "github.com/go-ozzo/ozzo-validation"
 )
 
 // we need a type alias to define functions on
@@ -192,3 +200,259 @@ func deleteQuery() string {
 	query := deleteQueryBase() + ` WHERE id=:id`
 	return query
 }
+
+// Read is the handler for GET requests to Regions of APIv5
+func Read(w http.ResponseWriter, r *http.Request) {
+	var maxTime time.Time
+	var runSecond bool
+
+	inf, sysErr, userErr, errCode := api.NewInfo(r, nil, nil)
+	tx := inf.Tx
+	if sysErr != nil || userErr != nil {
+		api.HandleErr(w, r, tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	// Query Parameters to Database Query column mappings
+	queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+		"division": {Column: "r.division"},
+		"id":       {Column: "r.id", Checker: api.IsInt},
+		"name":     {Column: "r.name"},
+	}
+	if _, ok := inf.Params["orderby"]; !ok {
+		inf.Params["orderby"] = "name"
+	}
+	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols)
+	if len(errs) > 0 {
+		api.HandleErr(w, r, tx.Tx, http.StatusBadRequest, util.JoinErrs(errs), nil)
+		return
+	}
+	if inf.Config.UseIMS {
+		runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, r.Header, queryValues, SelectMaxLastUpdatedQuery(where))
+		if !runSecond {
+			log.Debugln("IMS HIT")
+			api.AddLastModifiedHdr(w, maxTime)
+			w.WriteHeader(http.StatusNotModified)
+			return
+		}
+		log.Debugln("IMS MISS")
+	} else {
+		log.Debugln("Non IMS request")
+	}
+
+	query := `SELECT r.division, d.name as divisionname, r.id, r.last_updated, r.name FROM region r
+				JOIN division d ON r.division = d.id` + where + orderBy + pagination
+	rows, err := tx.NamedQuery(query, queryValues)
+	if err != nil {
+		api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("region get: error getting region(s): %w", err))
+	}
+	defer log.Close(rows, "unable to close DB connection")
+
+	typ := tc.RegionV5{}
+	regionList := []tc.RegionV5{}
+	for rows.Next() {
+		if err = rows.Scan(&typ.Division, &typ.DivisionName, &typ.ID, &typ.LastUpdated, &typ.Name); err != nil {
+			api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("error getting region(s): %w", err))
+		}
+		regionList = append(regionList, typ)
+	}
+
+	api.WriteResp(w, r, regionList)
+	return
+}
+
+// Create region with the passed data for APIv5.
+func Create(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+	tx := inf.Tx.Tx
+	rg, readValErr := readAndValidateJsonStruct(r)
+	if readValErr != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+		return
+	}
+
+	var exists bool
+	existErr := tx.QueryRow(`SELECT EXISTS(SELECT * from region where name = $1)`, rg.Name).Scan(&exists)
+
+	if existErr != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if region with name %s exists", existErr, rg.Name))
+		return
+	}
+	if exists {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("region name '%s' already exists", rg.Name), nil)
+		return
+	}
+
+	query := `INSERT INTO region ( division, name) VALUES ( $1, $2 ) 
+				RETURNING 
+					id,last_updated,
+					(SELECT d.name FROM division d WHERE d.id = region.division)`
+	err := tx.QueryRow(query, rg.Division, rg.Name).Scan(&rg.ID, &rg.LastUpdated, &rg.DivisionName)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("error: %w in creating region with name: %s", err, rg.Name), nil)
+			return
+		}
+		usrErr, sysErr, code := api.ParseDBError(err)
+		api.HandleErr(w, r, tx, code, usrErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "region is created.")
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, rg)
+	return
+}
+
+// Update a Region for APIv5
+func Update(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()
+
+	tx := inf.Tx.Tx
+	rg, readValErr := readAndValidateJsonStruct(r)
+	if readValErr != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+		return
+	}
+
+	requestedRegionId := inf.IntParams["id"]
+	// check if the entity was already updated
+	userErr, sysErr, errCode = api.CheckIfUnModified(r.Header, inf.Tx, requestedRegionId, "region")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	query := `UPDATE region SET division= $1, name= $2 WHERE id= $3 
+				RETURNING 
+					id,last_updated,
+					(SELECT d.name FROM division d WHERE d.id = region.division)`
+
+	err := tx.QueryRow(query, rg.Division, rg.Name, requestedRegionId).Scan(&rg.ID, &rg.LastUpdated, &rg.DivisionName)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("region: %d not found", requestedRegionId), nil)
+			return
+		}
+		usrErr, sysErr, code := api.ParseDBError(err)
+		api.HandleErr(w, r, tx, code, usrErr, sysErr)
+		return
+	}
+
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "region was updated")
+	api.WriteAlertsObj(w, r, http.StatusOK, alerts, rg)
+	return
+}
+
+// Delete an Region for APIv5
+func Delete(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()
+
+	requestedRegionId := inf.Params["id"]
+	requestedRegionName := inf.Params["name"]
+
+	var exists bool
+	var noExistsErrMsg string
+
+	if requestedRegionId == "" && requestedRegionName == "" {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("refusing to delete all resources of type Region"), nil)
+		return
+	} else if requestedRegionId != "" && requestedRegionName != "" { // checking if both id and name are passed
+		existErr := tx.QueryRow(`SELECT EXISTS(SELECT * from region where id = $1 AND name = $2)`, requestedRegionId, requestedRegionName).Scan(&exists)
+		noExistsErrMsg = fmt.Sprintf("no region exists by id: %s and name: %s", requestedRegionId, requestedRegionName)
+		if existErr != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if region with id %s exists", existErr, requestedRegionId))
+			return
+		}
+	} else if requestedRegionId != "" {
+		existErr := tx.QueryRow(`SELECT EXISTS(SELECT * from region where id = $1)`, requestedRegionId).Scan(&exists)
+		noExistsErrMsg = fmt.Sprintf("no region exists by id: %s", requestedRegionId)
+		if existErr != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if region with id %s exists", existErr, requestedRegionId))
+			return
+		}
+	} else if requestedRegionName != "" {
+		existErr := tx.QueryRow(`SELECT EXISTS(SELECT * from region where name = $1)`, requestedRegionName).Scan(&exists)
+		noExistsErrMsg = fmt.Sprintf("no region exists by name: %s", requestedRegionName)
+		if existErr != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if region with name %s exists", existErr, requestedRegionName))
+			return
+		}
+		if exists {
+			existErr := tx.QueryRow(`SELECT id from region where name = $1`, requestedRegionName).Scan(&requestedRegionId)
+			if existErr != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if region with name %s exists", existErr, requestedRegionName))
+				return
+			}
+		}
+	}
+
+	if !exists {
+		api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf(noExistsErrMsg), nil)
+		return
+	}
+
+	res, err := tx.Exec("DELETE FROM region WHERE id = $1", requestedRegionId)
+
+	if err != nil {
+		userErr, sysErr, errCode := api.ParseDBError(err)
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	rowsAffected, err := res.RowsAffected()
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("determining rows affected for delete region: %w", err))
+		return
+	}
+	if rowsAffected == 0 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("no rows deleted for region"), nil)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "region was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	return
+}
+
+// readAndValidateJsonStruct reads json body and validates json fields
+func readAndValidateJsonStruct(r *http.Request) (tc.RegionV5, error) {
+	var region tc.RegionV5
+	if err := json.NewDecoder(r.Body).Decode(&region); err != nil {
+		userErr := fmt.Errorf("error decoding POST request body into RegionV5 struct %w", err)
+		return region, userErr
+	}
+
+	// validate JSON body
+	errs := tovalidate.ToErrors(validation.Errors{
+		"division": validation.Validate(region.Division, validation.NotNil, validation.Min(0)),
+		"name":     validation.Validate(region.Name, validation.Required),
+	})
+	if len(errs) > 0 {
+		userErr := util.JoinErrs(errs)
+		return region, userErr
+	}
+	return region, nil
+}
+
+// SelectMaxLastUpdatedQuery used for TryIfModifiedSinceQuery()
+func SelectMaxLastUpdatedQuery(where string) string {
+	return `SELECT max(t) from (
+		SELECT max(r.last_updated) as t FROM region r
+JOIN division d ON r.division = d.id ` + where +
+		` UNION ALL
+	select max(last_updated) as t from last_deleted l where l.table_name='region') as res`
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index f62a77db4..26b5a0c17 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -278,10 +278,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `profiles/name/{new_profile}/copy/{existing_profile}`, Handler: profile.CopyProfileHandler, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"PROFILE:CREATE", "PROFILE:READ", "PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 40614320931},
 
 		//Region: CRUDs
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `regions/?$`, Handler: api.ReadHandler(&region.TORegion{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"REGION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 41003708531},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `regions/{id}$`, Handler: api.UpdateHandler(&region.TORegion{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"REGION:UPDATE", "REGION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42230822431},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `regions/?$`, Handler: api.CreateHandler(&region.TORegion{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"REGION:CREATE", "REGION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 428833448831},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `regions/?$`, Handler: api.DeleteHandler(&region.TORegion{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"REGION:DELETE"}, Authenticated: Authenticated, Middlewares: nil, ID: 423262675831},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `regions/?$`, Handler: region.Read, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"REGION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 41003708531},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `regions/{id}$`, Handler: region.Update, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"REGION:UPDATE", "REGION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42230822431},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `regions/?$`, Handler: region.Create, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"REGION:CREATE", "REGION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 428833448831},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `regions/?$`, Handler: region.Delete, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"REGION:DELETE"}, Authenticated: Authenticated, Middlewares: nil, ID: 423262675831},
 
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `topologies/?$`, Handler: api.CreateHandler(&topology.TOTopology{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"TOPOLOGY:CREATE", "TOPOLOGY:READ", "CACHE-GROUP:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 48714522211},
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `topologies/?$`, Handler: api.ReadHandler(&topology.TOTopology{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"TOPOLOGY:READ", "CACHE-GROUP:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 48714522221},
diff --git a/traffic_ops/traffic_ops_golang/server/servers.go b/traffic_ops/traffic_ops_golang/server/servers.go
index a2d2461ce..4253514d5 100644
--- a/traffic_ops/traffic_ops_golang/server/servers.go
+++ b/traffic_ops/traffic_ops_golang/server/servers.go
@@ -698,7 +698,7 @@ func Read(w http.ResponseWriter, r *http.Request) {
 		log.Warnf("Couldn't get config %v", e)
 	}
 
-	servers, serverCount, userErr, sysErr, errCode, maxTime = getServers(r.Header, inf.Params, inf.Tx, inf.User, useIMS, *version)
+	servers, serverCount, userErr, sysErr, errCode, maxTime = getServers(r.Header, inf.Params, inf.Tx, inf.User, useIMS, *version, inf.Config.RoleBasedPermissions)
 	if maxTime != nil && api.SetLastModifiedHeader(r, useIMS) {
 		api.AddLastModifiedHdr(w, *maxTime)
 	}
@@ -767,7 +767,7 @@ func getServerCount(tx *sqlx.Tx, query string, queryValues map[string]interface{
 	return serverCount, nil
 }
 
-func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth.CurrentUser, useIMS bool, version api.Version) ([]tc.ServerV40, uint64, error, error, int, *time.Time) {
+func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth.CurrentUser, useIMS bool, version api.Version, roleBasedPerms bool) ([]tc.ServerV40, uint64, error, error, int, *time.Time) {
 	var maxTime time.Time
 	var runSecond bool
 	// Query Parameters to Database Query column mappings
@@ -956,7 +956,12 @@ JOIN server_profile sp ON s.id = sp.server`
 		if err != nil {
 			return nil, serverCount, nil, errors.New("getting servers: " + err.Error()), http.StatusInternalServerError, nil
 		}
-		if user.PrivLevel < auth.PrivLevelOperations {
+		if (version.GreaterThanOrEqualTo(&api.Version{Major: 4}) && roleBasedPerms) || version.GreaterThanOrEqualTo(&api.Version{Major: 5}) {
+			if !user.Can("SECURE-SERVER:READ") {
+				s.ILOPassword = &HiddenField
+				s.XMPPPasswd = &HiddenField
+			}
+		} else if user.PrivLevel < auth.PrivLevelOperations {
 			s.ILOPassword = &HiddenField
 			s.XMPPPasswd = &HiddenField
 		}
@@ -1322,7 +1327,7 @@ func Update(w http.ResponseWriter, r *http.Request) {
 	id := inf.IntParams["id"]
 
 	// Get original server
-	originals, _, userErr, sysErr, errCode, _ := getServers(r.Header, inf.Params, inf.Tx, inf.User, false, *version)
+	originals, _, userErr, sysErr, errCode, _ := getServers(r.Header, inf.Params, inf.Tx, inf.User, false, *version, inf.Config.RoleBasedPermissions)
 	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
 		return
@@ -2185,7 +2190,7 @@ func Delete(w http.ResponseWriter, r *http.Request) {
 	}
 
 	var servers []tc.ServerV40
-	servers, _, userErr, sysErr, errCode, _ = getServers(r.Header, map[string]string{"id": inf.Params["id"]}, inf.Tx, inf.User, false, *version)
+	servers, _, userErr, sysErr, errCode, _ = getServers(r.Header, map[string]string{"id": inf.Params["id"]}, inf.Tx, inf.User, false, *version, inf.Config.RoleBasedPermissions)
 	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
 		return
diff --git a/traffic_ops/v5-client/region.go b/traffic_ops/v5-client/region.go
index 7ef85216b..e7c2edaa1 100644
--- a/traffic_ops/v5-client/region.go
+++ b/traffic_ops/v5-client/region.go
@@ -28,7 +28,7 @@ import (
 const apiRegions = "/regions"
 
 // CreateRegion creates the given Region.
-func (to *Session) CreateRegion(region tc.Region, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) CreateRegion(region tc.RegionV5, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	if region.Division == 0 && region.DivisionName != "" {
 		divisionOpts := NewRequestOptions()
 		divisionOpts.QueryParameters.Set("name", region.DivisionName)
@@ -47,7 +47,7 @@ func (to *Session) CreateRegion(region tc.Region, opts RequestOptions) (tc.Alert
 }
 
 // UpdateRegion replaces the Region identified by ID with the one provided.
-func (to *Session) UpdateRegion(id int, region tc.Region, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) UpdateRegion(id int, region tc.RegionV5, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	route := fmt.Sprintf("%s/%d", apiRegions, id)
 	var alerts tc.Alerts
 	reqInf, err := to.put(route, opts, region, &alerts)
@@ -55,8 +55,8 @@ func (to *Session) UpdateRegion(id int, region tc.Region, opts RequestOptions) (
 }
 
 // GetRegions returns all Regions in Traffic Ops.
-func (to *Session) GetRegions(opts RequestOptions) (tc.RegionsResponse, toclientlib.ReqInf, error) {
-	var data tc.RegionsResponse
+func (to *Session) GetRegions(opts RequestOptions) (tc.RegionsResponseV5, toclientlib.ReqInf, error) {
+	var data tc.RegionsResponseV5
 	reqInf, err := to.get(apiRegions, opts, &data)
 	return data, reqInf, err
 }


Test patch:
diff --git a/traffic_ops/testing/api/v5/regions_test.go b/traffic_ops/testing/api/v5/regions_test.go
index e30e7b86f..f031e542f 100644
--- a/traffic_ops/testing/api/v5/regions_test.go
+++ b/traffic_ops/testing/api/v5/regions_test.go
@@ -38,7 +38,7 @@ func TestRegions(t *testing.T) {
 		currentTimeRFC := currentTime.Format(time.RFC1123)
 		tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
 
-		methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.Region]{
+		methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.RegionV5]{
 			"GET": {
 				"NOT MODIFIED when NO CHANGES made": {
 					ClientSession: TOSession,
@@ -121,10 +121,9 @@ func TestRegions(t *testing.T) {
 			"POST": {
 				"NOT FOUND when DIVISION DOESNT EXIST": {
 					ClientSession: TOSession,
-					RequestBody: tc.Region{
-						Name:         "invalidDivision",
-						Division:     99999999,
-						DivisionName: "doesntexist",
+					RequestBody: tc.RegionV5{
+						Name:     "invalidDivision",
+						Division: 99999999,
 					},
 					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusNotFound)),
 				},
@@ -133,10 +132,9 @@ func TestRegions(t *testing.T) {
 				"OK when VALID request": {
 					EndpointID:    GetRegionID(t, "cdn-region2"),
 					ClientSession: TOSession,
-					RequestBody: tc.Region{
-						Name:         "newName",
-						Division:     GetDivisionID(t, "cdn-div2")(),
-						DivisionName: "cdn-div2",
+					RequestBody: tc.RegionV5{
+						Name:     "newName",
+						Division: GetDivisionID(t, "cdn-div2")(),
 					},
 					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
 						validateRegionsUpdateCreateFields("newName", map[string]interface{}{"Name": "newName"})),
@@ -145,20 +143,18 @@ func TestRegions(t *testing.T) {
 					EndpointID:    GetRegionID(t, "region1"),
 					ClientSession: TOSession,
 					RequestOpts:   client.RequestOptions{Header: http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}}},
-					RequestBody: tc.Region{
-						Name:         "newName",
-						Division:     GetDivisionID(t, "division1")(),
-						DivisionName: "division1",
+					RequestBody: tc.RegionV5{
+						Name:     "newName",
+						Division: GetDivisionID(t, "division1")(),
 					},
 					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
 				},
 				"PRECONDITION FAILED when updating with IFMATCH ETAG Header": {
 					EndpointID:    GetRegionID(t, "region1"),
 					ClientSession: TOSession,
-					RequestBody: tc.Region{
-						Name:         "newName",
-						Division:     GetDivisionID(t, "division1")(),
-						DivisionName: "division1",
+					RequestBody: tc.RegionV5{
+						Name:     "newName",
+						Division: GetDivisionID(t, "division1")(),
 					},
 					RequestOpts:  client.RequestOptions{Header: http.Header{rfc.IfMatch: {rfc.ETag(currentTime)}}},
 					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
@@ -229,7 +225,7 @@ func TestRegions(t *testing.T) {
 func validateRegionsFields(expectedResp map[string]interface{}) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected Regions response to not be nil.")
-		regionResp := resp.([]tc.Region)
+		regionResp := resp.([]tc.RegionV5)
 		for field, expected := range expectedResp {
 			for _, region := range regionResp {
 				switch field {
@@ -264,7 +260,7 @@ func validateRegionsSort() utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected Regions response to not be nil.")
 		var regionNames []string
-		regionResp := resp.([]tc.Region)
+		regionResp := resp.([]tc.RegionV5)
 		for _, region := range regionResp {
 			regionNames = append(regionNames, region.Name)
 		}
@@ -275,7 +271,7 @@ func validateRegionsSort() utils.CkReqFunc {
 func validateRegionsDescSort() utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected Regions response to not be nil.")
-		regionDescResp := resp.([]tc.Region)
+		regionDescResp := resp.([]tc.RegionV5)
 		var descSortedList []string
 		var ascSortedList []string
 		assert.RequireGreaterOrEqual(t, len(regionDescResp), 2, "Need at least 2 Regions in Traffic Ops to test desc sort, found: %d", len(regionDescResp))
@@ -298,7 +294,7 @@ func validateRegionsDescSort() utils.CkReqFunc {
 
 func validateRegionsPagination(paginationParam string) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
-		paginationResp := resp.([]tc.Region)
+		paginationResp := resp.([]tc.RegionV5)
 
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("orderby", "id")
diff --git a/traffic_ops/testing/api/v5/servers_test.go b/traffic_ops/testing/api/v5/servers_test.go
index 6e2b24283..5b488e772 100644
--- a/traffic_ops/testing/api/v5/servers_test.go
+++ b/traffic_ops/testing/api/v5/servers_test.go
@@ -39,6 +39,8 @@ func TestServers(t *testing.T) {
 		currentTimeRFC := currentTime.Format(time.RFC1123)
 		tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
 
+		opsUserSession := utils.CreateV5Session(t, Config.TrafficOps.URL, "operations", Config.TrafficOps.UserPassword, Config.Default.Session.TimeoutInSecs)
+
 		methodTests := utils.V5TestCase{
 			"GET": {
 				"NOT MODIFIED when NO CHANGES made": {
@@ -117,6 +119,12 @@ func TestServers(t *testing.T) {
 					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"dsId": {strconv.Itoa(GetDeliveryServiceId(t, "ds-based-top-with-no-mids")())}}},
 					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseLengthGreaterOrEqual(1), validateServerTypeIsNotMid()),
 				},
+				"VALUE HIDDEN when OPERATIONS USER views SERVER": {
+					ClientSession: opsUserSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"iloPassword": {"testSecure"}, "xmppPasswd": {"testSecure"}}},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseLengthGreaterOrEqual(1),
+						validateServerFields(map[string]interface{}{"ILOPassword": "********", "XMPPPasswd": "********"})),
+				},
 				"EMPTY RESPONSE when INVALID DSID parameter": {
 					ClientSession: TOSession,
 					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"dsId": {"999999"}}},
@@ -438,6 +446,12 @@ func validateServerFields(expectedResp map[string]interface{}) utils.CkReqFunc {
 				case "TypeID":
 					assert.RequireNotNil(t, server.TypeID, "Expected TypeID to not be nil")
 					assert.Equal(t, expected, *server.TypeID, "Expected Type to be %d, but got %d", expected, *server.TypeID)
+				case "XMPPPasswd":
+					assert.RequireNotNil(t, server.XMPPPasswd, "Expected XMPPPasswd to not be nil")
+					assert.Equal(t, expected, *server.XMPPPasswd, "Expected XMPPPasswd to be %s, but got %s", expected, *server.XMPPPasswd)
+				case "ILOPassword":
+					assert.RequireNotNil(t, server.ILOPassword, "Expected ILOPassword to not be nil")
+					assert.Equal(t, expected, *server.ILOPassword, "Expected ILOPassword to be %s, but got %s", expected, *server.ILOPassword)
 				default:
 					t.Errorf("Expected field: %v, does not exist in response", field)
 				}
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go b/traffic_ops/testing/api/v5/traffic_control_test.go
index 1ed3ad0b0..4263f50fa 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -43,7 +43,7 @@ type TrafficControl struct {
 	Parameters                                        []tc.Parameter                          `json:"parameters"`
 	ProfileParameters                                 []tc.ProfileParameter                   `json:"profileParameters"`
 	PhysLocations                                     []tc.PhysLocationV5                     `json:"physLocations"`
-	Regions                                           []tc.Region                             `json:"regions"`
+	Regions                                           []tc.RegionV5                           `json:"regions"`
 	Roles                                             []tc.RoleV4                             `json:"roles"`
 	Servers                                           []tc.ServerV4                           `json:"servers"`
 	ServerServerCapabilities                          []tc.ServerServerCapability             `json:"serverServerCapabilities"`
diff --git a/traffic_ops/testing/api_contract/v4/conftest.py b/traffic_ops/testing/api_contract/v4/conftest.py
index 07d1d3eb1..41e832867 100644
--- a/traffic_ops/testing/api_contract/v4/conftest.py
+++ b/traffic_ops/testing/api_contract/v4/conftest.py
@@ -1648,6 +1648,7 @@ def delivery_services_regex_data_post(to_session: TOSession, request_template_da
 @pytest.fixture(name="cdn_federation_post_data")
 def cdn_federation_data_post(to_session: TOSession, request_template_data: list[JSONData],
 		  cdn_post_data:dict[str, object], user_post_data:dict[str, object],
+		  federation_resolver_post_data:dict[str, object],
 		  delivery_services_post_data: dict[str, object]) -> dict[str, object]:
 	"""
 	PyTest Fixture to create POST data for cdn_name_federations endpoint.
@@ -1680,6 +1681,13 @@ def cdn_federation_data_post(to_session: TOSession, request_template_data: list[
 	response: tuple[JSONData, requests.Response] = to_session.assign_delivery_services_to_federations(federation_id=federation_id, data=delivery_service_federation)
 	delivery_service_federation_resp_obj = check_template_data(response, "delivery_service_federation")
 
+	#Assign a federation resolver to created federation
+	federation_resolver_id = federation_resolver_post_data["id"]
+	federation_federation_resolver = check_template_data(request_template_data["federation_federation_resolver"], "federation_federation_resolver")
+	federation_federation_resolver["fedResolverIds"][0] = federation_resolver_id
+	response: tuple[JSONData, requests.Response] = to_session.assign_federation_resolver_to_federations(federation_id=federation_id, data=federation_federation_resolver)
+	federation_federation_resolver_resp_obj = check_template_data(response, "federation_federation_resolver")
+
 	yield [cdn_name, cdn_federation_resp_obj, cdn_federation, federation_id, delivery_service_federation_resp_obj]
 	
 	msg = to_session.delete_federation_in_cdn(cdn_name=cdn_name, federation_id=federation_id)
@@ -1794,6 +1802,37 @@ def profile_parameters_post_data(to_session: TOSession, request_template_data: l
 		pytest.fail("Response from delete request is empty, Failing test_case")
 
 
+@pytest.fixture(name="service_category_post_data")
+def service_category_data_post(to_session: TOSession,
+		request_template_data: list[JSONData]) -> dict[str, object]:
+	"""
+	PyTest Fixture to create POST data for service_category endpoint.
+	:param to_session: Fixture to get Traffic Ops session.
+	:param request_template_data: Fixture to get service_category request template.
+	:returns: Sample POST data and the actual API response.
+	"""
+
+	service_category = check_template_data(
+		request_template_data["service_category"], "service_category")
+
+	# Return new post data and post response from service_category POST request
+	service_category_name = service_category["name"]
+	service_category["name"] = service_category_name + str(randint(0,1000))
+
+	logger.info("New service_category data to hit POST method %s", service_category)
+	# Hitting service_category POST methed
+	response: tuple[JSONData, requests.Response] = to_session.create_service_category(
+		data=service_category)
+	resp_obj = check_template_data(response, "service_category")
+	yield resp_obj
+	service_category_name = resp_obj.get("name")
+	msg = to_session.delete_service_category(service_category_name=service_category_name)
+	logger.info("Deleting service_category data... %s", msg)
+	if msg is None:
+		logger.error("service_category returned by Traffic Ops is missing an 'name' property")
+		pytest.fail("Response from delete request is empty, Failing test_case")
+
+
 @pytest.fixture(name="federation_resolver_post_data")
 def federation_resolver_data_post(to_session: TOSession, request_template_data: list[JSONData]
 				  ) -> dict[str, object]:
@@ -1812,8 +1851,8 @@ def federation_resolver_data_post(to_session: TOSession, request_template_data:
 	type_object = create_or_get_existing(to_session, "types", "type", type_data,
 				      {"useInTable": "federation"})
 	federation_resolver["typeId"] = type_object["id"]
-	ipaddress = federation_resolver["ipAddress"]
-	federation_resolver["ipAddress"] = ipaddress + randstr
+	federation_resolver["ipAddress"] = ".".join(map(str, (randint(0, 255) 
+                        for _ in range(4))))
 
 	logger.info("New federation_resolver data to hit POST method %s", federation_resolver)
 	# Hitting federation_resolver POST methed
diff --git a/traffic_ops/testing/api_contract/v4/data/request_template.json b/traffic_ops/testing/api_contract/v4/data/request_template.json
index ba1364518..0c6e7ad2f 100644
--- a/traffic_ops/testing/api_contract/v4/data/request_template.json
+++ b/traffic_ops/testing/api_contract/v4/data/request_template.json
@@ -419,6 +419,12 @@
 			"replace": true
 		}
 	],
+	"federation_federation_resolver":[
+		{
+			"fedResolverIds": [39],
+			"replace": true
+		}
+	],
 	"delivery_service_required_capabilities": [
 		{
 			"deliveryServiceID": 1,
@@ -441,6 +447,11 @@
 			"parameterId": 3
 		}
 	],
+	"service_category": [
+		{
+			"name": "SERVICE-CATEGORY-NAME"
+      }
+	],
 	"federation_resolver": [
 		{
 			"ipAddress": "::1/1",
diff --git a/traffic_ops/testing/api_contract/v4/data/response_template.json b/traffic_ops/testing/api_contract/v4/data/response_template.json
index 439dbca29..90195001a 100644
--- a/traffic_ops/testing/api_contract/v4/data/response_template.json
+++ b/traffic_ops/testing/api_contract/v4/data/response_template.json
@@ -1875,6 +1875,21 @@
             }
         }
     },
+    "service_category": {
+        "type": "object",
+        "required": [
+            "lastUpdated",
+            "name"
+        ],
+        "properties": {
+            "lastUpdated": {
+                "type": "string"
+            },
+            "name": {
+                "type": "string"
+            }
+        }
+    },
     "federation_resolver": {
         "type": "object",
         "required": [
@@ -1898,6 +1913,25 @@
             }
         }
     },
+    "federation_federation_resolver": {
+        "type": "object",
+        "required": [
+            "id",
+            "ipAddress",
+            "type"
+        ],
+        "properties": {
+            "id": {
+                "type": "integer"
+            },
+            "ipAddress": {
+                "type": "string"
+            },
+            "type": {
+                "type": "string"
+              }
+          }
+    },
     "delivery_service_capacity": {
         "type": "object",
         "required": [
diff --git a/traffic_ops/testing/api_contract/v4/test_cdn_federation.py b/traffic_ops/testing/api_contract/v4/test_cdn_federation.py
index 4aa423794..07413a415 100644
--- a/traffic_ops/testing/api_contract/v4/test_cdn_federation.py
+++ b/traffic_ops/testing/api_contract/v4/test_cdn_federation.py
@@ -64,6 +64,11 @@ def test_cdn_federation_contract(to_session: TOSession,
 		Union[dict[str, object], list[Union[dict[str, object], list[object], Primitive]], Primitive],
 		requests.Response
 	] = to_session.get_federation_delivery_services(federation_id=federation_id)
+
+	federation_resolvers_get_response: tuple[
+		Union[dict[str, object], list[Union[dict[str, object], list[object], Primitive]], Primitive],
+		requests.Response
+	] = to_session.get_federation_resolvers_by_id(federation_id=federation_id)
 	
     # Hitting cdn_federation PUT method
 	cdn_federation_sample_data["description"] = "test" + str(randint(0, 1000)) 
@@ -96,6 +101,12 @@ def test_cdn_federation_contract(to_session: TOSession,
 		if not isinstance(cdn_federation_put_data, dict):
 			raise TypeError(
 				"malformed API response; first cdn_federation_put in response is not an dict")
+		
+		federation_federation_resolver= federation_resolvers_get_response[0]
+		if not isinstance(federation_federation_resolver, list):
+			raise TypeError(
+				"malformed API response; first federation_federation_resolver in response is not an dict")
+		first_federation_federation_resolver = federation_federation_resolver[0]
 
 		cdn_federation_response_template = response_template_data.get(
 			"cdn_federation")
@@ -114,6 +125,11 @@ def test_cdn_federation_contract(to_session: TOSession,
 		if not isinstance(delivery_service_federation_response_template, dict):
 			raise TypeError(f"delivery_service_federation response template data must be a dict, not '"
 							f"{type(user_federation_response_template)}'")
+		
+		federation_federation_resolver_response_template = response_template_data.get("federation_federation_resolver")
+		if not isinstance(federation_federation_resolver_response_template, dict):
+			raise TypeError(f"delivery_service_federation response template data must be a dict, not '"
+							f"{type(user_federation_response_template)}'")
 
 		keys = ["cname", "ttl"]
 		prereq_values = [cdn_federation_data_post[key] for key in keys]
@@ -123,6 +139,7 @@ def test_cdn_federation_contract(to_session: TOSession,
 		assert validate(instance=first_user_federation, schema=user_federation_response_template) is None
 		assert validate(instance=first_delivery_service_federation, schema=delivery_service_federation_response_template) is None
 		assert validate(instance=cdn_federation_put_data, schema=cdn_federation_response_template) is None
+		assert validate(instance=first_federation_federation_resolver, schema=federation_federation_resolver_response_template) is None
 		
 		assert get_values == prereq_values
 	except IndexError:
diff --git a/traffic_ops/testing/api_contract/v4/test_service_categories.py b/traffic_ops/testing/api_contract/v4/test_service_categories.py
new file mode 100644
index 000000000..6e7679ac9
--- /dev/null
+++ b/traffic_ops/testing/api_contract/v4/test_service_categories.py
@@ -0,0 +1,72 @@
+#
+# Licensed 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.
+#
+
+"""API Contract Test Case for service_categories endpoint."""
+import logging
+from typing import Union
+import pytest
+import requests
+from jsonschema import validate
+
+from trafficops.tosession import TOSession
+
+# Create and configure logger
+logger = logging.getLogger()
+
+Primitive = Union[bool, int, float, str, None]
+
+
+def test_service_categories_contract(to_session: TOSession,
+	response_template_data: dict[str, Union[Primitive, list[Union[Primitive,
+							dict[str, object], list[object]]], dict[object, object]]],
+	service_category_post_data: dict[str, object]
+) -> None:
+	"""
+	Test step to validate keys, values and data types from service_category endpoint
+	response.
+	:param to_session: Fixture to get Traffic Ops session.
+	:param response_template_data: Fixture to get response template data from a prerequisites file.
+	:param service_category_post_data: Fixture to get service categories data.
+	"""
+	# validate service_category keys from api get response
+	logger.info("Accessing /service_category endpoint through Traffic ops session.")
+
+	service_category_name = service_category_post_data["name"]
+	if not isinstance(service_category_name, str):
+		raise TypeError("malformed API response; 'service_category_name' property not a string")
+
+	service_category_get_response: tuple[
+		Union[dict[str, object], list[Union[dict[str, object], list[object], Primitive]], Primitive],
+		requests.Response
+	] = to_session.get_service_categories(query_params={"name":service_category_name})
+    
+	try:
+		service_category = service_category_get_response[0]
+		if not isinstance(service_category, list):
+			raise TypeError(
+				"malformed API response; first service_category in response is not an array")
+		first_service_category = service_category[0]
+
+		service_category_response_template = response_template_data.get(
+			"service_category")
+		if not isinstance(service_category_response_template, dict):
+			raise TypeError(f"service_category response template data must be a dict, not'"
+							f"{type(service_category_response_template)}'")
+
+		assert first_service_category["name"] == service_category_post_data["name"]
+		assert validate(instance=first_service_category, schema=service_category_response_template) is None
+		
+	except IndexError:
+		logger.error("Either prerequisite data or API response was malformed")
+		pytest.fail("API contract test failed for service_category endpoint: API response was malformed")
diff --git a/traffic_ops/traffic_ops_golang/server/servers_test.go b/traffic_ops/traffic_ops_golang/server/servers_test.go
index bc15429d9..56199998b 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_test.go
@@ -265,7 +265,7 @@ func TestGetServersByCachegroup(t *testing.T) {
 
 	version := api.Version{Major: 4, Minor: 0}
 
-	servers, _, userErr, sysErr, errCode, _ := getServers(nil, v, db.MustBegin(), &user, false, version)
+	servers, _, userErr, sysErr, errCode, _ := getServers(nil, v, db.MustBegin(), &user, false, version, false)
 	if userErr != nil || sysErr != nil {
 		t.Errorf("getServers expected: no errors, actual: %v %v with status: %s", userErr, sysErr, http.StatusText(errCode))
 	}
@@ -380,7 +380,7 @@ func TestGetMidServers(t *testing.T) {
 
 	user := auth.CurrentUser{}
 	version := api.Version{Major: 4, Minor: 0}
-	servers, _, userErr, sysErr, errCode, _ := getServers(nil, v, db.MustBegin(), &user, false, version)
+	servers, _, userErr, sysErr, errCode, _ := getServers(nil, v, db.MustBegin(), &user, false, version, false)
 
 	if userErr != nil || sysErr != nil {
 		t.Errorf("getServers expected: no errors, actual: %v %v with status: %s", userErr, sysErr, http.StatusText(errCode))