You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2021/10/01 18:16:52 UTC

[trafficcontrol] branch master updated: Role and User struct changes for permission based roles (#6124)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0147097  Role and User struct changes for permission based roles (#6124)
0147097 is described below

commit 0147097bb4664307cfce3ebcfe0ae5cc14cac26d
Author: Srijeet Chatterjee <30...@users.noreply.github.com>
AuthorDate: Fri Oct 1 12:16:42 2021 -0600

    Role and User struct changes for permission based roles (#6124)
    
    * Role and User struct changes for permission based roles
    
    more changes to structs
    
    code review fixes
    
    fix user queries
    
    do not use cruder for update and create user
    
    make user v4 use non cruder methods
    
    add change logs
    
    changing update query for user v5
    
    copy user ID before update
    
    return a 400 instead of a 500 when user email already exists
    
    keeping consistent behavior for creates
    
    Revert "fix TP tests"
    
    This reverts commit 997692518db55c14a2087663b251585740e48e32.
    
    add comments
    
    back out all v5 changes
    
    backing out v5 changes for roles
    
    formatting and doc changes
    
    fix json in docs
    
    use string roles instead on ints in TP
    
    fix tests, hopefully
    
    dont GET the role name in TP, identify it directly
    
    change rolename to role in CIAB
    
    * adding TP changes for changes in role struct
    
    * code review fixes
    
    * fix docs
    
    * code review comments
    
    * removing version checkng from v4 handlers
    
    * code review fixes
    
    * address code review feedback
    
    * add ability for users to be filtered by capability name
    
    * construct version dynamically for location header
    
    * use the correct format specifier
    
    * fix TP create role view
---
 docs/source/api/v4/roles.rst                       | 111 ++--
 docs/source/api/v4/user_current.rst                |  46 +-
 docs/source/api/v4/users.rst                       |  58 +-
 docs/source/api/v4/users_id.rst                    |  85 ++-
 infrastructure/cdn-in-a-box/enroller/enroller.go   |   8 +-
 .../traffic_ops_data/users/010-tmonitor.json       |   2 +-
 .../traffic_ops_data/users/020-tvault.json         |   2 +-
 .../traffic_ops_data/users/030-tstats.json         |   2 +-
 lib/go-tc/roles.go                                 |  74 +++
 lib/go-tc/roles_test.go                            | 102 ++++
 lib/go-tc/users.go                                 | 343 +++++++++++-
 lib/go-tc/users_test.go                            | 414 ++++++++++++++
 traffic_ops/testing/api/v2/tc-fixtures.json        |   3 +-
 traffic_ops/testing/api/v3/tc-fixtures.json        |   3 +-
 traffic_ops/testing/api/v4/cachegroups_test.go     |  20 +-
 traffic_ops/testing/api/v4/cdn_locks_test.go       | 114 ++--
 traffic_ops/testing/api/v4/cdns_test.go            |  20 +-
 traffic_ops/testing/api/v4/crconfig_test.go        |  26 +-
 .../testing/api/v4/deliveryservices_test.go        |  20 +-
 traffic_ops/testing/api/v4/jobs_test.go            |  10 +-
 traffic_ops/testing/api/v4/loginfail_test.go       |   2 +-
 .../testing/api/v4/profile_parameters_test.go      |  20 +-
 traffic_ops/testing/api/v4/profiles_test.go        |  20 +-
 traffic_ops/testing/api/v4/roles_test.go           | 139 ++---
 traffic_ops/testing/api/v4/servers_test.go         |  20 +-
 .../testing/api/v4/staticdnsentries_test.go        |  20 +-
 traffic_ops/testing/api/v4/tc-fixtures.json        |  32 +-
 traffic_ops/testing/api/v4/topologies_test.go      |  20 +-
 traffic_ops/testing/api/v4/traffic_control_test.go |   4 +-
 traffic_ops/testing/api/v4/user_test.go            | 140 +----
 .../apicapability/api_capabilities.go              |   6 +
 .../traffic_ops_golang/dbhelpers/db_helpers.go     | 106 +++-
 traffic_ops/traffic_ops_golang/login/register.go   | 105 ++--
 traffic_ops/traffic_ops_golang/role/roles.go       | 359 ++++++++++++-
 traffic_ops/traffic_ops_golang/routing/routes.go   |  17 +-
 traffic_ops/traffic_ops_golang/user/current.go     |  46 +-
 traffic_ops/traffic_ops_golang/user/user.go        | 595 ++++++++++++++++++++-
 traffic_ops/v4-client/role.go                      |  15 +-
 traffic_ops/v4-client/user.go                      |  48 +-
 traffic_portal/app/src/common/api/RoleService.js   |   8 +-
 .../form/role/edit/FormEditRoleController.js       |   2 +-
 .../common/modules/form/role/form.role.tpl.html    |  10 -
 .../common/modules/form/user/FormUserController.js |   4 +-
 .../common/modules/form/user/form.user.tpl.html    |   4 +-
 .../user/register/FormRegisterUserController.js    |   4 +-
 .../form/user/register/form.user.register.tpl.html |   2 +-
 .../table.roleCapabilities.tpl.html                |   2 +-
 .../table/roleUsers/table.roleUsers.tpl.html       |   2 +-
 .../modules/table/roles/TableRolesController.js    |   4 +-
 .../modules/table/roles/table.roles.tpl.html       |   4 +-
 .../modules/table/users/table.users.tpl.html       |   2 +-
 .../modules/private/roles/capabilities/index.js    |   4 +-
 .../app/src/modules/private/roles/edit/index.js    |   4 +-
 .../app/src/modules/private/roles/list/index.js    |   2 +-
 .../app/src/modules/private/roles/users/index.js   |   4 +-
 .../app/src/modules/private/user/UserController.js |   2 +-
 .../private/user/edit/UserEditController.js        |   2 +-
 .../test/integration/Data/prerequisites.ts         |  96 +---
 58 files changed, 2520 insertions(+), 819 deletions(-)

diff --git a/docs/source/api/v4/roles.rst b/docs/source/api/v4/roles.rst
index a54d09c..3618d92 100644
--- a/docs/source/api/v4/roles.rst
+++ b/docs/source/api/v4/roles.rst
@@ -38,8 +38,6 @@ Request Structure
 	+-----------+----------+---------------------------------------------------------------------------------------------------------------+
 	| name      | no       | Return only the :term:`Role` with this name                                                                   |
 	+-----------+----------+---------------------------------------------------------------------------------------------------------------+
-	| privLevel | no       | Return only those :term:`Roles` that have this privilege level                                                |
-	+-----------+----------+---------------------------------------------------------------------------------------------------------------+
 	| orderby   | no       | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response`` |
 	|           |          | array                                                                                                         |
 	+-----------+----------+---------------------------------------------------------------------------------------------------------------+
@@ -57,7 +55,7 @@ Request Structure
 .. code-block:: http
 	:caption: Request Example
 
-	GET /api/4.0/roles?name=admin HTTP/1.1
+	GET /api/4.0/roles?name=read-only HTTP/1.1
 	Host: trafficops.infra.ciab.test
 	User-Agent: curl/7.47.0
 	Accept: */*
@@ -65,11 +63,10 @@ Request Structure
 
 Response Structure
 ------------------
-:capabilities: An array of the names of the Capabilities given to this :term:`Role`
+:permissions:  An array of the names of the Permissions given to this :term:`Role`
 :description:  A description of the :term:`Role`
 :id:           The integral, unique identifier for this :term:`Role`
 :name:         The name of the :term:`Role`
-:privLevel:    An integer that allows for comparison between :term:`Roles`
 
 .. code-block:: http
 	:caption: Response Example
@@ -83,19 +80,54 @@ Response Structure
 	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
 	Whole-Content-Sha512: TEDXlQqWMSnJbL10JtFdbw0nqciNpjc4bd6m7iAB8aymakWeF+ghs1k5LayjdzHcjeDE8UNF/HXSxOFvoLFEuA==
 	X-Server-Name: traffic_ops_golang/
-	Date: Wed, 04 Sep 2019 17:15:36 GMT
-	Content-Length: 120
+	Date: Wed, 25 Aug 2021 20:10:34 GMT
+	Content-Length: 888
 
 	{ "response": [
 		{
-			"id": 4,
-			"name": "admin",
-			"description": "super-user",
-			"privLevel": 30,
-			"capabilities": [
-				"all-write",
-				"all-read"
-			]
+			"name": "read-only",
+			"description": "Has access to all read capabilities",
+			"permissions": [
+				"auth",
+				"api-endpoints-read",
+				"asns-read",
+				"cache-config-files-read",
+				"cache-groups-read",
+				"capabilities-read",
+				"cdns-read",
+				"cdn-security-keys-read",
+				"change-logs-read",
+				"consistenthash-read",
+				"coordinates-read",
+				"delivery-services-read",
+				"delivery-service-security-keys-read",
+				"delivery-service-requests-read",
+				"delivery-service-servers-read",
+				"divisions-read",
+				"to-extensions-read",
+				"federations-read",
+				"hwinfo-read",
+				"jobs-read",
+				"origins-read",
+				"parameters-read",
+				"phys-locations-read",
+				"profiles-read",
+				"regions-read",
+				"roles-read",
+				"server-capabilities-read",
+				"servers-read",
+				"service-categories-read",
+				"stats-read",
+				"statuses-read",
+				"static-dns-entries-read",
+				"steering-read",
+				"steering-targets-read",
+				"system-info-read",
+				"tenants-read",
+				"types-read",
+				"users-read"
+			],
+			"lastUpdated": "2021-05-03T14:50:18.93513-06:00",
 		}
 	]}
 
@@ -109,10 +141,9 @@ Creates a new :term:`Role`.
 
 Request Structure
 -----------------
-:capabilities: An optional array of capability names that will be granted to the new :term:`Role`
+:permissions:  An optional array of permission names that will be granted to the new :term:`Role`\ [#permissions]_
 :description:  A helpful description of the :term:`Role`'s purpose.
 :name:         The name of the new :term:`Role`
-:privLevel:    The privilege level of the new :term:`Role`\ [#privlevel]_
 
 .. code-block:: http
 	:caption: Request Example
@@ -127,21 +158,19 @@ Request Structure
 
 	{
 		"name": "test",
-		"description": "quest",
-		"privLevel": 30
+		"description": "quest"
 	}
 
 
 Response Structure
 ------------------
-:capabilities: An array of the names of the Capabilities given to this :term:`Role`
+:permissions: An array of the names of the Permissions given to this :term:`Role`
 
 	.. tip:: This can be ``null`` *or* empty, depending on whether it was present in the request body, or merely empty. Obviously, it can also be a populated array.
 
 :description: A description of the :term:`Role`
 :id:          The integral, unique identifier for this :term:`Role`
 :name:        The name of the :term:`Role`
-:privLevel:   An integer that allows for comparison between :term:`Roles`
 
 .. code-block:: http
 	:caption: Response Example
@@ -156,18 +185,16 @@ Response Structure
 	Whole-Content-Sha512: gzfc7m/in5vVsVP+Y9h6JJfDhgpXKn9VAzoiPENhKbQfP8Q6jug08Rt2AK/3Nz1cx5zZ8P9IjVxDdIg7mlC8bw==
 	X-Server-Name: traffic_ops_golang/
 	Date: Wed, 04 Sep 2019 17:44:42 GMT
-	Content-Length: 150
+	Content-Length: 128
 
 	{ "alerts": [{
 		"text": "role was created.",
 		"level": "success"
 	}],
 	"response": {
-		"id": 5,
 		"name": "test",
 		"description": "quest",
-		"privLevel": 30,
-		"capabilities": null
+		"permissions": null
 	}}
 
 ``PUT``
@@ -185,21 +212,20 @@ Request Structure
 	+------+----------+--------------------------------------------------------------------+
 	| Name | Required | Description                                                        |
 	+======+==========+====================================================================+
-	| id   | yes      | The integral, unique identifier of the :term:`Role` to be replaced |
+	| name | yes      | The name of the :term:`Role` to be updated                         |
 	+------+----------+--------------------------------------------------------------------+
 
-:capabilities: An optional array of capability names that will be granted to the new :term:`Role`
+:permissions: An optional array of permission names that will be granted to the new :term:`Role`
 
-	.. warning:: When not present, the affected :term:`Role`'s Capabilities will be unchanged - *not* removed, unlike when the array is empty.
+	.. warning:: When not present, the affected :term:`Role`'s Permissions will be unchanged - *not* removed, unlike when the array is empty.
 
 :description: A helpful description of the :term:`Role`'s purpose.
 :name:        The new name of the :term:`Role`
-:privLevel:   The new privilege level of the new :term:`Role`\ [#privlevel]_
 
 .. code-block:: http
 	:caption: Request Example
 
-	PUT /api/4.0/roles?id=5 HTTP/1.1
+	PUT /api/4.0/roles?name=test HTTP/1.1
 	Host: trafficops.infra.ciab.test
 	User-Agent: curl/7.47.0
 	Accept: */*
@@ -209,22 +235,19 @@ Request Structure
 
 	{
 		"name":"test",
-		"privLevel": 29,
-		"description": "quest"
+		"description": "quest_updated"
 	}
 
 Response Structure
 ------------------
-:capabilities: An array of the names of the Capabilities given to this :term:`Role`
+:permissions: An array of the names of the Permissions given to this :term:`Role`
 
 	.. tip:: This can be ``null`` *or* empty, depending on whether it was present in the request body, or merely empty. Obviously, it can also be a populated array.
 
-	.. warning:: If no ``capabilities`` array was given in the request, this will *always* be ``null``, even if the :term:`Role` has Capabilities that would have gone unchanged.
+	.. warning:: If no ``permissions`` array was given in the request, this will *always* be ``null``, even if the :term:`Role` has Permissions that would have gone unchanged.
 
 :description: A description of the :term:`Role`
-:id:          The integral, unique identifier for this :term:`Role`
 :name:        The name of the :term:`Role`
-:privLevel:   An integer that allows for comparison between :term:`Roles`
 
 .. code-block:: http
 	:caption: Response Example
@@ -239,7 +262,7 @@ Response Structure
 	Whole-Content-Sha512: mlHQenE1Q3gjrIK2lC2hfueQOaTCpdYEfboN0A9vYPUIwTiaF5ZaAMPQBdfGyiAhgHRxowITs3bR7s1L++oFTQ==
 	X-Server-Name: traffic_ops_golang/
 	Date: Thu, 05 Sep 2019 12:56:46 GMT
-	Content-Length: 150
+	Content-Length: 136
 
 	{
 		"alerts": [
@@ -249,11 +272,9 @@ Response Structure
 			}
 		],
 		"response": {
-			"id": 5,
 			"name": "test",
-			"description": "quest",
-			"privLevel": 29,
-			"capabilities": null
+			"description": "quest_updated",
+			"permissions": null
 		}
 	}
 
@@ -273,13 +294,13 @@ Request Structure
 	+------+----------+--------------------------------------------------------------------+
 	| Name | Required | Description                                                        |
 	+======+==========+====================================================================+
-	| id   | yes      | The integral, unique identifier of the :term:`Role` to be replaced |
+	| name | yes      | The name of the :term:`Role` to be deleted                         |
 	+------+----------+--------------------------------------------------------------------+
 
 .. code-block:: http
 	:caption: Request Example
 
-	DELETE /api/4.0/roles?id=5 HTTP/1.1
+	DELETE /api/4.0/roles?name=test HTTP/1.1
 	Host: trafficops.infra.ciab.test
 	User-Agent: curl/7.47.0
 	Accept: */*
@@ -300,11 +321,11 @@ Response Structure
 	Whole-Content-Sha512: 10jeFZihtbvAus/XyHAW8rhgS9JBD+X/ezCp1iExYkEcHxN4gjr1L6x8zDFXORueBSlFldgtbWKT7QsmwCHUWA==
 	X-Server-Name: traffic_ops_golang/
 	Date: Thu, 05 Sep 2019 13:02:06 GMT
-	Content-Length: 59
+	Content-Length: 60
 
 	{ "alerts": [{
 		"text": "role was deleted.",
 		"level": "success"
 	}]}
 
-.. [#privlevel] ``privLevel`` cannot exceed the privilege level of the requesting user. Which, of course, must be the privilege level of "admin". Basically, this means that there can never exist a :term:`Role` with a higher privilege level than "admin".
+.. [#permissions] ``permissions`` cannot include permissions that are not included in the permissions of the requesting user.
diff --git a/docs/source/api/v4/user_current.rst b/docs/source/api/v4/user_current.rst
index b51de38..667a14d 100644
--- a/docs/source/api/v4/user_current.rst
+++ b/docs/source/api/v4/user_current.rst
@@ -51,8 +51,7 @@ Response Structure
 :postalCode:        The postal code of the area in which the user resides
 :publicSshKey:      The user's public key used for the SSH protocol
 :registrationSent:  If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
-:role:              The integral, unique identifier of the highest-privilege :term:`Role` assigned to this user
-:rolename:          The name of the highest-privilege :term:`Role` assigned to this user
+:role:              The name of the :term:`Role` assigned to this user
 :stateOrProvince:   The name of the state or province where this user resides
 :tenant:            The name of the :term:`Tenant` to which this user belongs
 :tenantId:          The integral, unique identifier of the :term:`Tenant` to which this user belongs
@@ -72,32 +71,31 @@ Response Structure
 	Whole-Content-Sha512: HQwu9FxFyinXSVFK5+wpEhSxU60KbqXuokFbMZ3OoerOoM5ZpWpglsHz7mRch8VAw0dzwsJzpPJivj07RiKaJg==
 	X-Server-Name: traffic_ops_golang/
 	Date: Thu, 13 Dec 2018 15:14:45 GMT
-	Content-Length: 382
+	Content-Length: 631
 
 	{ "response": {
 		"username": "admin",
 		"localUser": true,
-		"addressLine1": null,
-		"addressLine2": null,
-		"city": null,
-		"company": null,
-		"country": null,
-		"email": null,
-		"fullName": "admin",
+		"addressLine1": "not a real address",
+		"addressLine2": "not a real address either",
+		"city": "not a real city",
+		"company": "not a real company",
+		"country": "not a real country",
+		"email": "not@real.email",
+		"fullName": "Not a real fullName",
 		"gid": null,
 		"id": 2,
 		"newUser": false,
-		"phoneNumber": null,
-		"postalCode": null,
-		"publicSshKey": null,
-		"role": 1,
-		"rolename": "admin",
-		"stateOrProvince": null,
+		"phoneNumber": "not a real phone number",
+		"postalCode": "not a real postal code",
+		"publicSshKey": "not a real ssh key",
+		"role": "admin",
+		"stateOrProvince": "not a real state or province",
 		"tenant": "root",
 		"tenantId": 1,
 		"uid": null,
-		"lastUpdated": "2018-12-12 16:26:32+00",
-		"lastAuthenticated": "2021-07-09T14:44:10.371708-06:00"
+		"lastUpdated": "2021-09-16T09:55:09.309863-06:00",
+		"lastAuthenticated": "2021-09-16T09:55:09.309863-06:00"
 	}}
 
 ``PUT``
@@ -144,7 +142,7 @@ Request Structure
 	User-Agent: curl/7.47.0
 	Accept: */*
 	Cookie: mojolicious=...
-	Content-Length: 465
+	Content-Length: 469
 	Content-Type: application/json
 
 	{ "user": {
@@ -160,7 +158,7 @@ Request Structure
 		"phoneNumber": null,
 		"postalCode": null,
 		"publicSshKey": null,
-		"role": 1,
+		"role": "admin",
 		"stateOrProvince": null,
 		"tenantId": 1,
 		"uid": null,
@@ -184,8 +182,7 @@ Response Structure
 :postalCode:       The postal code of the area in which the user resides
 :publicSshKey:     The user's public key used for the SSH protocol
 :registrationSent: If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
-:role:             The integral, unique identifier of the highest-privilege :term:`Role` assigned to this user
-:rolename:         The name of the highest-privilege :term:`Role` assigned to this user
+:role:             The name of the :term:`Role` assigned to this user
 :stateOrProvince:  The name of the state or province where this user resides
 :tenant:           The name of the :term:`Tenant` to which this user belongs
 :tenantId:         The integral, unique identifier of the :term:`Tenant` to which this user belongs
@@ -206,7 +203,7 @@ Response Structure
 	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
 	Vary: Accept-Encoding
 	Whole-Content-Sha512: sHFqZQ4Cv7IIWaIejoAvM2Fr/HSupcX3D16KU/etjw+4jcK9EME3Bq5ohLC+eQ52BDCKW2Ra+AC3TfFtworJww==
-	Content-Length: 478
+	Content-Length: 462
 
 	{ "alerts": [
 		{
@@ -230,8 +227,7 @@ Response Structure
 		"postalCode": null,
 		"publicSshKey": null,
 		"registrationSent": null,
-		"role": 1,
-		"roleName": "admin",
+		"role": "admin",
 		"stateOrProvince": null,
 		"tenant": "root",
 		"tenantId": 1,
diff --git a/docs/source/api/v4/users.rst b/docs/source/api/v4/users.rst
index 658e191..92fd924 100644
--- a/docs/source/api/v4/users.rst
+++ b/docs/source/api/v4/users.rst
@@ -85,8 +85,7 @@ Response Structure
 :postalCode:        The postal code of the area in which the user resides
 :publicSshKey:      The user's public key used for the SSH protocol
 :registrationSent:  If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
-:role:              The integral, unique identifier of the highest-privilege role assigned to this user
-:rolename:          The name of the highest-privilege role assigned to this user
+:role:              The name of the role assigned to this user
 :stateOrProvince:   The name of the state or province where this user resides
 :tenant:            The name of the tenant to which this user belongs
 :tenantId:          The integral, unique identifier of the tenant to which this user belongs
@@ -106,7 +105,7 @@ Response Structure
 	Whole-Content-Sha512: YBJLN8NbOxOvECe1RGtcwCzIPDhyhLpW56nTJHQM5WI2WUDe2mAKREpaEE72nzrfBliq1GABwJlsxq2OdhcFkw==
 	X-Server-Name: traffic_ops_golang/
 	Date: Thu, 13 Dec 2018 01:03:53 GMT
-	Content-Length: 391
+	Content-Length: 454
 
 	{ "response": [
 		{
@@ -125,13 +124,12 @@ Response Structure
 			"phoneNumber": null,
 			"postalCode": null,
 			"publicSshKey": null,
-			"role": 1,
-			"rolename": "admin",
+			"role": "admin",
 			"stateOrProvince": null,
 			"tenant": "root",
 			"tenantId": 1,
 			"uid": null,
-			"lastUpdated": "2018-12-12 16:26:32+00",
+			"lastUpdated": "2021-08-25T14:08:13.974447-06:00",
 			"changeLogCount": 20,
 			"lastAuthenticated": "2021-07-09T14:44:10.371708-06:00"
 		}
@@ -162,7 +160,7 @@ Request Structure
 :phoneNumber:        An optional field which should contain the user's phone number
 :postalCode:         An optional field which should contain the user's postal code
 :publicSshKey:       An optional field which should contain the user's public encryption key used for the SSH protocol
-:role:               The number that corresponds to the highest permission role which will be permitted to the user
+:role:               The name that corresponds to the highest permission role which will be permitted to the user
 :stateOrProvince:    An optional field which should contain the name of the state or province in which the user resides
 :tenantId:           The integral, unique identifier of the tenant to which the new user shall belong
 
@@ -191,7 +189,7 @@ Request Structure
 		"localPasswd": "BFFsully",
 		"confirmLocalPasswd": "BFFsully",
 		"newUser": true,
-		"role": 1,
+		"role": "admin",
 		"tenantId": 1
 	}
 
@@ -212,8 +210,7 @@ Response Structure
 :postalCode:       The postal code of the area in which the user resides
 :publicSshKey:     The user's public key used for the SSH protocol
 :registrationSent: If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
-:role:             The integral, unique identifier of the highest-privilege role assigned to this user
-:roleName:         The name of the highest-privilege role assigned to this user
+:role:             The name of the role assigned to this user
 :stateOrProvince:  The name of the state or province where this user resides
 :tenant:           The name of the tenant to which this user belongs
 :tenantId:         The integral, unique identifier of the tenant to which this user belongs
@@ -223,47 +220,50 @@ Response Structure
 .. code-block:: http
 	:caption: Response Example
 
-	HTTP/1.1 200 OK
+	HTTP/1.1 201 Created
 	Access-Control-Allow-Credentials: true
 	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept
 	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
 	Access-Control-Allow-Origin: *
 	Cache-Control: no-cache, no-store, max-age=0, must-revalidate
 	Content-Type: application/json
+	Location: /api/4.0/users?id=44
 	Date: Thu, 13 Dec 2018 02:28:27 GMT
 	X-Server-Name: traffic_ops_golang/
 	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
 	Vary: Accept-Encoding
 	Whole-Content-Sha512: vDqbaMvgeeoIds1czqvIWlyDG8WLnCCJdF14Ub05nsE+oJOakkyeZ8odf4d0Zjtqpk01hoVo14H2tjuWPdqwgw==
-	Content-Length: 520
+	Content-Length: 623
 
 	{ "alerts": [
 		{
 			"level": "success",
-			"text": "User creation was successful."
+			"text": "user was created."
 		}
 	],
 	"response": {
-		"registrationSent": null,
-		"email": "mwazowski@minc.biz",
-		"tenantId": 1,
+		"addressLine1": "22 Mike Wazowski You've Got Your Life Back Lane",
+		"addressLine2": null,
+		"changeLogCount": null,
 		"city": "Monstropolis",
-		"tenant": "root",
-		"id": 8,
 		"company": null,
-		"roleName": "admin",
-		"phoneNumber": null,
+		"confirmLocalPasswd": "BFFsully",
 		"country": null,
+		"email": "mwazowski@minc.biz",
 		"fullName": "Mike Wazowski",
+		"gid": null,
+		"id": 26,
+		"lastAuthenticated": "2021-07-09T14:44:10.371708-06:00",
+		"lastUpdated": "2021-08-25T14:43:10.466412-06:00",
+		"newUser": true,
+		"phoneNumber": null,
+		"postalCode": null,
 		"publicSshKey": null,
-		"uid": null,
+		"registrationSent": null,
+		"role": "admin",
 		"stateOrProvince": null,
-		"lastUpdated": null,
-		"username": "mike",
-		"newUser": false,
-		"addressLine2": null,
-		"role": 1,
-		"addressLine1": "22 Mike Wazowski You've Got Your Life Back Lane",
-		"postalCode": null,
-		"gid": null
+		"tenant": "root",
+		"tenantId": 1,
+		"uid": null,
+		"username": "mike"
 	}}
diff --git a/docs/source/api/v4/users_id.rst b/docs/source/api/v4/users_id.rst
index e4dac10..233ce55 100644
--- a/docs/source/api/v4/users_id.rst
+++ b/docs/source/api/v4/users_id.rst
@@ -64,8 +64,7 @@ Response Structure
 :postalCode:        The postal code of the area in which the user resides
 :publicSshKey:      The user's public key used for the SSH protocol
 :registrationSent:  If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
-:role:              The integral, unique identifier of the highest-privilege role assigned to this user
-:rolename:          The name of the highest-privilege role assigned to this user
+:role:              The name of the role assigned to this user
 :stateOrProvince:   The name of the state or province where this user resides
 :tenant:            The name of the tenant to which this user belongs
 :tenantId:          The integral, unique identifier of the tenant to which this user belongs
@@ -85,33 +84,33 @@ Response Structure
 	Whole-Content-Sha512: 9vqUmt8fWEuDb+9LQJ4sGbbF4Z0a7uNyBNSWhyzAi3fBUZ5mGhd4Jx5IuSlEqiLZnYeViJJL8mpRortkHCgp5Q==
 	X-Server-Name: traffic_ops_golang/
 	Date: Thu, 13 Dec 2018 17:46:00 GMT
-	Content-Length: 588
+	Content-Length: 454
 
 	{ "response": [
 		{
-			"username": "admin",
-			"registrationSent": null,
-			"addressLine1": "not a real address",
-			"addressLine2": "not a real address either",
-			"city": "not a real city",
-			"company": "not a real company",
-			"country": "not a real country",
-			"email": "not@real.email",
-			"fullName": "Not a real Full Name",
+			"addressLine1": null,
+			"addressLine2": null,
+			"changeLogCount": null,
+			"city": null,
+			"company": null,
+			"country": null,
+			"email": null,
+			"fullName": null,
 			"gid": null,
 			"id": 2,
+			"lastAuthenticated": "0001-01-01T00:00:00Z",
+			"lastUpdated": "2021-08-25T14:08:13.974447-06:00",
 			"newUser": false,
-			"phoneNumber": "not a real phone number",
-			"postalCode": "not a real postal code",
-			"publicSshKey": "not a real ssh key",
-			"role": 1,
-			"rolename": "admin",
-			"stateOrProvince": "not a real state or province",
+			"phoneNumber": null,
+			"postalCode": null,
+			"publicSshKey": null,
+			"registrationSent": null,
+			"role": "admin",
+			"stateOrProvince": null,
 			"tenant": "root",
 			"tenantId": 1,
 			"uid": null,
-			"lastUpdated": "2018-12-13 17:24:23+00",
-			"lastAuthenticated": "2021-07-09T14:44:10.371708-06:00"
+			"username": "admin"
 		}
 	]}
 
@@ -151,7 +150,7 @@ Request Structure
 
 	.. note:: This field is optional if and only if tenancy is not enabled in Traffic Control
 
-:username: The new user's username
+:username: The user's username
 
 .. code-block:: http
 	:caption: Request Structure
@@ -177,7 +176,7 @@ Request Structure
 		"publicSshKey": "not a real ssh key",
 		"stateOrProvince": "not a real state or province",
 		"tenantId": 1,
-		"role": 1,
+		"role": "admin",
 		"username": "admin"
 	}
 
@@ -198,8 +197,7 @@ Response Structure
 :postalCode:       The postal code of the area in which the user resides
 :publicSshKey:     The user's public key used for the SSH protocol
 :registrationSent: If the user was created using the :ref:`to-api-users-register` endpoint, this will be the date and time at which the registration email was sent - otherwise it will be ``null``
-:role:             The integral, unique identifier of the highest-privilege role assigned to this user
-:roleName:         The name of the highest-privilege role assigned to this user
+:role:             The name of the role assigned to this user
 :stateOrProvince:  The name of the state or province where this user resides
 :tenant:           The name of the tenant to which this user belongs
 :tenantId:         The integral, unique identifier of the tenant to which this user belongs
@@ -221,35 +219,36 @@ Response Structure
 	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
 	Vary: Accept-Encoding
 	Whole-Content-Sha512: QKvGSIwSdreMI/OdgWv9WQfI/C1JbXSoQGGospTGfCVUJ32XNWMhmREGzojWsilW8os8b14TGYeyMLUWunf2Ug==
-	Content-Length: 661
+	Content-Length: 478
 
 	{ "alerts": [
 		{
 			"level": "success",
-			"text": "User update was successful."
+			"text": "user was updated."
 		}
 	],
 	"response": {
-		"registrationSent": null,
-		"email": "not@real.email",
-		"tenantId": 1,
+		"addressLine1": "not a real address",
+		"addressLine2": "not a real address either",
+		"changeLogCount": null,
 		"city": "not a real city",
-		"tenant": "root",
-		"id": 2,
 		"company": "not a real company",
-		"roleName": "admin",
-		"phoneNumber": "not a real phone number",
 		"country": "not a real country",
-		"fullName": "Not a real Full Name",
-		"publicSshKey": "not a real ssh key",
-		"uid": null,
-		"stateOrProvince": "not a real state or province",
-		"lastUpdated": "2018-12-12 16:26:32.821187+00",
-		"username": "admin",
+		"email": "not@real.email",
+		"fullName": "Not a real fullName",
+		"gid": null,
+		"id": 2,
+		"lastAuthenticated": "2021-07-09T14:44:10.371708-06:00",
+		"lastUpdated": "2021-08-25T15:05:16.32163-06:00",
 		"newUser": false,
-		"addressLine2": "not a real address either",
-		"role": 1,
-		"addressLine1": "not a real address",
+		"phoneNumber": "not a real phone number",
 		"postalCode": "not a real postal code",
-		"gid": null
+		"publicSshKey": "not a real ssh key",
+		"registrationSent": null,
+		"role": "admin",
+		"stateOrProvince": "not a real state or province",
+		"tenant": "root",
+		"tenantId": 1,
+		"uid": null,
+		"username": "admin"
 	}}
diff --git a/infrastructure/cdn-in-a-box/enroller/enroller.go b/infrastructure/cdn-in-a-box/enroller/enroller.go
index 47769e1..a773532 100644
--- a/infrastructure/cdn-in-a-box/enroller/enroller.go
+++ b/infrastructure/cdn-in-a-box/enroller/enroller.go
@@ -586,7 +586,7 @@ func enrollTenant(toSession *session, r io.Reader) error {
 
 func enrollUser(toSession *session, r io.Reader) error {
 	dec := json.NewDecoder(r)
-	var s tc.UserV40
+	var s tc.UserV4
 	err := dec.Decode(&s)
 	log.Infof("User is %++v\n", s)
 	if err != nil {
@@ -598,7 +598,7 @@ func enrollUser(toSession *session, r io.Reader) error {
 	if err != nil {
 		for _, alert := range alerts.Alerts.Alerts {
 			if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
-				log.Infof("user %s already exists\n", *s.Username)
+				log.Infof("user %s already exists\n", s.Username)
 				return nil
 			}
 		}
@@ -870,9 +870,7 @@ func enrollFederation(toSession *session, r io.Reader) error {
 			resp, _, err := toSession.CreateFederationUsers(*cdnFederation.ID, []int{*user.Response.ID}, true, client.RequestOptions{})
 			if err != nil {
 				var username string
-				if user.Response.UserName != nil {
-					username = *user.Response.UserName
-				}
+				username = user.Response.UserName
 				err = fmt.Errorf("assigning User '%s' to Federation with ID %d: %v - alerts: %+v", username, *cdnFederation.ID, err, resp.Alerts)
 				log.Infoln(err)
 				return err
diff --git a/infrastructure/cdn-in-a-box/traffic_ops_data/users/010-tmonitor.json b/infrastructure/cdn-in-a-box/traffic_ops_data/users/010-tmonitor.json
index 5e0791f..639469a 100644
--- a/infrastructure/cdn-in-a-box/traffic_ops_data/users/010-tmonitor.json
+++ b/infrastructure/cdn-in-a-box/traffic_ops_data/users/010-tmonitor.json
@@ -3,7 +3,7 @@
   "fullName": "Traffic Monitor",
   "localPasswd": "$TM_PASSWORD",
   "confirmLocalPasswd": "$TM_PASSWORD",
-  "rolename": "operations",
+  "role": "operations",
   "tenant": "root",
   "username": "$TM_USER"
 }
diff --git a/infrastructure/cdn-in-a-box/traffic_ops_data/users/020-tvault.json b/infrastructure/cdn-in-a-box/traffic_ops_data/users/020-tvault.json
index 0f4672d..b59672d 100644
--- a/infrastructure/cdn-in-a-box/traffic_ops_data/users/020-tvault.json
+++ b/infrastructure/cdn-in-a-box/traffic_ops_data/users/020-tvault.json
@@ -3,7 +3,7 @@
   "fullName": "Traffic Vault",
   "localPasswd": "$TV_PASSWORD",
   "confirmLocalPasswd": "$TV_PASSWORD",
-  "rolename": "operations",
+  "role": "operations",
   "tenant": "root",
   "username": "$TV_USER"
 }
diff --git a/infrastructure/cdn-in-a-box/traffic_ops_data/users/030-tstats.json b/infrastructure/cdn-in-a-box/traffic_ops_data/users/030-tstats.json
index dda460e..576ba90 100644
--- a/infrastructure/cdn-in-a-box/traffic_ops_data/users/030-tstats.json
+++ b/infrastructure/cdn-in-a-box/traffic_ops_data/users/030-tstats.json
@@ -3,7 +3,7 @@
   "fullName": "Traffic Stats",
   "localPasswd": "$TS_PASSWORD",
   "confirmLocalPasswd": "$TS_PASSWORD",
-  "rolename": "operations",
+  "role": "operations",
   "tenant": "root",
   "username": "$TS_USER"
 }
diff --git a/lib/go-tc/roles.go b/lib/go-tc/roles.go
index e17022a..a9687ac 100644
--- a/lib/go-tc/roles.go
+++ b/lib/go-tc/roles.go
@@ -1,5 +1,14 @@
 package tc
 
+import (
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
+	"github.com/apache/trafficcontrol/lib/go-util"
+
+	validation "github.com/go-ozzo/ozzo-validation"
+)
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -19,6 +28,71 @@ package tc
  * under the License.
  */
 
+// RoleV4 is an alias for the latest minor version for the major version 4.
+type RoleV4 RoleV40
+
+// RolesResponseV4 is a list of RoleV4 as a response.
+type RolesResponseV4 struct {
+	Response []RoleV4 `json:"response"`
+	Alerts
+}
+
+// RoleResponseV4 is a RoleV4 as a response.
+type RoleResponseV4 struct {
+	Response RoleV4 `json:"response"`
+	Alerts
+}
+
+// RoleV40 is the structure used to depict roles in API v4.0.
+type RoleV40 struct {
+	Name        string     `json:"name" db:"name"`
+	Permissions []string   `json:"permissions" db:"permissions"`
+	Description string     `json:"description" db:"description"`
+	LastUpdated *time.Time `json:"lastUpdated,omitempty" db:"last_updated"`
+}
+
+// Validate will validate and make sure all that the fields in the supplied RoleV4 struct are semantically correct.
+func (role RoleV4) Validate() error {
+	errs := validation.Errors{
+		"name":        validation.Validate(role.Name, validation.Required),
+		"description": validation.Validate(role.Description, validation.Required),
+	}
+	return util.JoinErrs(tovalidate.ToErrors(errs))
+}
+
+// Upgrade will convert the passed in instance of Role struct into an instance of RoleV4 struct.
+func (role Role) Upgrade() RoleV4 {
+	var roleV4 RoleV4
+	if role.Name != nil {
+		roleV4.Name = *role.Name
+	}
+	if role.Description != nil {
+		roleV4.Description = *role.Description
+	}
+	if role.Capabilities == nil {
+		roleV4.Permissions = nil
+	} else {
+		roleV4.Permissions = make([]string, len(*role.Capabilities))
+		copy(roleV4.Permissions, *role.Capabilities)
+	}
+	return roleV4
+}
+
+// Downgrade will convert the passed in instance of RoleV4 struct into an instance of Role struct.
+func (roleV4 RoleV4) Downgrade() Role {
+	var role Role
+	role.Name = &roleV4.Name
+	role.Description = &roleV4.Description
+	if len(roleV4.Permissions) == 0 {
+		role.Capabilities = nil
+	} else {
+		caps := make([]string, len(roleV4.Permissions))
+		copy(caps, roleV4.Permissions)
+		role.Capabilities = &caps
+	}
+	return role
+}
+
 // RolesResponse is a list of Roles as a response.
 // swagger:response RolesResponse
 // in: body
diff --git a/lib/go-tc/roles_test.go b/lib/go-tc/roles_test.go
new file mode 100644
index 0000000..878128c
--- /dev/null
+++ b/lib/go-tc/roles_test.go
@@ -0,0 +1,102 @@
+package tc
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"testing"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-util"
+)
+
+func TestDowngrade(t *testing.T) {
+	roleV4 := RoleV4{
+		Name:        "rolev40_name",
+		Permissions: []string{"perm1", "perm2"},
+		Description: "rolev40_desc",
+		LastUpdated: &time.Time{},
+	}
+
+	role := roleV4.Downgrade()
+	if role.Name == nil {
+		t.Errorf("role name became nil after downgrade")
+	} else if *role.Name != roleV4.Name {
+		t.Errorf("expected role names to be the same after downgrade, but the new role name %s doesn't match the old role name %s", *role.Name, roleV4.Name)
+	}
+
+	if role.Description == nil {
+		t.Errorf("role description became nil after downgrade")
+	} else if *role.Description != roleV4.Description {
+		t.Errorf("expected role descriptions to be the same after downgrade, but the new role description %s doesn't match the old role description %s", *role.Description, roleV4.Description)
+	}
+
+	if role.Capabilities == nil {
+		t.Errorf("role capabilities became nil after downgrade")
+	} else if len(*role.Capabilities) != len(roleV4.Permissions) {
+		t.Errorf("new role capabilities length %d after downgrade doesn't match old role permissions length %d", len(*role.Capabilities), len(roleV4.Permissions))
+	} else {
+		oldPermissions := make(map[string]struct{})
+		for _, perm := range roleV4.Permissions {
+			oldPermissions[perm] = struct{}{}
+		}
+		for _, cap := range *role.Capabilities {
+			if _, ok := oldPermissions[cap]; !ok {
+				t.Errorf("permission %s did not exist earlier, but is present after downgrade", cap)
+			}
+		}
+	}
+}
+
+func TestUpgrade(t *testing.T) {
+	role := Role{
+		RoleV11: RoleV11{
+			ID:          util.IntPtr(100),
+			Name:        util.StrPtr("role_name"),
+			Description: util.StrPtr("role_desc"),
+			PrivLevel:   util.IntPtr(10),
+		},
+		Capabilities: &[]string{"cap1", "cap2"},
+	}
+
+	roleV4 := role.Upgrade()
+	if roleV4.Name != *role.Name {
+		t.Errorf("expected role names to be the same after upgrade, but the new role name %s doesn't match the old role name %s", roleV4.Name, *role.Name)
+	}
+
+	if *role.Description != roleV4.Description {
+		t.Errorf("expected role descriptions to be the same after upgrade, but the new role description %s doesn't match the old role description %s", roleV4.Description, *role.Description)
+	}
+
+	if &roleV4 == nil {
+		t.Errorf("role permissions became nil after upgrade")
+	} else if len(roleV4.Permissions) != len(*role.Capabilities) {
+		t.Errorf("new role permissions length %d after upgrade doesn't match old role capabilities length %d", len(roleV4.Permissions), len(*role.Capabilities))
+	} else {
+		oldCapabilities := make(map[string]struct{})
+		for _, perm := range *role.Capabilities {
+			oldCapabilities[perm] = struct{}{}
+		}
+		for _, perm := range roleV4.Permissions {
+			if _, ok := oldCapabilities[perm]; !ok {
+				t.Errorf("capability %s did not exist earlier, but is present after upgrade", perm)
+			}
+		}
+	}
+}
diff --git a/lib/go-tc/users.go b/lib/go-tc/users.go
index 482c280..1852c3b 100644
--- a/lib/go-tc/users.go
+++ b/lib/go-tc/users.go
@@ -33,6 +33,111 @@ import (
 	"github.com/go-ozzo/ozzo-validation/is"
 )
 
+// copyStringIfNotNil makes a deep copy of s - unless it's nil, in which case it
+// just returns nil.
+func copyStringIfNotNil(s *string) *string {
+	if s == nil {
+		return nil
+	}
+	ret := new(string)
+	*ret = *s
+	return ret
+}
+
+// copyIntIfNotNil makes a deep copy of i - unless it's nil, in which case it
+// just returns nil.
+func copyIntIfNotNil(i *int) *int {
+	if i == nil {
+		return nil
+	}
+	ret := new(int)
+	*ret = *i
+	return ret
+}
+
+// Upgrade converts a User to a UserV4 (as seen in API versions 4.x).
+func (u User) Upgrade() UserV4 {
+	var ret UserV4
+	ret.AddressLine1 = copyStringIfNotNil(u.AddressLine1)
+	ret.AddressLine2 = copyStringIfNotNil(u.AddressLine2)
+	ret.City = copyStringIfNotNil(u.City)
+	ret.Company = copyStringIfNotNil(u.Company)
+	ret.ConfirmLocalPassword = copyStringIfNotNil(u.ConfirmLocalPassword)
+	ret.Country = copyStringIfNotNil(u.Country)
+	ret.Email = copyStringIfNotNil(u.Email)
+	ret.GID = copyIntIfNotNil(u.GID)
+	ret.ID = copyIntIfNotNil(u.ID)
+	ret.LocalPassword = copyStringIfNotNil(u.LocalPassword)
+	ret.PhoneNumber = copyStringIfNotNil(u.PhoneNumber)
+	ret.PostalCode = copyStringIfNotNil(u.PostalCode)
+	ret.PublicSSHKey = copyStringIfNotNil(u.PublicSSHKey)
+	ret.StateOrProvince = copyStringIfNotNil(u.StateOrProvince)
+	ret.Tenant = copyStringIfNotNil(u.Tenant)
+	ret.Token = copyStringIfNotNil(u.Token)
+	ret.UID = copyIntIfNotNil(u.UID)
+	ret.FullName = u.FullName
+	if u.LastUpdated != nil {
+		ret.LastUpdated = u.LastUpdated.Time
+	}
+	if u.NewUser != nil {
+		ret.NewUser = *u.NewUser
+	}
+	if u.RegistrationSent != nil {
+		ret.RegistrationSent = new(time.Time)
+		*ret.RegistrationSent = u.RegistrationSent.Time
+	}
+	if u.RoleName != nil {
+		ret.Role = *u.RoleName
+	}
+	if u.TenantID != nil {
+		ret.TenantID = *u.TenantID
+	}
+	if u.Username != nil {
+		ret.Username = *u.Username
+	}
+	return ret
+}
+
+// Downgrade converts a UserV4 to a User (as seen in API versions < 4.0).
+func (u UserV4) Downgrade() User {
+	var ret User
+	ret.FullName = new(string)
+	ret.FullName = u.FullName
+	ret.LastUpdated = TimeNoModFromTime(u.LastUpdated)
+	ret.NewUser = new(bool)
+	*ret.NewUser = u.NewUser
+	ret.RoleName = new(string)
+	*ret.RoleName = u.Role
+	ret.Role = nil
+	ret.TenantID = new(int)
+	*ret.TenantID = u.TenantID
+	ret.Username = new(string)
+	*ret.Username = u.Username
+
+	ret.AddressLine1 = copyStringIfNotNil(u.AddressLine1)
+	ret.AddressLine2 = copyStringIfNotNil(u.AddressLine2)
+	ret.City = copyStringIfNotNil(u.City)
+	ret.Company = copyStringIfNotNil(u.Company)
+	ret.ConfirmLocalPassword = copyStringIfNotNil(u.ConfirmLocalPassword)
+	ret.Country = copyStringIfNotNil(u.Country)
+	ret.Email = copyStringIfNotNil(u.Email)
+	ret.GID = copyIntIfNotNil(u.GID)
+	ret.ID = copyIntIfNotNil(u.ID)
+	ret.LocalPassword = copyStringIfNotNil(u.LocalPassword)
+	ret.PhoneNumber = copyStringIfNotNil(u.PhoneNumber)
+	ret.PostalCode = copyStringIfNotNil(u.PostalCode)
+	ret.PublicSSHKey = copyStringIfNotNil(u.PublicSSHKey)
+	if u.RegistrationSent != nil {
+		ret.RegistrationSent = TimeNoModFromTime(*u.RegistrationSent)
+	}
+	ret.StateOrProvince = copyStringIfNotNil(u.StateOrProvince)
+	ret.Tenant = copyStringIfNotNil(u.Tenant)
+	ret.Token = copyStringIfNotNil(u.Token)
+	ret.UID = copyIntIfNotNil(u.UID)
+
+	return ret
+}
+
 // UserCredentials contains Traffic Ops login credentials.
 type UserCredentials struct {
 	Username string `json:"u"`
@@ -80,17 +185,10 @@ type User struct {
 	// https://github.com/apache/trafficcontrol/blob/3b5dd406bf1a0bb456c062b0f6a465ec0617d8ef/traffic_ops/traffic_ops_golang/user/user.go#L197
 	// It's done that way in order to maintain "rolename" vs "roleName" JSON field capitalization for the different users APIs.
 	// TODO: make the breaking API change to make all user APIs use "roleName" consistently.
-	RoleName *string `json:"roleName,omitempty" db:"-"`
+	RoleName *string `json:"roleName,omitempty" db:"role_name"`
 	commonUserFields
 }
 
-// UserV40 contains ChangeLogCount field.
-type UserV40 struct {
-	User
-	ChangeLogCount    *int       `json:"changeLogCount" db:"change_log_count"`
-	LastAuthenticated *time.Time `json:"lastAuthenticated" db:"last_authenticated"`
-}
-
 // UserCurrent represents the profile for the authenticated user.
 type UserCurrent struct {
 	UserName  *string `json:"username"`
@@ -99,10 +197,119 @@ type UserCurrent struct {
 	commonUserFields
 }
 
-// UserCurrentV40 contains LastAuthenticated field.
+// UserCurrentV4 is an alias for the UserCurrent struct used for the latest minor version associated with api major version 4.
+type UserCurrentV4 UserCurrentV40
+
+// UserCurrentV40 represents the structure for the "current" user, and has the "Role" field as a *string, as opposed to a *int found in the older versions.
 type UserCurrentV40 struct {
-	UserCurrent
-	LastAuthenticated *time.Time `json:"lastAuthenticated" db:"last_authenticated"`
+	UserName          string    `json:"username"`
+	LocalUser         *bool     `json:"localUser"`
+	AddressLine1      *string   `json:"addressLine1"`
+	AddressLine2      *string   `json:"addressLine2"`
+	City              *string   `json:"city"`
+	Company           *string   `json:"company"`
+	Country           *string   `json:"country"`
+	Email             *string   `json:"email"`
+	FullName          *string   `json:"fullName"`
+	GID               *int      `json:"gid"`
+	ID                *int      `json:"id"`
+	NewUser           *bool     `json:"newUser"`
+	PhoneNumber       *string   `json:"phoneNumber"`
+	PostalCode        *string   `json:"postalCode"`
+	PublicSSHKey      *string   `json:"publicSshKey"`
+	Role              string    `json:"role"`
+	StateOrProvince   *string   `json:"stateOrProvince"`
+	Tenant            *string   `json:"tenant"`
+	TenantID          *int      `json:"tenantId"`
+	Token             *string   `json:"-"`
+	UID               *int      `json:"uid"`
+	LastUpdated       time.Time `json:"lastUpdated"`
+	LastAuthenticated time.Time `json:"lastAuthenticated"`
+}
+
+// Downgrade will convert a UserCurrentV4 struct into an instance of the UserCurrent struct.
+func (u UserCurrentV4) Downgrade() UserCurrent {
+	var ret UserCurrent
+	ret.FullName = new(string)
+	ret.FullName = u.FullName
+	ret.LastUpdated = TimeNoModFromTime(u.LastUpdated)
+	ret.NewUser = new(bool)
+	ret.NewUser = u.NewUser
+	ret.RoleName = new(string)
+	*ret.RoleName = u.Role
+	ret.Role = nil
+	ret.TenantID = new(int)
+	ret.TenantID = u.TenantID
+	ret.Tenant = u.Tenant
+	ret.UserName = &u.UserName
+	ret.LocalUser = u.LocalUser
+	ret.Token = copyStringIfNotNil(u.Token)
+	ret.AddressLine1 = copyStringIfNotNil(u.AddressLine1)
+	ret.AddressLine2 = copyStringIfNotNil(u.AddressLine2)
+	ret.City = copyStringIfNotNil(u.City)
+	ret.Company = copyStringIfNotNil(u.Company)
+	ret.Country = copyStringIfNotNil(u.Country)
+	ret.Email = copyStringIfNotNil(u.Email)
+	ret.GID = copyIntIfNotNil(u.GID)
+	ret.ID = copyIntIfNotNil(u.ID)
+	ret.UID = copyIntIfNotNil(u.UID)
+	ret.PhoneNumber = copyStringIfNotNil(u.PhoneNumber)
+	ret.PostalCode = copyStringIfNotNil(u.PostalCode)
+	ret.PublicSSHKey = copyStringIfNotNil(u.PublicSSHKey)
+	ret.StateOrProvince = copyStringIfNotNil(u.StateOrProvince)
+	ret.Tenant = copyStringIfNotNil(u.Tenant)
+	ret.Token = copyStringIfNotNil(u.Token)
+	ret.UID = copyIntIfNotNil(u.UID)
+
+	return ret
+}
+
+// UserV4 is an alias for the User struct used for the latest minor version associated with api major version 4.
+type UserV4 UserV40
+
+// A UserV40 is a representation of a Traffic Ops user as it appears in version
+// 4.0 of Traffic Ops's API.
+type UserV40 struct {
+	AddressLine1         *string    `json:"addressLine1" db:"address_line1"`
+	AddressLine2         *string    `json:"addressLine2" db:"address_line2"`
+	ChangeLogCount       *int       `json:"changeLogCount" db:"change_log_count"`
+	City                 *string    `json:"city" db:"city"`
+	Company              *string    `json:"company" db:"company"`
+	ConfirmLocalPassword *string    `json:"confirmLocalPasswd,omitempty" db:"confirm_local_passwd"`
+	Country              *string    `json:"country" db:"country"`
+	Email                *string    `json:"email" db:"email"`
+	FullName             *string    `json:"fullName" db:"full_name"`
+	GID                  *int       `json:"gid"`
+	ID                   *int       `json:"id" db:"id"`
+	LastAuthenticated    *time.Time `json:"lastAuthenticated" db:"last_authenticated"`
+	LastUpdated          time.Time  `json:"lastUpdated" db:"last_updated"`
+	LocalPassword        *string    `json:"localPasswd,omitempty" db:"local_passwd"`
+	NewUser              bool       `json:"newUser" db:"new_user"`
+	PhoneNumber          *string    `json:"phoneNumber" db:"phone_number"`
+	PostalCode           *string    `json:"postalCode" db:"postal_code"`
+	PublicSSHKey         *string    `json:"publicSshKey" db:"public_ssh_key"`
+	RegistrationSent     *time.Time `json:"registrationSent" db:"registration_sent"`
+	Role                 string     `json:"role" db:"role"`
+	StateOrProvince      *string    `json:"stateOrProvince" db:"state_or_province"`
+	Tenant               *string    `json:"tenant"`
+	TenantID             int        `json:"tenantId" db:"tenant_id"`
+	Token                *string    `json:"-" db:"token"`
+	UID                  *int       `json:"uid"`
+	Username             string     `json:"username" db:"username"`
+}
+
+// UsersResponseV4 is the type of a response from Traffic Ops to requests made
+// to /users which return more than one user for the latest 4.x api version variant.
+type UsersResponseV4 struct {
+	Response []UserV4 `json:"response"`
+	Alerts
+}
+
+// UserResponseV4 is the type of a response from Traffic Ops to requests made
+// to /users which return one user for the latest 4.x api version variant.
+type UserResponseV4 struct {
+	Response UserV4 `json:"response"`
+	Alerts
 }
 
 // CurrentUserUpdateRequest differs from a regular User/UserCurrent in that many of its fields are
@@ -136,9 +343,47 @@ type CurrentUserUpdateRequestUser struct {
 	Username           json.RawMessage `json:"username"`
 }
 
+// Upgrade converts a UserCurrent struct into an instance of UserCurrentV4.
+func (u UserCurrent) Upgrade() UserCurrentV4 {
+	var ret UserCurrentV4
+	ret.AddressLine1 = copyStringIfNotNil(u.AddressLine1)
+	ret.AddressLine2 = copyStringIfNotNil(u.AddressLine2)
+	ret.City = copyStringIfNotNil(u.City)
+	ret.Company = copyStringIfNotNil(u.Company)
+	ret.Country = copyStringIfNotNil(u.Country)
+	ret.Email = copyStringIfNotNil(u.Email)
+	ret.GID = copyIntIfNotNil(u.GID)
+	ret.ID = copyIntIfNotNil(u.ID)
+	ret.PhoneNumber = copyStringIfNotNil(u.PhoneNumber)
+	ret.PostalCode = copyStringIfNotNil(u.PostalCode)
+	ret.PublicSSHKey = copyStringIfNotNil(u.PublicSSHKey)
+	ret.StateOrProvince = copyStringIfNotNil(u.StateOrProvince)
+	ret.Tenant = copyStringIfNotNil(u.Tenant)
+	ret.Token = copyStringIfNotNil(u.Token)
+	ret.UID = copyIntIfNotNil(u.UID)
+	ret.FullName = u.FullName
+	if u.LastUpdated != nil {
+		ret.LastUpdated = u.LastUpdated.Time
+	}
+	if u.NewUser != nil {
+		ret.NewUser = u.NewUser
+	}
+
+	if u.RoleName != nil {
+		ret.Role = *u.RoleName
+	}
+	if u.TenantID != nil {
+		ret.TenantID = u.TenantID
+	}
+	if u.UserName != nil {
+		ret.UserName = *u.UserName
+	}
+	return ret
+}
+
 // UnmarshalAndValidate validates the request and returns a User into which the request's information
 // has been unmarshalled.
-func (u *CurrentUserUpdateRequestUser) UnmarshalAndValidate(user *User) error {
+func (u *CurrentUserUpdateRequestUser) UnmarshalAndValidate(user *User, useV4User bool) error {
 	errs := []error{}
 	if u.AddressLine1 != nil {
 		if err := json.Unmarshal(u.AddressLine1, &user.AddressLine1); err != nil {
@@ -228,11 +473,20 @@ func (u *CurrentUserUpdateRequestUser) UnmarshalAndValidate(user *User) error {
 	}
 
 	if u.Role != nil {
-		if err := json.Unmarshal(u.Role, &user.Role); err != nil {
-			errs = append(errs, fmt.Errorf("role: %v", err))
-		} else if user.Role == nil {
-			errs = append(errs, errors.New("role: cannot be null"))
+		if useV4User {
+			if err := json.Unmarshal(u.Role, &user.RoleName); err != nil {
+				errs = append(errs, fmt.Errorf("role: %v", err))
+			} else if user.RoleName == nil {
+				errs = append(errs, errors.New("role: cannot be null"))
+			}
+		} else {
+			if err := json.Unmarshal(u.Role, &user.Role); err != nil {
+				errs = append(errs, fmt.Errorf("role: %v", err))
+			} else if user.Role == nil {
+				errs = append(errs, errors.New("role: cannot be null"))
+			}
 		}
+
 	}
 
 	if u.StateOrProvince != nil {
@@ -277,9 +531,9 @@ type UsersResponse struct {
 	Alerts
 }
 
-// UsersResponseV40 is the Traffic Ops API version 4.0 variant of UserResponse.
-type UsersResponseV40 struct {
-	Response []UserV40 `json:"response"`
+// UserResponse can hold a Traffic Ops API response to a request to get a user.
+type UserResponse struct {
+	Response User `json:"response"`
 	Alerts
 }
 
@@ -289,12 +543,24 @@ type CreateUserResponse struct {
 	Alerts
 }
 
+// CreateUserResponseV4 can hold a Traffic Ops API response to a POST request to create a user in api v4.
+type CreateUserResponseV4 struct {
+	Response UserV4 `json:"response"`
+	Alerts
+}
+
 // UpdateUserResponse can hold a Traffic Ops API response to a PUT request to update a user.
 type UpdateUserResponse struct {
 	Response User `json:"response"`
 	Alerts
 }
 
+// UpdateUserResponseV4 can hold a Traffic Ops API response to a PUT request to update a user for the latest 4.x api version variant.
+type UpdateUserResponseV4 struct {
+	Response UserV4 `json:"response"`
+	Alerts
+}
+
 // DeleteUserResponse can theoretically hold a Traffic Ops API response to a
 // DELETE request to update a user. It is unused.
 type DeleteUserResponse struct {
@@ -308,9 +574,9 @@ type UserCurrentResponse struct {
 	Alerts
 }
 
-// UserCurrentResponseV40 is the Traffic Ops API version 4.0 variant of UserResponse.
-type UserCurrentResponseV40 struct {
-	Response UserCurrentV40 `json:"response"`
+// UserCurrentResponseV4 is the latest 4.x Traffic Ops API version variant of UserResponse.
+type UserCurrentResponseV4 struct {
+	Response UserCurrentV4 `json:"response"`
 	Alerts
 }
 
@@ -334,6 +600,39 @@ type UserRegistrationRequest struct {
 	TenantID uint `json:"tenantId"`
 }
 
+// UserRegistrationRequestV4 is the alias for the UserRegistrationRequest for the latest 4.x api version variant.
+type UserRegistrationRequestV4 UserRegistrationRequestV40
+
+// UserRegistrationRequestV40 is the request submitted by operators when they want to register a new
+// user in api V4.
+type UserRegistrationRequestV40 struct {
+	Email    rfc.EmailAddress `json:"email"`
+	Role     string           `json:"role"`
+	TenantID uint             `json:"tenantId"`
+}
+
+// Validate implements the
+// github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api.ParseValidator
+// interface.
+func (urr *UserRegistrationRequestV4) Validate(tx *sql.Tx) error {
+	var errs = []error{}
+	if urr.Role == "" {
+		errs = append(errs, errors.New("role: required and cannot be empty."))
+	}
+
+	if urr.TenantID == 0 {
+		errs = append(errs, errors.New("tenantId: required and cannot be zero."))
+	}
+
+	// This can only happen if an email isn't present in the request; the JSON parse handles actually
+	// invalid email addresses.
+	if urr.Email.Address.Address == "" {
+		errs = append(errs, errors.New("email: required"))
+	}
+
+	return util.JoinErrs(errs)
+}
+
 // Validate implements the
 // github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api.ParseValidator
 // interface.
diff --git a/lib/go-tc/users_test.go b/lib/go-tc/users_test.go
new file mode 100644
index 0000000..b756999
--- /dev/null
+++ b/lib/go-tc/users_test.go
@@ -0,0 +1,414 @@
+package tc
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"testing"
+	"time"
+)
+
+func TestUser_UpgradeFromLegacyUser(t *testing.T) {
+	addressLine1 := "Address Line 1"
+	addressLine2 := "Address Line 2"
+	city := "City"
+	company := "Company"
+	confirmLocalPassword := "Confirm LocalPasswd"
+	country := "Country"
+	email := "em@i.l"
+	fullName := "Full Name"
+	gid := 1
+	id := 2
+	lastUpdated := NewTimeNoMod()
+	localPassword := "LocalPasswd"
+	newUser := true
+	phoneNumber := "555-555-5555"
+	postalCode := "55555"
+	publicSSHKey := "Public SSH Key"
+	registrationSent := NewTimeNoMod()
+	role := "Role Name"
+	stateOrProvince := "State or Province"
+	tenant := "Tenant"
+	tenantID := 3
+	token := "Token"
+	uid := 4
+	username := "Username"
+
+	var user User
+	user.AddressLine1 = &addressLine1
+	user.AddressLine2 = &addressLine2
+	user.City = &city
+	user.Company = &company
+	user.ConfirmLocalPassword = &confirmLocalPassword
+	user.Country = &country
+	user.Email = &email
+	user.FullName = &fullName
+	user.GID = &gid
+	user.ID = &id
+	user.LastUpdated = lastUpdated
+	user.LocalPassword = &localPassword
+	user.NewUser = &newUser
+	user.PhoneNumber = &phoneNumber
+	user.PostalCode = &postalCode
+	user.PublicSSHKey = &publicSSHKey
+	user.RegistrationSent = registrationSent
+	user.Role = new(int)
+	user.RoleName = &role
+	user.StateOrProvince = &stateOrProvince
+	user.Tenant = &tenant
+	user.TenantID = &tenantID
+	user.Token = &token
+	user.UID = &uid
+	user.Username = &username
+
+	upgraded := user.Upgrade()
+	if upgraded.AddressLine1 == nil {
+		t.Error("AddressLine1 became nil after upgrade")
+	} else if *upgraded.AddressLine1 != addressLine1 {
+		t.Errorf("Incorrect AddressLine1 after upgrade; want: '%s', got: '%s'", addressLine1, *upgraded.AddressLine1)
+	}
+	if upgraded.AddressLine2 == nil {
+		t.Error("AddressLine2 became nil after upgrade")
+	} else if *upgraded.AddressLine2 != addressLine2 {
+		t.Errorf("Incorrect AddressLine2 after upgrade; want: '%s', got: '%s'", addressLine2, *upgraded.AddressLine2)
+	}
+	if upgraded.City == nil {
+		t.Error("City became nil after upgrade")
+	} else if *upgraded.City != city {
+		t.Errorf("Incorrect City after upgrade; want: '%s', got: '%s'", city, *upgraded.City)
+	}
+	if upgraded.Company == nil {
+		t.Error("Company became nil after upgrade")
+	} else if *upgraded.Company != company {
+		t.Errorf("Incorrect Company after upgrade; want: '%s', got: '%s'", company, *upgraded.Company)
+	}
+	if upgraded.ConfirmLocalPassword == nil {
+		t.Error("ConfirmLocalPassword became nil after upgrade")
+	} else if *upgraded.ConfirmLocalPassword != confirmLocalPassword {
+		t.Errorf("Incorrect ConfirmLocalPassword after upgrade; want: '%s', got: '%s'", confirmLocalPassword, *upgraded.ConfirmLocalPassword)
+	}
+	if upgraded.Country == nil {
+		t.Error("Country became nil after upgrade")
+	} else if *upgraded.Country != country {
+		t.Errorf("Incorrect Country after upgrade; want: '%s', got: '%s'", country, *upgraded.Country)
+	}
+	if upgraded.Email == nil {
+		t.Error("Email became nil after upgrade")
+	} else if *upgraded.Email != email {
+		t.Errorf("Incorrect Email after upgrade; want: '%s', got: '%s'", email, *upgraded.Email)
+	}
+	if upgraded.FullName == nil {
+		t.Error("Fullname became nil after upgrade")
+	} else if *upgraded.FullName != fullName {
+		t.Errorf("Incorrect FullName after upgrade; want: '%s', got: '%s'", fullName, *upgraded.FullName)
+	}
+	if upgraded.GID == nil {
+		t.Error("GID became nil after upgrade")
+	} else if *upgraded.GID != gid {
+		t.Errorf("Incorrect GID after upgrade; want: %d, got: %d", gid, *upgraded.GID)
+	}
+	if upgraded.ID == nil {
+		t.Error("ID became nil after upgrade")
+	} else if *upgraded.ID != id {
+		t.Errorf("Incorrect ID after upgrade; want: %d, got: %d", id, *upgraded.ID)
+	}
+	if !upgraded.LastUpdated.Equal(lastUpdated.Time) {
+		t.Errorf("Incorrect LastUpdated after upgrade; want: %v, got: %v", lastUpdated.Time, upgraded.LastUpdated)
+	}
+	if upgraded.LocalPassword == nil {
+		t.Error("LocalPassword became nil after upgrade")
+	} else if *upgraded.LocalPassword != localPassword {
+		t.Errorf("Incorrect LocalPassword after upgrade; want: '%s', got: '%s'", localPassword, *upgraded.LocalPassword)
+	}
+	if upgraded.NewUser != newUser {
+		t.Errorf("Incorrect NewUser after upgrade; want: %t, got: %t", newUser, upgraded.NewUser)
+	}
+	if upgraded.PhoneNumber == nil {
+		t.Error("PhoneNumber became nil after upgrade")
+	} else if *upgraded.PhoneNumber != phoneNumber {
+		t.Errorf("Incorrect PhoneNumber after upgrade; want: '%s', got: '%s'", phoneNumber, *upgraded.PhoneNumber)
+	}
+	if upgraded.PostalCode == nil {
+		t.Error("PostalCode became nil after upgrade")
+	} else if *upgraded.PostalCode != postalCode {
+		t.Errorf("Incorrect PostalCode after upgrade; want: '%s', got: '%s'", postalCode, *upgraded.PostalCode)
+	}
+	if upgraded.PublicSSHKey == nil {
+		t.Error("PublicSSHKey became nil after upgrade")
+	} else if *upgraded.PublicSSHKey != publicSSHKey {
+		t.Errorf("Incorrect PublicSSHKey after upgrade; want: '%s', got: '%s'", publicSSHKey, *upgraded.PublicSSHKey)
+	}
+	if upgraded.RegistrationSent == nil {
+		t.Error("RegistrationSent became nil after upgrade")
+	} else if !upgraded.RegistrationSent.Equal(registrationSent.Time) {
+		t.Errorf("Incorrect RegistrationSent after upgrade; want: %v, got: %v", registrationSent.Time, *upgraded.RegistrationSent)
+	}
+	if upgraded.Role != role {
+		t.Errorf("Incorrect Role after upgrade; want: '%s', got: '%s'", role, upgraded.Role)
+	}
+	if upgraded.StateOrProvince == nil {
+		t.Error("StateOrProvince became nil after upgrade")
+	} else if *upgraded.StateOrProvince != stateOrProvince {
+		t.Errorf("Incorrect StateOrProvince after upgrade; want: '%s', got: '%s'", stateOrProvince, *upgraded.StateOrProvince)
+	}
+	if upgraded.Tenant == nil {
+		t.Error("Tenant became nil after upgrade")
+	} else if *upgraded.Tenant != tenant {
+		t.Errorf("Incorrect Tenant after upgrade; want: '%s', got: '%s'", tenant, *upgraded.Tenant)
+	}
+	if upgraded.TenantID != tenantID {
+		t.Errorf("Incorrect TenantID after upgrade; want: %d, got: %d", tenantID, upgraded.TenantID)
+	}
+	if upgraded.Token == nil {
+		t.Error("Token became nil after upgrade")
+	} else if *upgraded.Token != token {
+		t.Errorf("Incorrect Token after upgrade; want: '%s', got: '%s'", token, *upgraded.Token)
+	}
+	if upgraded.UID == nil {
+		t.Error("UID became nil after upgrade")
+	} else if *upgraded.UID != uid {
+		t.Errorf("Incorrect UID after upgrade; want: %d, got: %d", uid, *upgraded.UID)
+	}
+	if upgraded.Username != username {
+		t.Errorf("Incorrect Username after upgrade; want: '%s', got: '%s'", username, upgraded.Username)
+	}
+
+	user = upgraded.Downgrade()
+	if user.Role != nil {
+		t.Errorf("Expected Role to be nil after downgrade, got: %d", *user.Role)
+	}
+}
+
+func TestUserV4_Downgrade(t *testing.T) {
+	addressLine1 := "Address Line 1"
+	addressLine2 := "Address Line 2"
+	city := "City"
+	company := "Company"
+	confirmLocalPassword := "Confirm LocalPasswd"
+	country := "Country"
+	email := "em@i.l"
+	fullName := "Full Name"
+	gid := 1
+	id := 2
+	lastUpdated := time.Now()
+	localPassword := "LocalPasswd"
+	newUser := true
+	phoneNumber := "555-555-5555"
+	postalCode := "55555"
+	publicSSHKey := "Public SSH Key"
+	registrationSent := time.Now().Add(time.Second)
+	role := "Role Name"
+	stateOrProvince := "State or Province"
+	tenant := "Tenant"
+	tenantID := 3
+	token := "Token"
+	uid := 4
+	username := "Username"
+
+	user := UserV4{
+		FullName:    &fullName,
+		LastUpdated: lastUpdated,
+		NewUser:     newUser,
+		Role:        role,
+		TenantID:    tenantID,
+		Username:    username,
+	}
+	user.AddressLine1 = &addressLine1
+	user.AddressLine2 = &addressLine2
+	user.City = &city
+	user.Company = &company
+	user.ConfirmLocalPassword = &confirmLocalPassword
+	user.Country = &country
+	user.Email = &email
+	user.GID = &gid
+	user.ID = &id
+	user.LocalPassword = &localPassword
+	user.PhoneNumber = &phoneNumber
+	user.PostalCode = &postalCode
+	user.PublicSSHKey = &publicSSHKey
+	user.RegistrationSent = &registrationSent
+	user.StateOrProvince = &stateOrProvince
+	user.Tenant = &tenant
+	user.Token = &token
+	user.UID = &uid
+
+	downgraded := user.Downgrade()
+	if downgraded.AddressLine1 == nil {
+		t.Error("AddressLine1 became nil after downgrade")
+	} else if *downgraded.AddressLine1 != addressLine1 {
+		t.Errorf("Incorrect AddressLine1 after downgrade; want: '%s', got: '%s'", addressLine1, *downgraded.AddressLine1)
+	}
+	if downgraded.AddressLine2 == nil {
+		t.Error("AddressLine2 became nil after downgrade")
+	} else if *downgraded.AddressLine2 != addressLine2 {
+		t.Errorf("Incorrect AddressLine2 after downgrade; want: '%s', got: '%s'", addressLine2, *downgraded.AddressLine2)
+	}
+	if downgraded.City == nil {
+		t.Error("City became nil after downgrade")
+	} else if *downgraded.City != city {
+		t.Errorf("Incorrect City after downgrade; want: '%s', got: '%s'", city, *downgraded.City)
+	}
+	if downgraded.Company == nil {
+		t.Error("Company became nil after downgrade")
+	} else if *downgraded.Company != company {
+		t.Errorf("Incorrect Company after downgrade; want: '%s', got: '%s'", company, *downgraded.Company)
+	}
+	if downgraded.ConfirmLocalPassword == nil {
+		t.Error("ConfirmLocalPassword became nil after downgrade")
+	} else if *downgraded.ConfirmLocalPassword != confirmLocalPassword {
+		t.Errorf("Incorrect ConfirmLocalPassword after downgrade; want: '%s', got: '%s'", confirmLocalPassword, *downgraded.ConfirmLocalPassword)
+	}
+	if downgraded.Country == nil {
+		t.Error("Country became nil after downgrade")
+	} else if *downgraded.Country != country {
+		t.Errorf("Incorrect Country after downgrade; want: '%s', got: '%s'", country, *downgraded.Country)
+	}
+	if downgraded.Email == nil {
+		t.Error("Email became nil after downgrade")
+	} else if *downgraded.Email != email {
+		t.Errorf("Incorrect Email after downgrade; want: '%s', got: '%s'", email, *downgraded.Email)
+	}
+	if downgraded.FullName == nil {
+		t.Error("FullName became nil after downgrade")
+	} else if *downgraded.FullName != fullName {
+		t.Errorf("Incorrect FullName after downgrade; want: '%s', got: '%s'", fullName, *downgraded.FullName)
+	}
+	if downgraded.GID == nil {
+		t.Error("GID became nil after downgrade")
+	} else if *downgraded.GID != gid {
+		t.Errorf("Incorrect GID after downgrade; want: %d, got: %d", gid, *downgraded.GID)
+	}
+	if downgraded.ID == nil {
+		t.Error("ID became nil after downgrade")
+	} else if *downgraded.ID != id {
+		t.Errorf("Incorrect ID after downgrade; want: %d, got: %d", id, *downgraded.ID)
+	}
+	if downgraded.LastUpdated == nil {
+		t.Error("LastUpdated became nil after downgrade")
+	} else if !downgraded.LastUpdated.Time.Equal(lastUpdated) {
+		t.Errorf("Incorrect LastUpdated after downgrade; want: %v, got: %v", lastUpdated, downgraded.LastUpdated.Time)
+	}
+	if downgraded.LocalPassword == nil {
+		t.Error("LocalPassword became nil after downgrade")
+	} else if *downgraded.LocalPassword != localPassword {
+		t.Errorf("Incorrect LocalPassword after downgrade; want: '%s', got: '%s'", localPassword, *downgraded.LocalPassword)
+	}
+	if downgraded.NewUser == nil {
+		t.Error("NewUser became nil after downgrade")
+	} else if *downgraded.NewUser != newUser {
+		t.Errorf("Incorrect NewUser after downgrade; want: %t, got: %t", newUser, *downgraded.NewUser)
+	}
+	if downgraded.PhoneNumber == nil {
+		t.Error("PhoneNumber became nil after downgrade")
+	} else if *downgraded.PhoneNumber != phoneNumber {
+		t.Errorf("Incorrect PhoneNumber after downgrade; want: '%s', got: '%s'", phoneNumber, *downgraded.PhoneNumber)
+	}
+	if downgraded.PostalCode == nil {
+		t.Error("PostalCode became nil after downgrade")
+	} else if *downgraded.PostalCode != postalCode {
+		t.Errorf("Incorrect PostalCode after downgrade; want: '%s', got: '%s'", postalCode, *downgraded.PostalCode)
+	}
+	if downgraded.PublicSSHKey == nil {
+		t.Error("PublicSSHKey became nil after downgrade")
+	} else if *downgraded.PublicSSHKey != publicSSHKey {
+		t.Errorf("Incorrect PublicSSHKey after downgrade; want: '%s', got: '%s'", publicSSHKey, *downgraded.PublicSSHKey)
+	}
+	if downgraded.RegistrationSent == nil {
+		t.Error("RegistrationSent became nil after downgrade")
+	} else if !downgraded.RegistrationSent.Time.Equal(registrationSent) {
+		t.Errorf("Incorrect RegistrationSent after downgrade; want: %v, got: %v", registrationSent, downgraded.RegistrationSent.Time)
+	}
+	if downgraded.RoleName == nil {
+		t.Error("RoleName became nil after downgrade")
+	} else if *downgraded.RoleName != role {
+		t.Errorf("Incorrect RoleName after downgrade; want: '%s', got: '%s'", role, *downgraded.RoleName)
+	}
+	if downgraded.StateOrProvince == nil {
+		t.Error("StateOrProvince became nil after downgrade")
+	} else if *downgraded.StateOrProvince != stateOrProvince {
+		t.Errorf("Incorrect StateOrProvince after downgrade; want: '%s', got: '%s'", stateOrProvince, *downgraded.StateOrProvince)
+	}
+	if downgraded.Tenant == nil {
+		t.Error("Tenant became nil after downgrade")
+	} else if *downgraded.Tenant != tenant {
+		t.Errorf("Incorrect Tenant after downgrade; want: '%s', got: '%s'", tenant, *downgraded.Tenant)
+	}
+	if downgraded.TenantID == nil {
+		t.Error("TenantID became nil after downgrade")
+	} else if *downgraded.TenantID != tenantID {
+		t.Errorf("Incorrect TenantID after downgrade; want: %d, got: %d", tenantID, *downgraded.TenantID)
+	}
+	if downgraded.Token == nil {
+		t.Error("Token became nil after downgrade")
+	} else if *downgraded.Token != token {
+		t.Errorf("Incorrect Token after downgrade; want: '%s', got: '%s'", token, *downgraded.Token)
+	}
+	if downgraded.UID == nil {
+		t.Error("UID became nil after downgrade")
+	} else if *downgraded.UID != uid {
+		t.Errorf("Incorrect UID after downgrade; want: %d, got: %d", uid, *downgraded.UID)
+	}
+	if downgraded.Username == nil {
+		t.Error("Username became nil after downgrade")
+	} else if *downgraded.Username != username {
+		t.Errorf("Incorrect Username after downgrade; want: '%s', got: '%s'", username, *downgraded.Username)
+	}
+}
+
+func TestCopyUtilities(t *testing.T) {
+	var s *string
+	copiedS := copyStringIfNotNil(s)
+	if copiedS != nil {
+		t.Errorf("Copying a nil string should've given nil, got: %s", *copiedS)
+	}
+	s = new(string)
+	*s = "test string"
+	copiedS = copyStringIfNotNil(s)
+	if copiedS == nil {
+		t.Errorf("Copied pointer to '%s' was nil", *s)
+	} else {
+		if *copiedS != *s {
+			t.Errorf("Incorrectly copied string pointer; expected: '%s', got: '%s'", *s, *copiedS)
+		}
+		*s = "a different test string"
+		if *copiedS == *s {
+			t.Error("Expected copy to be 'deep' but modifying the original string changed the copy")
+		}
+	}
+
+	var i *int
+	copiedI := copyIntIfNotNil(i)
+	if copiedI != nil {
+		t.Errorf("Copying a nil int should've given nil, got: %d", *copiedI)
+	}
+	i = new(int)
+	*i = 9000
+	copiedI = copyIntIfNotNil(i)
+	if copiedI == nil {
+		t.Errorf("Copied pointer to %d was nil", *i)
+	} else {
+		if *copiedI != *i {
+			t.Errorf("Incorrectly copied int pointer; expected: %d, got: %d", *i, *copiedI)
+		}
+		*i = 9001
+		if *copiedI == *i {
+			t.Error("Expected copy to be 'deep' but modifying the original int changed the copy")
+		}
+	}
+}
diff --git a/traffic_ops/testing/api/v2/tc-fixtures.json b/traffic_ops/testing/api/v2/tc-fixtures.json
index 59f090f..b1bd27e 100644
--- a/traffic_ops/testing/api/v2/tc-fixtures.json
+++ b/traffic_ops/testing/api/v2/tc-fixtures.json
@@ -1512,8 +1512,7 @@
             "privLevel": 30,
             "capabilities": [
                 "all-read",
-                "all-write",
-                "cdn-read"
+                "all-write"
             ]
         },
         {
diff --git a/traffic_ops/testing/api/v3/tc-fixtures.json b/traffic_ops/testing/api/v3/tc-fixtures.json
index 9200849..8b626b8 100644
--- a/traffic_ops/testing/api/v3/tc-fixtures.json
+++ b/traffic_ops/testing/api/v3/tc-fixtures.json
@@ -2395,8 +2395,7 @@
             "privLevel": 30,
             "capabilities": [
                 "all-read",
-                "all-write",
-                "cdn-read"
+                "all-write"
             ]
         },
         {
diff --git a/traffic_ops/testing/api/v4/cachegroups_test.go b/traffic_ops/testing/api/v4/cachegroups_test.go
index 50bd571..b2eb675 100644
--- a/traffic_ops/testing/api/v4/cachegroups_test.go
+++ b/traffic_ops/testing/api/v4/cachegroups_test.go
@@ -101,26 +101,24 @@ func UpdateCachegroupWithLocks(t *testing.T) {
 	}
 
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(1)
+	user1.TenantID = 1
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err = TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s. err: %v", user1.Username, err)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
diff --git a/traffic_ops/testing/api/v4/cdn_locks_test.go b/traffic_ops/testing/api/v4/cdn_locks_test.go
index 94daa22..d6f1341 100644
--- a/traffic_ops/testing/api/v4/cdn_locks_test.go
+++ b/traffic_ops/testing/api/v4/cdn_locks_test.go
@@ -16,7 +16,6 @@ package v4
 */
 
 import (
-	"fmt"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -186,52 +185,47 @@ func AdminCdnLocks(t *testing.T) {
 	}
 
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(resp.Response[0].ID)
+	user1.TenantID = resp.Response[0].ID
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err = TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s", user1.Username)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Create another new user with operations level privileges
-	user2 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user2"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word2"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word2"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user2 := tc.UserV4{
+		Username:             "lock_user2",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word2"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word2"),
+		Role:                 "operations",
 	}
 	user2.Email = util.StrPtr("newlockuseremail@domain.com")
-	user2.TenantID = util.IntPtr(resp.Response[0].ID)
+	user2.TenantID = resp.Response[0].ID
 	user2.FullName = util.StrPtr("firstName2 LastName2")
 	_, _, err = TOSession.CreateUser(user2, client.RequestOptions{})
 	if err != nil {
-		fmt.Println(err)
-		t.Fatalf("could not create test user with username: %s", *user2.Username)
+		t.Fatalf("could not create test user with username: %s, err: %v", user2.Username, err)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user2"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
 
 	// Establish another session with the newly created non admin level user
-	userSession2, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user2.Username, *user2.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession2, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user2.Username, *user2.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
@@ -276,26 +270,24 @@ func SnapshotWithLock(t *testing.T) {
 	}
 
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(resp.Response[0].ID)
+	user1.TenantID = resp.Response[0].ID
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err = TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s", user1.Username)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
@@ -307,7 +299,7 @@ func SnapshotWithLock(t *testing.T) {
 	opts.QueryParameters.Set("cdn", cdn)
 	_, _, err = userSession.SnapshotCRConfig(opts)
 	if err != nil {
-		t.Errorf("expected no error while snapping cdn %s by user %s, but got %v", cdn, *user1.Username, err)
+		t.Errorf("expected no error while snapping cdn %s by user %s, but got %v", cdn, user1.Username, err)
 	}
 
 	// Create a lock for this user
@@ -323,7 +315,7 @@ func SnapshotWithLock(t *testing.T) {
 	// "lock_user1", which has the lock on CDN "bar", tries to snap it -> this should pass
 	_, _, err = userSession.SnapshotCRConfig(opts)
 	if err != nil {
-		t.Errorf("expected no error while snapping cdn %s by user %s, but got %v", cdn, *user1.Username, err)
+		t.Errorf("expected no error while snapping cdn %s by user %s, but got %v", cdn, user1.Username, err)
 	}
 
 	// Admin user, which doesn't have the lock on the CDN "bar", is trying to snap it -> this should fail
@@ -352,26 +344,24 @@ func QueueUpdatesWithLock(t *testing.T) {
 	}
 
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(resp.Response[0].ID)
+	user1.TenantID = resp.Response[0].ID
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err = TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s", user1.Username)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
@@ -384,7 +374,7 @@ func QueueUpdatesWithLock(t *testing.T) {
 	// Currently, no user has a lock on the "bar" CDN, so when "lock_user1", which does not have the lock on CDN "bar", tries to queue updates on a server in the same CDN, this should pass
 	_, _, err = userSession.SetServerQueueUpdate(serverID, true, client.RequestOptions{})
 	if err != nil {
-		t.Errorf("expected no error while queueing updates for a server in cdn %s by user %s, but got %v", cdn, *user1.Username, err)
+		t.Errorf("expected no error while queueing updates for a server in cdn %s by user %s, but got %v", cdn, user1.Username, err)
 	}
 
 	// Create a lock for this user
@@ -400,7 +390,7 @@ func QueueUpdatesWithLock(t *testing.T) {
 	// "lock_user1", which has the lock on CDN "bar", tries to queue updates on a server in it -> this should pass
 	_, _, err = userSession.SetServerQueueUpdate(serverID, true, client.RequestOptions{})
 	if err != nil {
-		t.Errorf("expected no error while queueing updates for a server in cdn %s by user %s, but got %v", cdn, *user1.Username, err)
+		t.Errorf("expected no error while queueing updates for a server in cdn %s by user %s, but got %v", cdn, user1.Username, err)
 	}
 
 	// Admin user, which doesn't have the lock on the CDN "bar", is trying to queue updates on a server in it -> this should fail
@@ -429,26 +419,24 @@ func QueueUpdatesFromTopologiesWithLock(t *testing.T) {
 	}
 
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(resp.Response[0].ID)
+	user1.TenantID = resp.Response[0].ID
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err = TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s", user1.Username)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
@@ -461,7 +449,7 @@ func QueueUpdatesFromTopologiesWithLock(t *testing.T) {
 	// Currently, no user has a lock on the "bar" CDN, so when "lock_user1", which does not have the lock on CDN "bar", tries to queue updates on a topology in the same CDN, this should pass
 	_, _, err = userSession.TopologiesQueueUpdate(topology, tc.TopologiesQueueUpdateRequest{Action: "queue", CDNID: int64(cdnID)}, client.RequestOptions{})
 	if err != nil {
-		t.Errorf("expected no error while queueing updates for a topology server in cdn %s by user %s, but got %v", cdnName, *user1.Username, err)
+		t.Errorf("expected no error while queueing updates for a topology server in cdn %s by user %s, but got %v", cdnName, user1.Username, err)
 	}
 
 	// Create a lock for this user
@@ -477,7 +465,7 @@ func QueueUpdatesFromTopologiesWithLock(t *testing.T) {
 	// "lock_user1", which has the lock on CDN "bar", tries to queue updates on a topology in it -> this should pass
 	_, _, err = userSession.TopologiesQueueUpdate(topology, tc.TopologiesQueueUpdateRequest{Action: "queue", CDNID: int64(cdnID)}, client.RequestOptions{})
 	if err != nil {
-		t.Errorf("expected no error while queueing updates for a topology server in cdn %s by user %s, but got %v", cdnName, *user1.Username, err)
+		t.Errorf("expected no error while queueing updates for a topology server in cdn %s by user %s, but got %v", cdnName, user1.Username, err)
 	}
 
 	// Admin user, which doesn't have the lock on the CDN "bar", is trying to queue updates on a topology in it -> this should fail
diff --git a/traffic_ops/testing/api/v4/cdns_test.go b/traffic_ops/testing/api/v4/cdns_test.go
index 5299d9e..c7a1543 100644
--- a/traffic_ops/testing/api/v4/cdns_test.go
+++ b/traffic_ops/testing/api/v4/cdns_test.go
@@ -63,26 +63,24 @@ func TestCDNs(t *testing.T) {
 
 func UpdateDeleteCDNWithLocks(t *testing.T) {
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(1)
+	user1.TenantID = 1
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err := TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s", user1.Username)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
diff --git a/traffic_ops/testing/api/v4/crconfig_test.go b/traffic_ops/testing/api/v4/crconfig_test.go
index d563146..495511e 100644
--- a/traffic_ops/testing/api/v4/crconfig_test.go
+++ b/traffic_ops/testing/api/v4/crconfig_test.go
@@ -56,24 +56,22 @@ func SnapshotWithReadOnlyUser(t *testing.T) {
 	}
 
 	toReqTimeout := time.Second * time.Duration(Config.Default.Session.TimeoutInSecs)
-	user := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("test_user"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("read-only user"),
-		},
-	}
-	user.Email = util.StrPtr("email@domain.com")
-	user.TenantID = util.IntPtr(resp.Response[0].ID)
+	user := tc.UserV4{
+		Username:             "test_user_tm",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "read-only user",
+	}
+	user.Email = util.StrPtr("email_tm@domain.com")
+	user.TenantID = resp.Response[0].ID
 	user.FullName = util.StrPtr("firstName LastName")
 
 	u, _, err := TOSession.CreateUser(user, client.RequestOptions{})
 	if err != nil {
 		t.Fatalf("could not create read-only user: %v - alerts: %+v", err, u.Alerts)
 	}
-	client, _, err := toclient.LoginWithAgent(TOSession.URL, "test_user", "test_pa$$word", true, "to-api-v4-client-tests/tenant4user", true, toReqTimeout)
+	client, _, err := toclient.LoginWithAgent(TOSession.URL, "test_user_tm", "test_pa$$word", true, "to-api-v4-client-tests/tenant4user", true, toReqTimeout)
 	if err != nil {
 		t.Fatalf("failed to log in with test_user: %v", err.Error())
 	}
@@ -86,9 +84,7 @@ func SnapshotWithReadOnlyUser(t *testing.T) {
 	if reqInf.StatusCode != http.StatusForbidden {
 		t.Errorf("expected a 403 forbidden status code, but got %d", reqInf.StatusCode)
 	}
-	if u.Response.Username != nil {
-		ForceDeleteTestUsersByUsernames(t, []string{"test_user"})
-	}
+	ForceDeleteTestUsersByUsernames(t, []string{"test_user_tm"})
 }
 
 func UpdateTestCRConfigSnapshot(t *testing.T) {
diff --git a/traffic_ops/testing/api/v4/deliveryservices_test.go b/traffic_ops/testing/api/v4/deliveryservices_test.go
index 09fdef1..339df83 100644
--- a/traffic_ops/testing/api/v4/deliveryservices_test.go
+++ b/traffic_ops/testing/api/v4/deliveryservices_test.go
@@ -100,27 +100,25 @@ func TestDeliveryServices(t *testing.T) {
 
 func CUDDeliveryServiceWithLocks(t *testing.T) {
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(1)
+	user1.TenantID = 1
 	//util.IntPtr(resp.Response[0].ID)
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err := TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s", user1.Username)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
diff --git a/traffic_ops/testing/api/v4/jobs_test.go b/traffic_ops/testing/api/v4/jobs_test.go
index 971b6ae..047ddc7 100644
--- a/traffic_ops/testing/api/v4/jobs_test.go
+++ b/traffic_ops/testing/api/v4/jobs_test.go
@@ -390,7 +390,7 @@ func GetTestJobsByValidData(t *testing.T) {
 	keyword := jobs.Keyword
 
 	//Get Jobs by Asset URL
-	if len(*assetUrl) > 1 {
+	if len(*assetUrl) > 0 {
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("assetUrl", *assetUrl)
 		toJobs, _, _ = TOSession.GetInvalidationJobs(opts)
@@ -414,7 +414,7 @@ func GetTestJobsByValidData(t *testing.T) {
 	}
 
 	//Get Jobs by ID
-	if *id > 1 {
+	if id != nil && *id >= 1 {
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("id", strconv.FormatUint(uint64(*id), 10))
 		toJobs, _, _ = TOSession.GetInvalidationJobs(opts)
@@ -426,7 +426,7 @@ func GetTestJobsByValidData(t *testing.T) {
 	}
 
 	//Get Jobs by Keyword
-	if len(*keyword) > 1 {
+	if keyword != nil && len(*keyword) > 1 {
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("Keyword", *keyword)
 		toJobs, _, _ = TOSession.GetInvalidationJobs(opts)
@@ -438,7 +438,7 @@ func GetTestJobsByValidData(t *testing.T) {
 	}
 
 	//Get Delivery Service ID by Name
-	if len(*dsName) > 0 {
+	if dsName != nil && len(*dsName) > 0 {
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("xmlId", *dsName)
 		toDSes, _, _ := TOSession.GetDeliveryServices(opts)
@@ -468,7 +468,7 @@ func GetTestJobsByValidData(t *testing.T) {
 	userResp, _, _ := TOSession.GetUsers(opts)
 	if len(userResp.Response) > 0 {
 		userId := userResp.Response[0].ID
-		if *userId > 0 {
+		if userId != nil && *userId > 0 {
 			//Get Jobs by userID
 			opts := client.NewRequestOptions()
 			opts.QueryParameters.Set("userId", strconv.Itoa(*userId))
diff --git a/traffic_ops/testing/api/v4/loginfail_test.go b/traffic_ops/testing/api/v4/loginfail_test.go
index ad0bfe5..cb4ccd8 100644
--- a/traffic_ops/testing/api/v4/loginfail_test.go
+++ b/traffic_ops/testing/api/v4/loginfail_test.go
@@ -67,7 +67,7 @@ func PostTestLoginFail(t *testing.T) {
 }
 
 func LoginWithEmptyCredentialsTest(t *testing.T) {
-	userAgent := "to-api-v3-client-tests-loginfailtest"
+	userAgent := "to-api-v4-client-tests-loginfailtest"
 	_, _, err := toclient.LoginWithAgent(Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, "", true, userAgent, false, time.Second*time.Duration(Config.Default.Session.TimeoutInSecs))
 	if err == nil {
 		t.Fatal("expected error when logging in with empty credentials, actual nil")
diff --git a/traffic_ops/testing/api/v4/profile_parameters_test.go b/traffic_ops/testing/api/v4/profile_parameters_test.go
index fa11548..17e5a88 100644
--- a/traffic_ops/testing/api/v4/profile_parameters_test.go
+++ b/traffic_ops/testing/api/v4/profile_parameters_test.go
@@ -45,26 +45,24 @@ func TestProfileParameters(t *testing.T) {
 
 func CreateDeleteProfileParameterWithLocks(t *testing.T) {
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(1)
+	user1.TenantID = 1
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err := TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s", user1.Username)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
diff --git a/traffic_ops/testing/api/v4/profiles_test.go b/traffic_ops/testing/api/v4/profiles_test.go
index b9ef892..3ef9382 100644
--- a/traffic_ops/testing/api/v4/profiles_test.go
+++ b/traffic_ops/testing/api/v4/profiles_test.go
@@ -64,26 +64,24 @@ func CUDProfileWithLocks(t *testing.T) {
 	}
 
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(resp.Response[0].ID)
+	user1.TenantID = resp.Response[0].ID
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err = TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s", user1.Username)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
diff --git a/traffic_ops/testing/api/v4/roles_test.go b/traffic_ops/testing/api/v4/roles_test.go
index a25bc7d..d933dad 100644
--- a/traffic_ops/testing/api/v4/roles_test.go
+++ b/traffic_ops/testing/api/v4/roles_test.go
@@ -20,7 +20,6 @@ import (
 	"net/url"
 	"reflect"
 	"sort"
-	"strconv"
 	"testing"
 	"time"
 
@@ -63,30 +62,26 @@ func UpdateTestRolesWithHeaders(t *testing.T, header http.Header) {
 		t.Fatal("Need at least one Role to test updating a Role with HTTP headers")
 	}
 	firstRole := testData.Roles[0]
-	if firstRole.Name == nil {
-		t.Fatal("Found a Role in the testing data with null or undefined name")
-	}
 
 	// Retrieve the Role by role so we can get the id for the Update
 	opts := client.NewRequestOptions()
 	opts.Header = header
-	opts.QueryParameters.Set("name", *firstRole.Name)
+	opts.QueryParameters.Set("name", firstRole.Name)
 	resp, _, err := TOSession.GetRoles(opts)
 	if err != nil {
-		t.Errorf("cannot get Role '%s' by name: %v - alerts: %+v", *firstRole.Name, err, resp.Alerts)
+		t.Errorf("cannot get Role '%s' by name: %v - alerts: %+v", firstRole.Name, err, resp.Alerts)
 	}
 	if len(resp.Response) != 1 {
-		t.Fatalf("Expected exactly one Role to exist with name '%s', found: %d", *firstRole.Name, len(resp.Response))
+		t.Fatalf("Expected exactly one Role to exist with name '%s', found: %d", firstRole.Name, len(resp.Response))
 	}
 	remoteRole := resp.Response[0]
-	if remoteRole.ID == nil {
-		t.Fatal("Traffic Ops returned a representation for a Role with null or undefined ID")
-	}
-
-	expectedRole := "new admin2"
-	remoteRole.Name = &expectedRole
+	expectedDescription := "new description"
+	remoteRole.Description = expectedDescription
 	opts.QueryParameters.Del("name")
-	_, reqInf, _ := TOSession.UpdateRole(*remoteRole.ID, remoteRole, opts)
+	_, reqInf, err := TOSession.UpdateRole(remoteRole.Name, remoteRole, opts)
+	if err == nil {
+		t.Errorf("updating role with name: %s, expected an error stating resource was modified, but got nothing", remoteRole.Name)
+	}
 	if reqInf.StatusCode != http.StatusPreconditionFailed {
 		t.Errorf("Expected status code 412, got %v", reqInf.StatusCode)
 	}
@@ -97,12 +92,9 @@ func GetTestRolesIMSAfterChange(t *testing.T, header http.Header) {
 		t.Fatalf("Need at least %d Roles to test getting Roles with IMS change", roleGood+1)
 	}
 	role := testData.Roles[roleGood]
-	if role.Name == nil {
-		t.Fatal("Found a Role in the testing data with null or undefined name")
-	}
 
 	opts := client.NewRequestOptions()
-	opts.QueryParameters.Set("name", *role.Name)
+	opts.QueryParameters.Set("name", role.Name)
 	opts.Header = header
 	resp, reqInf, err := TOSession.GetRoles(opts)
 	if err != nil {
@@ -130,11 +122,6 @@ func GetTestRolesIMS(t *testing.T) {
 	if len(testData.Roles) < roleGood+1 {
 		t.Fatalf("Need at least %d Roles to test getting Roles with IMS change", roleGood+1)
 	}
-	role := testData.Roles[roleGood]
-	if role.Name == nil {
-		t.Fatal("Found a Role in the testing data with null or undefined name")
-	}
-
 	futureTime := time.Now().AddDate(0, 0, 1)
 	time := futureTime.Format(time.RFC1123)
 	opts := client.NewRequestOptions()
@@ -155,22 +142,10 @@ func CreateTestRoles(t *testing.T) {
 	if len(testData.Roles) > 3 {
 		t.Fatal("Too many Roles in the test data. Tests can only handle 3")
 	}
-
-	expectedAlerts := []string{
-		"",
-		"can not add non-existent capabilities: [invalid-capability]",
-		"",
-	}
-	for i, role := range testData.Roles {
-		alerts, _, err := TOSession.CreateRole(role, client.RequestOptions{})
+	for _, role := range testData.Roles {
+		_, _, err := TOSession.CreateRole(role, client.RequestOptions{})
 		if err != nil {
-			if expectedAlerts[i] == "" {
-				t.Errorf("Unexpected error creating a Role: %v - alerts: %+v", err, alerts.Alerts)
-			} else if !alertsHaveError(alerts.Alerts, expectedAlerts[i]) {
-				t.Errorf("expected: error containing '%s', actual: %v - alerts: %+v", expectedAlerts[i], err, alerts.Alerts)
-			}
-		} else if expectedAlerts[i] != "" {
-			t.Errorf("expected: error containing '%s', actual: nil", expectedAlerts[i])
+			t.Errorf("no error expected, but got %v", err)
 		}
 	}
 }
@@ -183,11 +158,7 @@ func SortTestRoles(t *testing.T) {
 
 	sortedList := make([]string, 0, len(resp.Response))
 	for _, role := range resp.Response {
-		if role.Name == nil {
-			t.Error("Traffic Ops returned a representation for a Role with null or undefined name")
-			continue
-		}
-		sortedList = append(sortedList, *role.Name)
+		sortedList = append(sortedList, role.Name)
 	}
 
 	if !sort.StringsAreSorted(sortedList) {
@@ -200,55 +171,44 @@ func UpdateTestRoles(t *testing.T) {
 		t.Fatalf("Need at least on Role to test updating Roles")
 	}
 	firstRole := testData.Roles[0]
-	if firstRole.Name == nil {
-		t.Fatal("Found Role with null or undefined name in test data")
-	}
-
 	// Retrieve the Role by role so we can get the id for the Update
 	opts := client.NewRequestOptions()
-	opts.QueryParameters.Set("name", *firstRole.Name)
+	opts.QueryParameters.Set("name", firstRole.Name)
 	resp, _, err := TOSession.GetRoles(opts)
 	if err != nil {
-		t.Errorf("cannot get Role '%s' by name: %v - alerts: %+v", *firstRole.Name, err, resp.Alerts)
+		t.Errorf("cannot get Role '%s' by name: %v - alerts: %+v", firstRole.Name, err, resp.Alerts)
 	}
 	if len(resp.Response) != 1 {
-		t.Fatalf("Expected exactly one Role to exist with name '%s', found: %d", *firstRole.Name, len(resp.Response))
+		t.Fatalf("Expected exactly one Role to exist with name '%s', found: %d", firstRole.Name, len(resp.Response))
 	}
 	remoteRole := resp.Response[0]
-	if remoteRole.ID == nil {
-		t.Fatal("Role returned from Traffic Ops had null or undefined ID")
-	}
-
-	expectedRole := "new admin2"
-	remoteRole.Name = &expectedRole
+	expectedDescription := "new description"
+	remoteRole.Description = expectedDescription
 	var alert tc.Alerts
-	alert, _, err = TOSession.UpdateRole(*remoteRole.ID, remoteRole, client.RequestOptions{})
+	alert, _, err = TOSession.UpdateRole(remoteRole.Name, remoteRole, client.RequestOptions{})
 	if err != nil {
 		t.Fatalf("cannot update Role: %v - alerts: %+v", err, alert.Alerts)
 	}
 
 	// Retrieve the Role to check role got updated
 	opts.QueryParameters.Del("name")
-	opts.QueryParameters.Set("id", strconv.Itoa(*remoteRole.ID))
+	opts.QueryParameters.Set("name", remoteRole.Name)
 	resp, _, err = TOSession.GetRoles(opts)
 	if err != nil {
 		t.Errorf("cannot get Role by ID after update: %v - alerts: %+v", err, resp.Alerts)
 	}
 	if len(resp.Response) != 1 {
-		t.Fatalf("Expected exactly one Role to exist with ID %d, found: %d", *remoteRole.ID, len(resp.Response))
+		t.Fatalf("Expected exactly one Role to exist with name %s, found: %d", remoteRole.Name, len(resp.Response))
 	}
 	respRole := resp.Response[0]
-	if respRole.Name == nil {
-		t.Fatal("Traffic Ops returned a representation for a Role that had null or undefined name")
-	}
 
-	if *respRole.Name != expectedRole {
-		t.Errorf("results do not match actual: %s, expected: %s", *respRole.Name, expectedRole)
+	if respRole.Description != expectedDescription {
+		t.Errorf("results do not match actual: %s, expected: %s", respRole.Description, expectedDescription)
 	}
 
 	// Set the name back to the fixture value so we can delete it after
 	remoteRole.Name = firstRole.Name
-	alert, _, err = TOSession.UpdateRole(*remoteRole.ID, remoteRole, client.RequestOptions{})
+	alert, _, err = TOSession.UpdateRole(remoteRole.Name, remoteRole, client.RequestOptions{})
 	if err != nil {
 		t.Errorf("cannot update Role: %v - alerts: %+v", err, alert.Alerts)
 	}
@@ -260,15 +220,12 @@ func GetTestRoles(t *testing.T) {
 		t.Fatalf("Need at least %d Roles to test getting Roles with IMS change", roleGood+1)
 	}
 	role := testData.Roles[roleGood]
-	if role.Name == nil {
-		t.Fatal("Found a Role in the testing data with null or undefined name")
-	}
 
 	opts := client.NewRequestOptions()
-	opts.QueryParameters.Set("name", *role.Name)
+	opts.QueryParameters.Set("name", role.Name)
 	resp, _, err := TOSession.GetRoles(opts)
 	if err != nil {
-		t.Errorf("cannot get Role '%s' by name: %v - alerts: %+v", *role.Name, err, resp)
+		t.Errorf("cannot get Role '%s' by name: %v - alerts: %+v", role.Name, err, resp)
 	}
 
 }
@@ -305,42 +262,10 @@ func VerifyGetRolesOrder(t *testing.T) {
 }
 
 func DeleteTestRoles(t *testing.T) {
-	if len(testData.Roles) < roleGood+1 {
-		t.Fatalf("Need at least %d Roles to test getting Roles with IMS change", roleGood+1)
-	}
-	role := testData.Roles[roleGood]
-	if role.Name == nil {
-		t.Fatal("Found a Role in the testing data with null or undefined name")
-	}
-
-	// Retrieve the Role by name so we can get the id
-	opts := client.NewRequestOptions()
-	opts.QueryParameters.Set("name", *role.Name)
-	resp, _, err := TOSession.GetRoles(opts)
-	if err != nil {
-		t.Errorf("cannot get Role '%s' by name: %v - alerts: %+v", *role.Name, err, resp.Alerts)
-	}
-	if len(resp.Response) != 1 {
-		t.Fatalf("Expected exactly one Role to exist with name '%s', found: %d", *role.Name, len(resp.Response))
-	}
-	respRole := resp.Response[0]
-	if respRole.ID == nil {
-		t.Fatal("Traffic Ops returned a representation for a Role that had null or undefined ID")
-	}
-
-	delResp, _, err := TOSession.DeleteRole(*respRole.ID, client.RequestOptions{})
-
-	if err != nil {
-		t.Errorf("cannot delete Role: %v - alerts: %+v", err, delResp.Alerts)
-	}
-
-	// Retrieve the Role to see if it got deleted
-	roleResp, _, err := TOSession.GetRoles(opts)
-	if err != nil {
-		t.Errorf("error fetching Role after deletion: %v - alerts: %+v", err, roleResp.Alerts)
-	}
-	if len(roleResp.Response) > 0 {
-		t.Errorf("expected Role '%s' to be deleted, but it was found in Traffic Ops", *role.Name)
+	for _, r := range testData.Roles {
+		_, _, err := TOSession.DeleteRole(r.Name, client.NewRequestOptions())
+		if err != nil {
+			t.Errorf("expected no error while deleting role %s, but got %v", r.Name, err)
+		}
 	}
-
 }
diff --git a/traffic_ops/testing/api/v4/servers_test.go b/traffic_ops/testing/api/v4/servers_test.go
index 064621f..4c7b464 100644
--- a/traffic_ops/testing/api/v4/servers_test.go
+++ b/traffic_ops/testing/api/v4/servers_test.go
@@ -69,26 +69,24 @@ func CUDServerWithLocks(t *testing.T) {
 	}
 
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(resp.Response[0].ID)
+	user1.TenantID = resp.Response[0].ID
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err = TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s", user1.Username)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
diff --git a/traffic_ops/testing/api/v4/staticdnsentries_test.go b/traffic_ops/testing/api/v4/staticdnsentries_test.go
index f7190e490..266bf76 100644
--- a/traffic_ops/testing/api/v4/staticdnsentries_test.go
+++ b/traffic_ops/testing/api/v4/staticdnsentries_test.go
@@ -54,26 +54,24 @@ func TestStaticDNSEntries(t *testing.T) {
 
 func CreateUpdateDeleteStaticDNSEntriesWithLocks(t *testing.T) {
 	// Create a new user with operations level privileges
-	user1 := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("lock_user1"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("operations"),
-		},
+	user1 := tc.UserV4{
+		Username:             "lock_user1",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "operations",
 	}
 	user1.Email = util.StrPtr("lockuseremail@domain.com")
-	user1.TenantID = util.IntPtr(1)
+	user1.TenantID = 1
 	user1.FullName = util.StrPtr("firstName LastName")
 	_, _, err := TOSession.CreateUser(user1, client.RequestOptions{})
 	if err != nil {
-		t.Fatalf("could not create test user with username: %s", *user1.Username)
+		t.Fatalf("could not create test user with username: %s", user1.Username)
 	}
 	defer ForceDeleteTestUsersByUsernames(t, []string{"lock_user1"})
 
 	// Establish a session with the newly created non admin level user
-	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, *user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
+	userSession, _, err := client.LoginWithAgent(Config.TrafficOps.URL, user1.Username, *user1.LocalPassword, true, "to-api-v4-client-tests", false, toReqTimeout)
 	if err != nil {
 		t.Fatalf("could not login with user lock_user1: %v", err)
 	}
diff --git a/traffic_ops/testing/api/v4/tc-fixtures.json b/traffic_ops/testing/api/v4/tc-fixtures.json
index a4f83f6..2844182 100644
--- a/traffic_ops/testing/api/v4/tc-fixtures.json
+++ b/traffic_ops/testing/api/v4/tc-fixtures.json
@@ -2526,21 +2526,17 @@
         {
             "name": "new_admin",
             "description": "super-user 2",
-            "privLevel": 30,
-            "capabilities": [
+            "permissions": [
                 "all-read",
-                "all-write",
-                "cdn-read"
+                "all-write"
             ]
         },
         {
-            "name": "bad_admin",
+            "name": "another_role",
             "description": "super-user 3",
-            "privLevel": 30,
-            "capabilities": [
+            "permissions": [
                 "all-read",
-                "all-write",
-                "invalid-capability"
+                "all-write"
             ]
         }
     ],
@@ -5466,7 +5462,7 @@
             "phoneNumber": "810-555-9876",
             "postalCode": "55443",
             "publicSshKey": "",
-            "role": 4,
+            "role": "admin",
             "stateOrProvince": "LA",
             "tenant": "root",
             "token": "test",
@@ -5488,8 +5484,7 @@
             "phoneNumber": "",
             "postalCode": "",
             "publicSshKey": "",
-            "registrationSent": "",
-            "role": 1,
+            "role": "disallowed",
             "stateOrProvince": "",
             "tenant": "tenant1",
             "token": "quest",
@@ -5511,8 +5506,7 @@
             "phoneNumber": "",
             "postalCode": "",
             "publicSshKey": "",
-            "registrationSent": "",
-            "role": 2,
+            "role": "read-only user",
             "stateOrProvince": "",
             "tenant": "tenant1",
             "uid": 0,
@@ -5533,7 +5527,7 @@
             "phoneNumber": "810-555-9876",
             "postalCode": "55443",
             "publicSshKey": "",
-            "role": 4,
+            "role": "admin",
             "stateOrProvince": "LA",
             "tenant": "tenant3",
             "uid": 0,
@@ -5554,7 +5548,7 @@
             "phoneNumber": "810-555-9876",
             "postalCode": "55443",
             "publicSshKey": "",
-            "role": 4,
+            "role": "admin",
             "stateOrProvince": "LA",
             "tenant": "tenant4",
             "uid": 0,
@@ -5575,8 +5569,7 @@
             "phoneNumber": "",
             "postalCode": "",
             "publicSshKey": "",
-            "registrationSent": "",
-            "role": 3,
+            "role": "operations",
             "stateOrProvince": "",
             "tenant": "root",
             "uid": 0,
@@ -5597,8 +5590,7 @@
             "phoneNumber": "",
             "postalCode": "",
             "publicSshKey": "",
-            "registrationSent": "",
-            "role": 6,
+            "role": "steering",
             "stateOrProvince": "",
             "tenant": "root",
             "uid": 0,
diff --git a/traffic_ops/testing/api/v4/topologies_test.go b/traffic_ops/testing/api/v4/topologies_test.go
index c7e9b31..05db1cd 100644
--- a/traffic_ops/testing/api/v4/topologies_test.go
+++ b/traffic_ops/testing/api/v4/topologies_test.go
@@ -991,17 +991,15 @@ func CRUDTopologyReadOnlyUser(t *testing.T) {
 	}
 
 	toReqTimeout := time.Second * time.Duration(Config.Default.Session.TimeoutInSecs)
-	user := tc.UserV40{
-		User: tc.User{
-			Username:             util.StrPtr("test_user"),
-			RegistrationSent:     tc.TimeNoModFromTime(time.Now()),
-			LocalPassword:        util.StrPtr("test_pa$$word"),
-			ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
-			RoleName:             util.StrPtr("read-only user"),
-		},
+	user := tc.UserV4{
+		Username:             "test_user",
+		RegistrationSent:     new(time.Time),
+		LocalPassword:        util.StrPtr("test_pa$$word"),
+		ConfirmLocalPassword: util.StrPtr("test_pa$$word"),
+		Role:                 "read-only user",
 	}
 	user.Email = util.StrPtr("email@domain.com")
-	user.TenantID = util.IntPtr(resp.Response[0].ID)
+	user.TenantID = resp.Response[0].ID
 	user.FullName = util.StrPtr("firstName LastName")
 
 	u, _, err := TOSession.CreateUser(user, client.RequestOptions{})
@@ -1065,9 +1063,7 @@ func CRUDTopologyReadOnlyUser(t *testing.T) {
 		t.Errorf("expected error about Read-Only users not being able to delete topologies, but got nothing")
 	}
 
-	if u.Response.Username != nil {
-		ForceDeleteTestUsersByUsernames(t, []string{"test_user"})
-	}
+	ForceDeleteTestUsersByUsernames(t, []string{"test_user"})
 }
 
 func UpdateTopologyWithCachegroupAssignedToBecomeParentOfItself(t *testing.T) {
diff --git a/traffic_ops/testing/api/v4/traffic_control_test.go b/traffic_ops/testing/api/v4/traffic_control_test.go
index a686023..2de4786 100644
--- a/traffic_ops/testing/api/v4/traffic_control_test.go
+++ b/traffic_ops/testing/api/v4/traffic_control_test.go
@@ -41,7 +41,7 @@ type TrafficControl struct {
 	ProfileParameters                                 []tc.ProfileParameter                   `json:"profileParameters"`
 	PhysLocations                                     []tc.PhysLocation                       `json:"physLocations"`
 	Regions                                           []tc.Region                             `json:"regions"`
-	Roles                                             []tc.Role                               `json:"roles"`
+	Roles                                             []tc.RoleV4                             `json:"roles"`
 	Servers                                           []tc.ServerV40                          `json:"servers"`
 	ServerServerCapabilities                          []tc.ServerServerCapability             `json:"serverServerCapabilities"`
 	ServerCapabilities                                []tc.ServerCapability                   `json:"serverCapabilities"`
@@ -55,6 +55,6 @@ type TrafficControl struct {
 	Types                                             []tc.Type                               `json:"types"`
 	SteeringTargets                                   []tc.SteeringTargetNullable             `json:"steeringTargets"`
 	Serverchecks                                      []tc.ServercheckRequestNullable         `json:"serverchecks"`
-	Users                                             []tc.UserV40                            `json:"users"`
+	Users                                             []tc.UserV4                             `json:"users"`
 	InvalidationJobs                                  []tc.InvalidationJobInput               `json:"invalidationJobs"`
 }
diff --git a/traffic_ops/testing/api/v4/user_test.go b/traffic_ops/testing/api/v4/user_test.go
index af7d7a4..8acd77e 100644
--- a/traffic_ops/testing/api/v4/user_test.go
+++ b/traffic_ops/testing/api/v4/user_test.go
@@ -15,7 +15,6 @@ package v4
 */
 
 import (
-	"bytes"
 	"fmt"
 	"net/http"
 	"net/mail"
@@ -42,7 +41,6 @@ func TestUsers(t *testing.T) {
 		SortTestUsers(t)
 		UpdateTestUsers(t)
 		GetTestUsersIMSAfterChange(t, header)
-		RolenameCapitalizationTest(t)
 		OpsUpdateAdminTest(t)
 		UserSelfUpdateTest(t)
 		UserUpdateOwnRoleTest(t)
@@ -108,71 +106,6 @@ func CreateTestUsers(t *testing.T) {
 	}
 }
 
-func RolenameCapitalizationTest(t *testing.T) {
-
-	roles, _, err := TOSession.GetRoles(client.RequestOptions{})
-	if err != nil {
-		t.Errorf("could not get roles: %v - alerts: %+v", err, roles.Alerts)
-	}
-	if len(roles.Response) == 0 {
-		t.Fatal("there should be at least one role to test the user")
-	}
-	if roles.Response[0].ID == nil {
-		t.Fatal("Traffic Ops returned a representation of a Role that had null or undefined ID")
-	}
-
-	tenants, _, err := TOSession.GetTenants(client.RequestOptions{})
-	if err != nil {
-		t.Errorf("could not get tenants: %v", err)
-	}
-	if len(tenants.Response) == 0 {
-		t.Fatal("there should be at least one tenant to test the user")
-	}
-
-	// this user never does anything, so the role and tenant aren't important
-	blob := fmt.Sprintf(`
-	{
-		"username": "test_user",
-		"email": "cooldude6@example.com",
-		"fullName": "full name is required",
-		"localPasswd": "better_twelve",
-		"confirmLocalPasswd": "better_twelve",
-		"role": %d,
-		"tenantId": %d
-	}`, *roles.Response[0].ID, tenants.Response[0].ID)
-
-	reader := strings.NewReader(blob)
-	request, err := http.NewRequest("POST", fmt.Sprintf("%v%s/users", TOSession.URL, TestAPIBase), reader)
-	if err != nil {
-		t.Errorf("could not make new request: %v", err)
-	}
-	resp, err := TOSession.Client.Do(request)
-	if err != nil {
-		t.Errorf("could not do request: %v", err)
-	}
-
-	buf := new(bytes.Buffer)
-	buf.ReadFrom(resp.Body)
-	strResp := buf.String()
-	if !strings.Contains(strResp, "roleName") {
-		t.Error("incorrect json was returned for POST")
-	}
-
-	request, err = http.NewRequest("GET", fmt.Sprintf("%v%s/users?username=test_user", TOSession.URL, TestAPIBase), nil)
-	if err != nil {
-		t.Fatalf("Failed to create HTTP request: %v", err)
-	}
-	resp, _ = TOSession.Client.Do(request)
-
-	buf = new(bytes.Buffer)
-	buf.ReadFrom(resp.Body)
-	strResp = buf.String()
-	if !strings.Contains(strResp, "rolename") {
-		t.Error("incorrect json was returned for GET")
-	}
-
-}
-
 func OpsUpdateAdminTest(t *testing.T) {
 	toReqTimeout := time.Second * time.Duration(Config.Default.Session.TimeoutInSecs)
 	opsTOClient, _, err := client.LoginWithAgent(TOSession.URL, "opsuser", "pa$$word", true, "to-api-v3-client-tests/opsuser", true, toReqTimeout)
@@ -212,11 +145,7 @@ func SortTestUsers(t *testing.T) {
 	}
 	sortedList := make([]string, 0, len(resp.Response))
 	for _, user := range resp.Response {
-		if user.Username == nil {
-			t.Errorf("Traffic Ops returned a representation for a user with null or undefined username")
-			continue
-		}
-		sortedList = append(sortedList, *user.Username)
+		sortedList = append(sortedList, user.Username)
 	}
 
 	if !sort.StringsAreSorted(sortedList) {
@@ -229,8 +158,8 @@ func UserRegistrationTest(t *testing.T) {
 	var emails []string
 	opts := client.NewRequestOptions()
 	for _, user := range testData.Users {
-		if user.Tenant == nil || user.Role == nil || user.Email == nil {
-			t.Error("Found User in the testing data with null or undefined Tenant and/or Role ID and/or Email address")
+		if user.Tenant == nil || user.Email == nil {
+			t.Error("Found User in the testing data with null or undefined Tenant and/or Email address")
 			continue
 		}
 		opts.QueryParameters.Set("name", *user.Tenant)
@@ -243,7 +172,7 @@ func UserRegistrationTest(t *testing.T) {
 		}
 		tenant := resp.Response[0]
 
-		regResp, _, err := TOSession.RegisterNewUser(uint(tenant.ID), uint(*user.Role), rfc.EmailAddress{Address: mail.Address{Address: *user.Email}}, client.RequestOptions{})
+		regResp, _, err := TOSession.RegisterNewUser(uint(tenant.ID), user.Role, rfc.EmailAddress{Address: mail.Address{Address: *user.Email}}, client.RequestOptions{})
 		if err != nil {
 			t.Fatalf("could not register user: %v - alerts: %+v", err, regResp.Alerts)
 		}
@@ -378,11 +307,11 @@ func UserUpdateOwnRoleTest(t *testing.T) {
 		t.Errorf("cannot get users filtered by username '%s': %v - alerts: %+v", SessionUserName, err, resp.Alerts)
 	}
 	user := resp.Response[0]
-	if user.Role == nil || user.ID == nil {
-		t.Fatalf("Traffic Ops returned a representation for user '%s' with null or undefined Role ID and/or ID", SessionUserName)
+	if user.ID == nil {
+		t.Fatalf("Traffic Ops returned a representation for user '%s' with null or undefined ID", SessionUserName)
 	}
 
-	*user.Role = *user.Role + 1
+	user.Role = user.Role + "_updated"
 	_, _, err = TOSession.UpdateUser(*user.ID, user, client.RequestOptions{})
 	if err == nil {
 		t.Error("user incorrectly updated their role")
@@ -393,10 +322,7 @@ func UpdateTestUsers(t *testing.T) {
 	if len(testData.Users) < 1 {
 		t.Fatal("Need at least one User to test updating users")
 	}
-	if testData.Users[0].Username == nil {
-		t.Fatal("Found a user in the test data with null or undefined username")
-	}
-	firstUsername := *testData.Users[0].Username
+	firstUsername := testData.Users[0].Username
 
 	opts := client.NewRequestOptions()
 	opts.QueryParameters.Set("username", firstUsername)
@@ -414,7 +340,7 @@ func UpdateTestUsers(t *testing.T) {
 	newCity := "kidz kable kown"
 	*user.City = newCity
 
-	var updateResp tc.UpdateUserResponse
+	var updateResp tc.UpdateUserResponseV4
 	updateResp, _, err = TOSession.UpdateUser(*user.ID, user, client.RequestOptions{})
 	if err != nil {
 		t.Errorf("cannot update user: %v - alerts: %+v", err, updateResp.Alerts)
@@ -457,9 +383,6 @@ func GetTestUsers(t *testing.T) {
 	if len(resp.Response) < 1 {
 		t.Fatalf("expected a users list, got nothing")
 	}
-	if resp.Response[0].LastAuthenticated == nil {
-		t.Errorf("current user's authenticated time, expected: '%s' actual: %v", resp.Response[0].LastAuthenticated, nil)
-	}
 }
 
 func GetTestUserCurrent(t *testing.T) {
@@ -467,13 +390,8 @@ func GetTestUserCurrent(t *testing.T) {
 	if err != nil {
 		t.Errorf("cannot get current user: %v - alerts: %+v", err, user.Alerts)
 	}
-	if user.Response.UserName == nil {
-		t.Errorf("current user expected: '%s' actual: %v", SessionUserName, nil)
-	} else if *user.Response.UserName != SessionUserName {
-		t.Errorf("current user expected: '%s' actual: '%s'", SessionUserName, *user.Response.UserName)
-	}
-	if user.Response.LastAuthenticated == nil {
-		t.Errorf("current user's authenticated time, expected: '%s' actual: %v", user.Response.LastAuthenticated, nil)
+	if user.Response.UserName != SessionUserName {
+		t.Errorf("current user expected: '%s' actual: '%s'", SessionUserName, user.Response.UserName)
 	}
 }
 
@@ -486,18 +404,18 @@ func UserTenancyTest(t *testing.T) {
 	tenant4Found := false
 	tenant3Username := "tenant3user"
 	tenant4Username := "tenant4user"
-	tenant3User := tc.UserV40{}
+	tenant3User := tc.UserV4{}
 
 	// assert admin user can view tenant3user and tenant4user
 	for _, user := range users.Response {
-		if user.Username == nil || user.ID == nil {
-			t.Error("Traffic Ops returned a representation for a user with null or undefined username and/or ID")
+		if user.ID == nil {
+			t.Error("Traffic Ops returned a representation for a user with null or undefined ID")
 			continue
 		}
-		if *user.Username == tenant3Username {
+		if user.Username == tenant3Username {
 			tenant3Found = true
 			tenant3User = user
-		} else if *user.Username == tenant4Username {
+		} else if user.Username == tenant4Username {
 			tenant4Found = true
 		}
 		if tenant3Found && tenant4Found {
@@ -521,16 +439,12 @@ func UserTenancyTest(t *testing.T) {
 
 	tenant4canReadItself := false
 	for _, user := range usersReadableByTenant4.Response {
-		if user.Username == nil {
-			t.Error("Traffic Ops returned a representation of a user with null or undefined username")
-			continue
-		}
 		// assert that tenant4user cannot read tenant3user
-		if *user.Username == tenant3Username {
+		if user.Username == tenant3Username {
 			t.Error("expected tenant4user to be unable to read tenant3user")
 		}
 		// assert that tenant4user can read itself
-		if *user.Username == tenant4Username {
+		if user.Username == tenant4Username {
 			tenant4canReadItself = true
 		}
 	}
@@ -560,8 +474,8 @@ func UserTenancyTest(t *testing.T) {
 	}
 	newUser := testData.Users[0]
 	newUser.Email = util.StrPtr("testusertenancy@example.com")
-	newUser.Username = util.StrPtr("testusertenancy")
-	newUser.TenantID = &rootTenant.ID
+	newUser.Username = "testusertenancy"
+	newUser.TenantID = rootTenant.ID
 	if _, _, err = tenant4TOClient.CreateUser(newUser, client.RequestOptions{}); err == nil {
 		t.Error("expected tenant4user to be unable to create a new user in the root tenant")
 	}
@@ -581,7 +495,7 @@ func ForceDeleteTestUsers(t *testing.T) {
 
 	var usernames []string
 	for _, user := range testData.Users {
-		usernames = append(usernames, `'`+*user.Username+`'`)
+		usernames = append(usernames, `'`+user.Username+`'`)
 	}
 
 	// there is a constraint that prevents users from being deleted when they have a log
@@ -629,14 +543,10 @@ func ForceDeleteTestUsersByUsernames(t *testing.T, usernames []string) {
 func DeleteTestUsers(t *testing.T) {
 	opts := client.NewRequestOptions()
 	for _, user := range testData.Users {
-		if user.Username == nil {
-			t.Error("Found a user in the testing data with null or undefined username")
-			continue
-		}
-		opts.QueryParameters.Set("username", *user.Username)
+		opts.QueryParameters.Set("username", user.Username)
 		resp, _, err := TOSession.GetUsers(opts)
 		if err != nil {
-			t.Errorf("cannot get users filtered by username '%s': %v - alerts: %+v", *user.Username, err, resp.Alerts)
+			t.Errorf("cannot get users filtered by username '%s': %v - alerts: %+v", user.Username, err, resp.Alerts)
 		}
 		if len(resp.Response) > 0 {
 			respUser := resp.Response[0]
@@ -647,7 +557,7 @@ func DeleteTestUsers(t *testing.T) {
 
 			delResp, _, err := TOSession.DeleteUser(*respUser.ID, client.RequestOptions{})
 			if err != nil {
-				t.Errorf("cannot delete user '%s': %v - alerts: %+v", *user.Username, err, delResp.Alerts)
+				t.Errorf("cannot delete user '%s': %v - alerts: %+v", user.Username, err, delResp.Alerts)
 			}
 
 			// Make sure it got deleted
@@ -656,7 +566,7 @@ func DeleteTestUsers(t *testing.T) {
 				t.Errorf("error getting users filtered by username after supposed deletion: %v - alerts: %+v", err, resp.Alerts)
 			}
 			if len(resp.Response) > 0 {
-				t.Errorf("expected user: %s to be deleted", *user.Username)
+				t.Errorf("expected user: %s to be deleted", user.Username)
 			}
 		}
 	}
diff --git a/traffic_ops/traffic_ops_golang/apicapability/api_capabilities.go b/traffic_ops/traffic_ops_golang/apicapability/api_capabilities.go
index 89d04b3..32fe164 100644
--- a/traffic_ops/traffic_ops_golang/apicapability/api_capabilities.go
+++ b/traffic_ops/traffic_ops_golang/apicapability/api_capabilities.go
@@ -1,3 +1,9 @@
+// Package apicapability defines the API handlers for Traffic Ops's API's
+// /api_capabilities endpoint.
+//
+// Deprecated: "Capabilities" (now called Permissions) are no longer handled
+// this way, and this package should be removed once API versions that use it
+// have been fully removed.
 package apicapability
 
 import (
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 8d70fc3..f891a9c 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -419,16 +419,79 @@ func GetPrivLevelFromRoleID(tx *sql.Tx, id int) (int, bool, error) {
 	var privLevel int
 	err := tx.QueryRow(`SELECT priv_level FROM role WHERE role.id = $1`, id).Scan(&privLevel)
 
-	if err == sql.ErrNoRows {
+	if errors.Is(err, sql.ErrNoRows) {
+		return 0, false, nil
+	}
+
+	if err != nil {
+		return 0, false, fmt.Errorf("getting priv_level from role ID: %w", err)
+	}
+	return privLevel, true, nil
+}
+
+// GetPrivLevelFromRole returns the priv_level associated with a role, whether it exists, and any error.
+// This method exists on a temporary basis. After priv_level is fully deprecated and capabilities take over,
+// this method will not only no longer be needed, but the corresponding new privilege check should be done
+// via the primary database query for the users endpoint. The users json response will contain a list of
+// capabilities in the future, whereas now the users json response currently does not contain privLevel.
+// See the wiki page on the roles/capabilities as a system:
+// https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=68715910
+func GetPrivLevelFromRole(tx *sql.Tx, role string) (int, bool, error) {
+	var privLevel int
+	err := tx.QueryRow(`SELECT priv_level FROM role WHERE role.name = $1`, role).Scan(&privLevel)
+
+	if errors.Is(err, sql.ErrNoRows) {
 		return 0, false, nil
 	}
 
 	if err != nil {
-		return 0, false, fmt.Errorf("getting priv_level from role: %v", err)
+		return 0, false, fmt.Errorf("getting priv_level from role: %w", err)
 	}
 	return privLevel, true, nil
 }
 
+// GetCapabilitiesFromRoleID returns the capabilities for the supplied role ID.
+func GetCapabilitiesFromRoleID(tx *sql.Tx, roleID int) ([]string, error) {
+	var caps []string
+	var cap string
+
+	rows, err := tx.Query(`SELECT cap_name FROM role_capability WHERE role_id = $1`, roleID)
+
+	if err != nil {
+		return caps, fmt.Errorf("getting capabilities from role ID: %w", err)
+	}
+	defer rows.Close()
+	for rows.Next() {
+		err = rows.Scan(&cap)
+		if err != nil {
+			return caps, fmt.Errorf("scanning capabilities: %w", err)
+		}
+		caps = append(caps, cap)
+	}
+	return caps, nil
+}
+
+// GetCapabilitiesFromRoleName returns the capabilities for the supplied role name.
+func GetCapabilitiesFromRoleName(tx *sql.Tx, role string) ([]string, error) {
+	var caps []string
+	var cap string
+
+	rows, err := tx.Query(`SELECT cap_name FROM role_capability rc JOIN role r ON r.id = rc.role_id WHERE r.name = $1`, role)
+
+	if err != nil {
+		return caps, fmt.Errorf("getting capabilities from role name: %w", err)
+	}
+	defer rows.Close()
+	for rows.Next() {
+		err = rows.Scan(&cap)
+		if err != nil {
+			return caps, fmt.Errorf("scanning capabilities: %w", err)
+		}
+		caps = append(caps, cap)
+	}
+	return caps, nil
+}
+
 // GetDSNameFromID loads the DeliveryService's xml_id from the database, from the ID. Returns whether the delivery service was found, and any error.
 func GetDSNameFromID(tx *sql.Tx, id int) (tc.DeliveryServiceName, bool, error) {
 	name := tc.DeliveryServiceName("")
@@ -1604,6 +1667,45 @@ func GetDSIDFromStaticDNSEntry(tx *sql.Tx, staticDNSEntryID int) (int, error) {
 	return dsID, nil
 }
 
+// AppendWhere appends 'extra' safely to the WHERE clause 'where'. What is
+// returned is guaranteed to be a valid WHERE clause (including a blank string),
+// provided the supplied 'where' and 'extra' clauses are valid.
+func AppendWhere(where, extra string) string {
+	if where == "" && extra == "" {
+		return ""
+	}
+	if where == "" {
+		where = BaseWhere + " "
+	} else {
+		where += " AND "
+	}
+	return where + extra
+}
+
+// GetRoleIDFromName returns the ID of the role associated with the supplied name.
+func GetRoleIDFromName(tx *sql.Tx, roleName string) (int, bool, error) {
+	var id int
+	if err := tx.QueryRow(`SELECT id FROM role WHERE name = $1`, roleName).Scan(&id); err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return id, false, nil
+		}
+		return id, false, fmt.Errorf("querying role ID from name: %w", err)
+	}
+	return id, true, nil
+}
+
+// GetRoleNameFromID returns the name of the role associated with the supplied ID.
+func GetRoleNameFromID(tx *sql.Tx, roleID int) (string, bool, error) {
+	var name string
+	if err := tx.QueryRow(`SELECT name FROM role WHERE id = $1`, roleID).Scan(&name); err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return name, false, nil
+		}
+		return name, false, fmt.Errorf("querying role name from ID: %w", err)
+	}
+	return name, true, nil
+}
+
 // GetCDNNameDomain returns the name and domain for a given CDN ID.
 func GetCDNNameDomain(cdnID int, tx *sql.Tx) (string, string, error) {
 	q := `SELECT cdn.name, cdn.domain_name from cdn where cdn.id = $1`
diff --git a/traffic_ops/traffic_ops_golang/login/register.go b/traffic_ops/traffic_ops_golang/login/register.go
index dcc7c04..a10d7e4 100644
--- a/traffic_ops/traffic_ops_golang/login/register.go
+++ b/traffic_ops/traffic_ops_golang/login/register.go
@@ -19,7 +19,10 @@ package login
  * under the License.
  */
 
-import "bytes"
+import (
+	"bytes"
+	"encoding/json"
+)
 import "database/sql"
 import "errors"
 import "fmt"
@@ -150,6 +153,11 @@ func createRegistrationMsg(addr rfc.EmailAddress, t string, tx *sql.Tx, c config
 
 // RegisterUser is the handler for /users/register. It sends registration through Email.
 func RegisterUser(w http.ResponseWriter, r *http.Request) {
+	var tenantID uint
+	var req tc.UserRegistrationRequest
+	var reqV4 tc.UserRegistrationRequestV4
+	var email rfc.EmailAddress
+
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
 	var tx = inf.Tx.Tx
 	if userErr != nil || sysErr != nil {
@@ -159,43 +167,74 @@ func RegisterUser(w http.ResponseWriter, r *http.Request) {
 	defer inf.Close()
 	defer r.Body.Close()
 
-	var req tc.UserRegistrationRequest
-	if userErr = api.Parse(r.Body, tx, &req); userErr != nil {
-		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
-		return
+	// ToDo: uncomment this once the perm based roles and config options are implemented
+	if inf.Version.Major >= 4 {
+		if err := json.NewDecoder(r.Body).Decode(&reqV4); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := reqV4.Validate(tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		tenantID = reqV4.TenantID
+		email = reqV4.Email
+	} else {
+		if userErr = api.Parse(r.Body, tx, &req); userErr != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+			return
+		}
+		tenantID = req.TenantID
+		email = req.Email
 	}
 
-	if ok, err := inf.IsResourceAuthorizedToCurrentUser(int(req.TenantID)); err != nil {
-		sysErr = fmt.Errorf("Checking tenancy permissions of current user (%+v) on tenant #%d", inf.User, req.TenantID)
+	if ok, err := inf.IsResourceAuthorizedToCurrentUser(int(tenantID)); err != nil {
+		sysErr = fmt.Errorf("Checking tenancy permissions of current user (%+v) on tenant #%d", inf.User, tenantID)
 		errCode = http.StatusInternalServerError
 		api.HandleErr(w, r, tx, errCode, nil, sysErr)
 		return
 	} else if !ok {
-		sysErr = fmt.Errorf("User %s requested unauthorized access to tenant #%d to register new user", inf.User.UserName, req.TenantID)
+		sysErr = fmt.Errorf("User %s requested unauthorized access to tenant #%d to register new user", inf.User.UserName, tenantID)
 		userErr = errors.New("not authorized on this tenant")
 		errCode = http.StatusForbidden
 		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
 		return
 	}
 
-	privLevel, ok, err := dbhelpers.GetPrivLevelFromRoleID(tx, int(req.Role))
-	if err != nil {
-		sysErr = fmt.Errorf("Checking role #%d privilege level: %v", req.Role, err)
-		errCode = http.StatusInternalServerError
-		api.HandleErr(w, r, tx, errCode, nil, sysErr)
-		return
-	}
-	if !ok {
-		userErr = fmt.Errorf("No such role: %d", req.Role)
-		errCode = http.StatusNotFound
-		api.HandleErr(w, r, tx, errCode, userErr, nil)
-		return
-	}
-	if privLevel > inf.User.PrivLevel {
-		userErr = errors.New("Cannot register a user with a role with higher privileges than yourself")
-		errCode = http.StatusForbidden
-		api.HandleErr(w, r, tx, errCode, userErr, nil)
-		return
+	// ToDo: Add checks for permission based role checking here, if the version is >=5 and the config option is turned on.
+	if inf.Version.Major < 4 {
+		privLevel, ok, err := dbhelpers.GetPrivLevelFromRoleID(tx, int(req.Role))
+		if err != nil {
+			sysErr = fmt.Errorf("checking role #%d privilege level: %w", req.Role, err)
+			errCode = http.StatusInternalServerError
+			api.HandleErr(w, r, tx, errCode, nil, sysErr)
+			return
+		}
+		if !ok {
+			userErr = fmt.Errorf("No such role: %d", req.Role)
+			errCode = http.StatusNotFound
+			api.HandleErr(w, r, tx, errCode, userErr, nil)
+			return
+		}
+		if privLevel > inf.User.PrivLevel {
+			userErr = errors.New("Cannot register a user with a role with higher privileges than yourself")
+			errCode = http.StatusForbidden
+			api.HandleErr(w, r, tx, errCode, userErr, nil)
+			return
+		}
+	} else {
+		req.Email = reqV4.Email
+		req.TenantID = reqV4.TenantID
+		dbhelpers.GetRoleIDFromName(tx, reqV4.Role)
+		roleID, ok, err := dbhelpers.GetRoleIDFromName(tx, reqV4.Role)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error fetching ID from role name: %w", err))
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+		req.Role = uint(roleID)
 	}
 
 	t, err := generateToken()
@@ -208,10 +247,10 @@ func RegisterUser(w http.ResponseWriter, r *http.Request) {
 
 	var role string
 	var tenant string
-	user, exists, err := dbhelpers.GetUserByEmail(req.Email.Address.Address, inf.Tx.Tx)
+	user, exists, err := dbhelpers.GetUserByEmail(email.Address.Address, inf.Tx.Tx)
 	if err != nil {
 		errCode = http.StatusInternalServerError
-		sysErr = fmt.Errorf("Checking for existing user with email %s: %v", req.Email, err)
+		sysErr = fmt.Errorf("Checking for existing user with email %s: %v", email, err)
 		api.HandleErr(w, r, tx, errCode, nil, sysErr)
 		return
 	}
@@ -235,7 +274,7 @@ func RegisterUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	msg, err := createRegistrationMsg(req.Email, t, tx, inf.Config.ConfigPortal)
+	msg, err := createRegistrationMsg(email, t, tx, inf.Config.ConfigPortal)
 	if err != nil {
 		sysErr = fmt.Errorf("Failed to create email message: %v", err)
 		errCode = http.StatusInternalServerError
@@ -243,19 +282,19 @@ func RegisterUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	log.Debugf("Sending registration email to %s", req.Email)
+	log.Debugf("Sending registration email to %s", email)
 
-	if errCode, userErr, sysErr = inf.SendMail(req.Email, msg); userErr != nil || sysErr != nil {
+	if errCode, userErr, sysErr = inf.SendMail(email, msg); userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
 		return
 	}
 
 	var alert = "Sent user registration to %s with the following permissions [ role: %s | tenant: %s ]"
-	alert = fmt.Sprintf(alert, req.Email, role, tenant)
+	alert = fmt.Sprintf(alert, email, role, tenant)
 	api.WriteRespAlert(w, r, tc.SuccessLevel, alert)
 
 	var changeLog = "USER: %s, EMAIL: %s, ACTION: registration sent with role %s and tenant %s"
-	changeLog = fmt.Sprintf(changeLog, req.Email, req.Email, role, tenant)
+	changeLog = fmt.Sprintf(changeLog, email, email, role, tenant)
 	api.CreateChangeLogRawTx(api.ApiChange, changeLog, inf.User, tx)
 }
 
diff --git a/traffic_ops/traffic_ops_golang/role/roles.go b/traffic_ops/traffic_ops_golang/role/roles.go
index 5b71274..561f7b3 100644
--- a/traffic_ops/traffic_ops_golang/role/roles.go
+++ b/traffic_ops/traffic_ops_golang/role/roles.go
@@ -20,10 +20,12 @@ package role
  */
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
@@ -32,6 +34,7 @@ import (
 	"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"
 	"github.com/jmoiron/sqlx"
@@ -45,6 +48,21 @@ type TORole struct {
 	PQCapabilities *pq.StringArray `json:"-" db:"capabilities"`
 }
 
+func updateLegacyRoleQuery() string {
+	return `UPDATE
+role SET
+name=$1,
+description=$2
+WHERE id=$3 RETURNING last_updated`
+}
+
+func updateRoleQuery() string {
+	return `UPDATE
+role SET
+description=$1
+WHERE name=$2 RETURNING last_updated`
+}
+
 func (v *TORole) GetLastUpdated() (*time.Time, bool, error) {
 	return api.GetLastUpdated(v.APIInfo().Tx, *v.ID, "role")
 }
@@ -127,7 +145,11 @@ func (role *TORole) Create() (error, error, int) {
 	if *role.PrivLevel > role.ReqInfo.User.PrivLevel {
 		return errors.New("can not create a role with a higher priv level than your own"), nil, http.StatusBadRequest
 	}
-
+	caps := *role.Capabilities
+	missing := role.ReqInfo.User.MissingPermissions(caps...)
+	if len(missing) != 0 {
+		return fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil, http.StatusForbidden
+	}
 	userErr, sysErr, errCode := api.GenericCreate(role)
 	if userErr != nil || sysErr != nil {
 		return userErr, sysErr, errCode
@@ -158,7 +180,7 @@ func (role *TORole) createRoleCapabilityAssociations(tx *sqlx.Tx) (error, error,
 }
 
 func (role *TORole) deleteRoleCapabilityAssociations(tx *sqlx.Tx) (error, error, int) {
-	result, err := tx.Exec(deleteAssociatedCapabilities(), role.ID)
+	result, err := tx.Exec(deleteAssociatedCapabilities(), role.Name)
 	if err != nil {
 		return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError
 	}
@@ -194,6 +216,11 @@ func (role *TORole) Update(h http.Header) (error, error, int) {
 	if *role.PrivLevel > role.ReqInfo.User.PrivLevel {
 		return errors.New("can not create a role with a higher priv level than your own"), nil, http.StatusForbidden
 	}
+	caps := *role.Capabilities
+	missing := role.ReqInfo.User.MissingPermissions(caps...)
+	if len(missing) != 0 {
+		return fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil, http.StatusForbidden
+	}
 	userErr, sysErr, errCode := api.GenericUpdate(h, role)
 	if userErr != nil || sysErr != nil {
 		return userErr, sysErr, errCode
@@ -245,7 +272,7 @@ WHERE id=:id RETURNING last_updated`
 
 func deleteAssociatedCapabilities() string {
 	return `DELETE FROM role_capability
-WHERE role_id=$1`
+WHERE role_id=(SELECT id from role r WHERE r.name=$1)`
 }
 
 func associateCapabilities() string {
@@ -273,3 +300,329 @@ RETURNING id, last_updated`
 func deleteQuery() string {
 	return `DELETE FROM role WHERE id = :id`
 }
+
+// Update will modify the role identified by the role name.
+func Update(w http.ResponseWriter, r *http.Request) {
+	var roleID int
+	var roleName string
+	var roleDesc string
+	var roleCapabilities []string
+	var roleV4 tc.RoleV4
+	var ok bool
+	var err error
+
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	tx := inf.Tx.Tx
+	if err := json.NewDecoder(r.Body).Decode(&roleV4); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	if err := roleV4.Validate(); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	missing := inf.User.MissingPermissions(roleV4.Permissions...)
+	if len(missing) != 0 {
+		api.HandleErr(w, r, tx, http.StatusForbidden, fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil)
+		return
+	}
+	roleDesc = roleV4.Description
+	roleCapabilities = roleV4.Permissions
+	roleName = inf.Params["name"]
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdatedByName(inf.Tx, roleName, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	if !api.IsUnmodified(r.Header, *existingLastUpdated) {
+		api.HandleErr(w, r, tx, http.StatusPreconditionFailed, api.ResourceModifiedError, nil)
+		return
+	}
+	rows, err := tx.Query(updateRoleQuery(), roleDesc, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("updating role: %w", err))
+		return
+	}
+	defer rows.Close()
+	if !rows.Next() {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+	var lastUpdated time.Time
+	for rows.Next() {
+		if err := rows.Scan(&lastUpdated); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning lastUpdated from role update: %w", err))
+			return
+		}
+		roleV4.LastUpdated = &lastUpdated
+	}
+
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleName)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	userErr, sysErr, errCode = createRoleCapabilityAssociations(inf.Tx, roleID, &roleCapabilities)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was updated.")
+	var roleResponse interface{}
+	capabilities := roleCapabilities
+	roleResponse = tc.RoleV4{
+		Name:        roleName,
+		Permissions: capabilities,
+		Description: roleDesc,
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, alerts, roleResponse)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Updated Role", roleName, roleID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func deleteRoleQuery() string {
+	return `DELETE FROM role WHERE name = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+name,
+description,
+last_updated,
+ARRAY(SELECT rc.cap_name FROM role_capability AS rc WHERE rc.role_id=id) AS permissions
+FROM role`
+}
+
+func createQuery() string {
+	return `INSERT INTO role (
+name,
+description,
+priv_level
+) VALUES (
+$1,
+$2,
+$3
+)
+RETURNING id, last_updated`
+}
+
+func createRoleCapabilityAssociations(tx *sqlx.Tx, roleID int, permissions *[]string) (error, error, int) {
+	result, err := tx.Exec(associateCapabilities(), roleID, pq.Array(permissions))
+	if err != nil {
+		return nil, fmt.Errorf("creating role capabilities: %w", err), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %w", err), http.StatusInternalServerError
+	} else if expected := int64(len(*permissions)); rows != expected {
+		return nil, fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), http.StatusInternalServerError
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleName string) (error, error, int) {
+	_, err := tx.Exec(deleteAssociatedCapabilities(), roleName)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	return nil, nil, http.StatusOK
+}
+
+// Delete will delete the role identified by the role name.
+func Delete(w http.ResponseWriter, r *http.Request) {
+	var roleName string
+	var ok bool
+	var err error
+	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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role name to delete"), nil)
+		return
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role= (SELECT id FROM role r WHERE r.name=$1)", roleName); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("role delete counting assigned users: %w", err))
+		return
+	} else if assignedUsers != 0 {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("can not delete a role with %d assigned users", assignedUsers), nil)
+		return
+	}
+
+	rows, err := tx.Query(deleteRoleQuery(), roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("deleting role: %w", err))
+		return
+	}
+	rows.Close()
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ACTION: Deleted Role", roleName)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+// Create will create a new role based on the struct supplied.
+func Create(w http.ResponseWriter, r *http.Request) {
+	var roleID int
+	var roleName string
+	var roleDesc string
+	var privLevel int
+	var roleCapabilities []string
+	var lastUpdated time.Time
+	var roleV4 tc.RoleV4
+
+	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
+	if err := json.NewDecoder(r.Body).Decode(&roleV4); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	if err := roleV4.Validate(); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	missing := inf.User.MissingPermissions(roleV4.Permissions...)
+	if len(missing) != 0 {
+		api.HandleErr(w, r, tx, http.StatusForbidden, fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil)
+		return
+	}
+	roleName = roleV4.Name
+	roleDesc = roleV4.Description
+	privLevel = inf.User.PrivLevel
+	roleCapabilities = roleV4.Permissions
+
+	rows, err := tx.Query(createQuery(), roleName, roleDesc, privLevel)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("creating role: %w", err))
+		return
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		if err := rows.Scan(&roleID, &lastUpdated); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("role create: scanning role ID: %w", err))
+			return
+		}
+	}
+
+	if len(roleCapabilities) > 0 {
+		userErr, sysErr, errCode = createRoleCapabilityAssociations(inf.Tx, roleID, &roleCapabilities)
+		if userErr != nil || sysErr != nil {
+			api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+			return
+		}
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was created.")
+	var roleResponse interface{}
+	capabilities := roleCapabilities
+	roleResponse = tc.RoleV4{
+		Name:        roleName,
+		Permissions: capabilities,
+		Description: roleDesc,
+		LastUpdated: &lastUpdated,
+	}
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, roleResponse)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Created Role", roleName, roleID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+// Get will read the roles and return them to the user.
+func Get(w http.ResponseWriter, r *http.Request) {
+	var maxTime time.Time
+	var runSecond bool
+	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
+	params := make(map[string]dbhelpers.WhereColumnInfo, 1)
+	params["name"] = dbhelpers.WhereColumnInfo{Column: "name"}
+
+	if _, ok := inf.Params["orderby"]; !ok {
+		inf.Params["orderby"] = "name"
+	}
+	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, params)
+	if len(errs) != 0 {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, util.JoinErrs(errs), nil)
+		return
+	}
+	if perm, ok := inf.Params["can"]; ok {
+		queryValues["can"] = perm
+		where = dbhelpers.AppendWhere(where, "permissions @> :can")
+	}
+	if inf.Config.UseIMS {
+		runSecond, maxTime = ims.TryIfModifiedSinceQuery(inf.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 := readQuery() + where + orderBy + pagination
+
+	rows, err := inf.Tx.NamedQuery(query, queryValues)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("querying Roles: %w", err))
+		return
+	}
+	defer log.Close(rows, "reading in Roles from the database")
+
+	var roleV4 tc.RoleV4
+	rolesV4 := []tc.RoleV4{}
+
+	for rows.Next() {
+		if err = rows.Scan(&roleV4.Name, &roleV4.Description, &roleV4.LastUpdated, pq.Array(&roleV4.Permissions)); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning RoleV4 row: %w", err))
+			return
+		}
+		rolesV4 = append(rolesV4, roleV4)
+	}
+	api.WriteResp(w, r, rolesV4)
+}
+
+func selectMaxLastUpdatedQuery(where string) string {
+	return `SELECT max(t) FROM (
+		SELECT max(r.last_updated) AS t FROM role r ` + where + ` UNION ALL
+		SELECT max(l.last_updated) AS t FROM last_deleted l WHERE l.table_name='role' OR l.table_name='role_capability' UNION ALL
+		SELECT max(rc.last_updated) AS t FROM role_capability rc INNER JOIN role ON rc.role_id = role.id)
+		AS res`
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 365f2f1..62cbb55 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -126,7 +126,6 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		// NOTE: Route IDs are immutable and unique. DO NOT change the ID of an existing Route; otherwise, existing
 		// configurations may break. New Route IDs can be any integer between 0 and 2147483647 (inclusive), as long as
 		// it's unique.
-
 		/**
 		 * 4.x API
 		 */
@@ -238,10 +237,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		{api.Version{Major: 4, Minor: 0}, http.MethodPost, `isos/?$`, iso.ISOs, auth.PrivLevelOperations, nil, Authenticated, nil, 4760336573},
 
 		//User: CRUD
-		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `users/?$`, api.ReadHandler(&user.TOUser{}), auth.PrivLevelReadOnly, nil, Authenticated, nil, 44919299003},
-		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `users/{id}$`, api.ReadHandler(&user.TOUser{}), auth.PrivLevelReadOnly, nil, Authenticated, nil, 4138099803},
-		{api.Version{Major: 4, Minor: 0}, http.MethodPut, `users/{id}$`, api.UpdateHandler(&user.TOUser{}), auth.PrivLevelOperations, nil, Authenticated, nil, 4354334043},
-		{api.Version{Major: 4, Minor: 0}, http.MethodPost, `users/?$`, api.CreateHandler(&user.TOUser{}), auth.PrivLevelOperations, nil, Authenticated, nil, 4762448163},
+		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `users/?$`, user.Get, auth.PrivLevelReadOnly, nil, Authenticated, nil, 44919299003},
+		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `users/{id}$`, user.Get, auth.PrivLevelReadOnly, nil, Authenticated, nil, 4138099803},
+		{api.Version{Major: 4, Minor: 0}, http.MethodPut, `users/{id}$`, user.Update, auth.PrivLevelOperations, nil, Authenticated, nil, 4354334043},
+		{api.Version{Major: 4, Minor: 0}, http.MethodPost, `users/?$`, user.Create, auth.PrivLevelOperations, nil, Authenticated, nil, 4762448163},
 
 		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `user/current/?$`, user.Current, auth.PrivLevelReadOnly, nil, Authenticated, nil, 46107016143},
 		{api.Version{Major: 4, Minor: 0}, http.MethodPut, `user/current/?$`, user.ReplaceCurrent, auth.PrivLevelReadOnly, nil, Authenticated, nil, 4203},
@@ -412,10 +411,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		{api.Version{Major: 4, Minor: 0}, http.MethodDelete, `origins/?$`, api.DeleteHandler(&origin.TOOrigin{}), auth.PrivLevelOperations, nil, Authenticated, nil, 4602732633},
 
 		//Roles
-		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `roles/?$`, api.ReadHandler(&role.TORole{}), auth.PrivLevelReadOnly, nil, Authenticated, nil, 4870885833},
-		{api.Version{Major: 4, Minor: 0}, http.MethodPut, `roles/?$`, api.UpdateHandler(&role.TORole{}), auth.PrivLevelAdmin, nil, Authenticated, nil, 46128974893},
-		{api.Version{Major: 4, Minor: 0}, http.MethodPost, `roles/?$`, api.CreateHandler(&role.TORole{}), auth.PrivLevelAdmin, nil, Authenticated, nil, 4306524063},
-		{api.Version{Major: 4, Minor: 0}, http.MethodDelete, `roles/?$`, api.DeleteHandler(&role.TORole{}), auth.PrivLevelAdmin, nil, Authenticated, nil, 43567059823},
+		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `roles/?$`, role.Get, auth.PrivLevelReadOnly, nil, Authenticated, nil, 4870885833},
+		{api.Version{Major: 4, Minor: 0}, http.MethodPut, `roles/?$`, role.Update, auth.PrivLevelAdmin, nil, Authenticated, nil, 46128974893},
+		{api.Version{Major: 4, Minor: 0}, http.MethodPost, `roles/?$`, role.Create, auth.PrivLevelAdmin, nil, Authenticated, nil, 4306524063},
+		{api.Version{Major: 4, Minor: 0}, http.MethodDelete, `roles/?$`, role.Delete, auth.PrivLevelAdmin, nil, Authenticated, nil, 43567059823},
 
 		//Delivery Services Regexes
 		{api.Version{Major: 4, Minor: 0}, http.MethodGet, `deliveryservices_regexes/?$`, deliveryservicesregexes.Get, auth.PrivLevelReadOnly, nil, Authenticated, nil, 4055014533},
diff --git a/traffic_ops/traffic_ops_golang/user/current.go b/traffic_ops/traffic_ops_golang/user/current.go
index d12abe0..ea6e1ea 100644
--- a/traffic_ops/traffic_ops_golang/user/current.go
+++ b/traffic_ops/traffic_ops_golang/user/current.go
@@ -106,7 +106,7 @@ func Current(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	currentUser, err := getUser(inf.Tx.Tx, inf.User.ID)
+	currentUser, role, err := getUser(inf.Tx.Tx, inf.User.ID)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting current user: "+err.Error()))
 		return
@@ -120,11 +120,13 @@ func Current(w http.ResponseWriter, r *http.Request) {
 	if version.Major >= 4 {
 		api.WriteResp(w, r, currentUser)
 	} else {
-		api.WriteResp(w, r, currentUser.UserCurrent)
+		legacyUser := currentUser.Downgrade()
+		legacyUser.Role = &role
+		api.WriteResp(w, r, legacyUser)
 	}
 }
 
-func getUser(tx *sql.Tx, id int) (tc.UserCurrentV40, error) {
+func getUser(tx *sql.Tx, id int) (tc.UserCurrentV4, int, error) {
 	q := `
 SELECT
 u.address_line1,
@@ -153,16 +155,19 @@ LEFT JOIN role as r ON r.id = u.role
 INNER JOIN tenant as t ON t.id = u.tenant_id
 WHERE u.id=$1
 `
-	u := tc.UserCurrentV40{}
+	u := tc.UserCurrentV4{}
 	localPassword := sql.NullString{}
-	if err := tx.QueryRow(q, id).Scan(&u.AddressLine1, &u.AddressLine2, &u.City, &u.Company, &u.Country, &u.Email, &u.FullName, &u.ID, &u.LastUpdated, &u.LastAuthenticated, &localPassword, &u.NewUser, &u.PhoneNumber, &u.PostalCode, &u.PublicSSHKey, &u.Role, &u.RoleName, &u.StateOrProvince, &u.Tenant, &u.TenantID, &u.UserName); err != nil {
-		return tc.UserCurrentV40{}, errors.New("querying current user: " + err.Error())
+	var role int
+	if err := tx.QueryRow(q, id).Scan(&u.AddressLine1, &u.AddressLine2, &u.City, &u.Company, &u.Country, &u.Email, &u.FullName, &u.ID, &u.LastUpdated, &u.LastAuthenticated, &localPassword, &u.NewUser, &u.PhoneNumber, &u.PostalCode, &u.PublicSSHKey, &role, &u.Role, &u.StateOrProvince, &u.Tenant, &u.TenantID, &u.UserName); err != nil {
+		return tc.UserCurrentV4{}, role, errors.New("querying current user: " + err.Error())
 	}
 	u.LocalUser = util.BoolPtr(localPassword.Valid)
-	return u, nil
+	return u, role, nil
 }
 
 func ReplaceCurrent(w http.ResponseWriter, r *http.Request) {
+	var useV4User bool
+	var userV4 tc.UserV4
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
 	tx := inf.Tx.Tx
 	if userErr != nil || sysErr != nil {
@@ -174,28 +179,33 @@ func ReplaceCurrent(w http.ResponseWriter, r *http.Request) {
 	var userRequest tc.CurrentUserUpdateRequest
 	if err := json.NewDecoder(r.Body).Decode(&userRequest); err != nil {
 		errCode = http.StatusBadRequest
-		userErr = fmt.Errorf("Couldn't parse request: %v", err)
+		userErr = fmt.Errorf("couldn't parse request: %v", err)
 		api.HandleErr(w, r, tx, errCode, userErr, nil)
 		return
 	}
-
+	if inf.Version.Major >= 4 {
+		useV4User = true
+	}
 	user, exists, err := dbhelpers.GetUserByID(inf.User.ID, tx)
+	if useV4User {
+		userV4 = user.Upgrade()
+	}
 	if err != nil {
-		sysErr = fmt.Errorf("Getting user by ID %d: %v", inf.User.ID, err)
+		sysErr = fmt.Errorf("getting user by ID %d: %v", inf.User.ID, err)
 		errCode = http.StatusInternalServerError
 		api.HandleErr(w, r, tx, errCode, nil, sysErr)
 		return
 	}
 	if !exists {
-		sysErr = fmt.Errorf("Current user (#%d) doesn't exist... ??", inf.User.ID)
+		sysErr = fmt.Errorf("current user (#%d) doesn't exist... ??", inf.User.ID)
 		errCode = http.StatusInternalServerError
 		api.HandleErr(w, r, tx, errCode, nil, sysErr)
 		return
 	}
 
-	if err := userRequest.User.UnmarshalAndValidate(&user); err != nil {
+	if err := userRequest.User.UnmarshalAndValidate(&user, useV4User); err != nil {
 		errCode = http.StatusBadRequest
-		userErr = fmt.Errorf("Couldn't parse request: %v", err)
+		userErr = fmt.Errorf("couldn't parse request: %v", err)
 		api.HandleErr(w, r, tx, errCode, userErr, nil)
 		return
 	}
@@ -241,7 +251,7 @@ func ReplaceCurrent(w http.ResponseWriter, r *http.Request) {
 		changeConfirmPasswd = true
 	}
 
-	if *user.Role != inf.User.Role {
+	if *user.Role != inf.User.Role && !useV4User {
 		privLevel, exists, err := dbhelpers.GetPrivLevelFromRoleID(tx, *user.Role)
 		if err != nil {
 			sysErr = fmt.Errorf("Getting privLevel for Role #%d: %v", *user.Role, err)
@@ -301,12 +311,16 @@ func ReplaceCurrent(w http.ResponseWriter, r *http.Request) {
 
 	if err = updateUser(&user, tx, changePasswd, changeConfirmPasswd); err != nil {
 		errCode = http.StatusInternalServerError
-		sysErr = fmt.Errorf("Updating user: %v", err)
+		sysErr = fmt.Errorf("updating user: %v", err)
 		api.HandleErr(w, r, tx, errCode, nil, sysErr)
 		return
 	}
 
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "User profile was successfully updated", user)
+	if useV4User {
+		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "User profile was successfully updated", userV4)
+	} else {
+		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "User profile was successfully updated", user)
+	}
 }
 
 func updateUser(u *tc.User, tx *sql.Tx, changePassword bool, changeConfirmPasswd bool) error {
diff --git a/traffic_ops/traffic_ops_golang/user/user.go b/traffic_ops/traffic_ops_golang/user/user.go
index e3fd5f4..746c48e 100644
--- a/traffic_ops/traffic_ops_golang/user/user.go
+++ b/traffic_ops/traffic_ops_golang/user/user.go
@@ -21,12 +21,16 @@ package user
 
 import (
 	"database/sql"
+	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
 	"github.com/apache/trafficcontrol/lib/go-util"
@@ -38,6 +42,7 @@ import (
 
 	"github.com/go-ozzo/ozzo-validation"
 	"github.com/go-ozzo/ozzo-validation/is"
+	"github.com/jmoiron/sqlx"
 )
 
 type TOUser struct {
@@ -120,6 +125,13 @@ func (user *TOUser) postValidate() error {
 	return util.JoinErrs(tovalidate.ToErrors(validateErrs))
 }
 
+func postValidateV40(user tc.UserV4) error {
+	validateErrs := validation.Errors{
+		"localPasswd": validation.Validate(user.LocalPassword, validation.Required),
+	}
+	return util.JoinErrs(tovalidate.ToErrors(validateErrs))
+}
+
 // Note: Not using GenericCreate because Scan also needs to scan tenant and rolename
 func (user *TOUser) Create() (error, error, int) {
 
@@ -133,7 +145,19 @@ func (user *TOUser) Create() (error, error, int) {
 	if usrErr, sysErr, code := user.privCheck(); code != http.StatusOK {
 		return usrErr, sysErr, code
 	}
-
+	var caps []string
+	if user.Role != nil {
+		caps, err = dbhelpers.GetCapabilitiesFromRoleID(user.ReqInfo.Tx.Tx, *user.Role)
+	} else if user.RoleName != nil {
+		caps, err = dbhelpers.GetCapabilitiesFromRoleName(user.ReqInfo.Tx.Tx, *user.RoleName)
+	}
+	if err != nil {
+		return nil, err, http.StatusInternalServerError
+	}
+	missing := user.ReqInfo.User.MissingPermissions(caps...)
+	if len(missing) != 0 {
+		return fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil, http.StatusForbidden
+	}
 	// Convert password to SCRYPT
 	*user.LocalPassword, err = auth.DerivePassword(*user.LocalPassword)
 	if err != nil {
@@ -189,7 +213,7 @@ func (this *TOUser) Read(h http.Header, useIMS bool) ([]interface{}, error, erro
 
 	tenantIDs, err := tenant.GetUserTenantIDListTx(inf.Tx.Tx, inf.User.TenantID)
 	if err != nil {
-		return nil, nil, fmt.Errorf("getting tenant list for user: %v\n", err), http.StatusInternalServerError, nil
+		return nil, nil, fmt.Errorf("getting tenant list for user: %w", err), http.StatusInternalServerError, nil
 	}
 	where, queryValues = dbhelpers.AddTenancyCheck(where, queryValues, "u.tenant_id", tenantIDs)
 
@@ -219,7 +243,7 @@ func (this *TOUser) Read(h http.Header, useIMS bool) ([]interface{}, error, erro
 
 	rows, err := inf.Tx.NamedQuery(query, queryValues)
 	if err != nil {
-		return nil, nil, fmt.Errorf("querying users : %v", err), http.StatusInternalServerError, nil
+		return nil, nil, fmt.Errorf("querying users : %w", err), http.StatusInternalServerError, nil
 	}
 	defer rows.Close()
 
@@ -239,12 +263,12 @@ func (this *TOUser) Read(h http.Header, useIMS bool) ([]interface{}, error, erro
 	for rows.Next() {
 		if version.Major >= 4 {
 			if err = rows.StructScan(user40); err != nil {
-				return nil, nil, fmt.Errorf("parsing user rows: %v", err), http.StatusInternalServerError, nil
+				return nil, nil, fmt.Errorf("parsing user rows: %w", err), http.StatusInternalServerError, nil
 			}
 			users = append(users, *user40)
 		} else {
 			if err = rows.StructScan(user); err != nil {
-				return nil, nil, fmt.Errorf("parsing user rows: %v", err), http.StatusInternalServerError, nil
+				return nil, nil, fmt.Errorf("parsing user rows: %w", err), http.StatusInternalServerError, nil
 			}
 			users = append(users, *user)
 		}
@@ -263,7 +287,13 @@ func selectMaxLastUpdatedQuery(where string) string {
 }
 
 func (user *TOUser) privCheck() (error, error, int) {
-	requestedPrivLevel, _, err := dbhelpers.GetPrivLevelFromRoleID(user.ReqInfo.Tx.Tx, *user.Role)
+	var requestedPrivLevel int
+	var err error
+	if user.Role == nil {
+		requestedPrivLevel, _, err = dbhelpers.GetPrivLevelFromRole(user.ReqInfo.Tx.Tx, *user.RoleName)
+	} else {
+		requestedPrivLevel, _, err = dbhelpers.GetPrivLevelFromRoleID(user.ReqInfo.Tx.Tx, *user.Role)
+	}
 	if err != nil {
 		return nil, err, http.StatusInternalServerError
 	}
@@ -287,6 +317,21 @@ func (user *TOUser) Update(h http.Header) (error, error, int) {
 		return usrErr, sysErr, code
 	}
 
+	var caps []string
+	var err error
+	if user.Role != nil {
+		caps, err = dbhelpers.GetCapabilitiesFromRoleID(user.ReqInfo.Tx.Tx, *user.Role)
+	} else if user.RoleName != nil {
+		caps, err = dbhelpers.GetCapabilitiesFromRoleName(user.ReqInfo.Tx.Tx, *user.RoleName)
+	}
+	if err != nil {
+		return nil, err, http.StatusInternalServerError
+	}
+	missing := user.ReqInfo.User.MissingPermissions(caps...)
+	if len(missing) != 0 {
+		return fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil, http.StatusForbidden
+	}
+
 	if user.LocalPassword != nil {
 		var err error
 		*user.LocalPassword, err = auth.DerivePassword(*user.LocalPassword)
@@ -464,7 +509,32 @@ func (user *TOUser) UpdateQuery() string {
 	 (SELECT r.name FROM role r WHERE id = u.role)`
 }
 
-func (user *TOUser) InsertQuery() string {
+func UpdateQueryV40() string {
+	return `
+	UPDATE tm_user u SET
+	username=:username,
+	public_ssh_key=:public_ssh_key,
+	role=(SELECT id FROM role WHERE role.name = :role),
+	company=:company,
+	email=:email,
+	full_name=:full_name,
+	new_user=COALESCE(:new_user, FALSE),
+	address_line1=:address_line1,
+	address_line2=:address_line2,
+	city=:city,
+	state_or_province=:state_or_province,
+	phone_number=:phone_number,
+	postal_code=:postal_code,
+	country=:country,
+	tenant_id=:tenant_id,
+	local_passwd=COALESCE(:local_passwd, local_passwd)
+	WHERE id=:id
+	RETURNING last_updated,
+	 (SELECT t.name FROM tenant t WHERE id = u.tenant_id),
+	 (SELECT r.name FROM role r WHERE id = u.role)`
+}
+
+func InsertQueryV40() string {
 	return `
 	INSERT INTO tm_user (
 	username,
@@ -486,7 +556,7 @@ func (user *TOUser) InsertQuery() string {
 	) VALUES (
 	:username,
 	:public_ssh_key,
-	:role,
+	(SELECT id FROM role WHERE name = :role),
 	:company,
 	:email,
 	:full_name,
@@ -508,3 +578,512 @@ func (user *TOUser) InsertQuery() string {
 func (user *TOUser) DeleteQuery() string {
 	return `DELETE FROM tm_user WHERE id = :id`
 }
+
+const readBaseQuery = `
+SELECT
+	u.id,
+	u.username AS username,
+	u.public_ssh_key,
+	u.company,
+	u.email,
+	u.full_name,
+	u.new_user,
+	u.address_line1,
+	u.address_line2,
+	u.city,
+	u.state_or_province,
+	u.phone_number,
+	u.postal_code,
+	u.country,
+	u.registration_sent,
+	u.tenant_id,
+	t.name AS tenant,
+	u.last_updated,`
+
+const readQuery = readBaseQuery + `
+u.last_authenticated,
+(SELECT count(l.tm_user) FROM log as l WHERE l.tm_user = u.id) as change_log_count,
+r.name as role
+FROM tm_user u
+LEFT JOIN tenant t ON u.tenant_id = t.id
+LEFT JOIN role r ON u.role = r.id
+LEFT JOIN role_capability rc on rc.role_id = r.id
+`
+
+const legacyReadQuery = readBaseQuery + `
+	r.name AS rolename,
+	u.role
+FROM tm_user u
+LEFT JOIN tenant t ON u.tenant_id = t.id
+LEFT JOIN role r ON u.role = r.id
+`
+
+// this is necessary because tc.User doesn't read its RoleName field in sql
+// driver scans.
+type userGet struct {
+	RoleName *string `json:"rolename" db:"rolename"`
+	tc.User
+}
+
+type userGet40 struct {
+	userGet
+	ChangeLogCount    *int       `json:"changeLogCount" db:"change_log_count"`
+	LastAuthenticated *time.Time `json:"lastAuthenticated" db:"last_authenticated"`
+}
+
+func read(rows *sqlx.Rows) ([]tc.UserV4, error) {
+	if rows == nil {
+		return nil, errors.New("cannot read from nil rows")
+	}
+
+	users := []tc.UserV4{}
+	for rows.Next() {
+		var user tc.UserV4
+		if err := rows.StructScan(&user); err != nil {
+			return nil, fmt.Errorf("scanning UserV4 row: %w", err)
+		}
+		users = append(users, user)
+	}
+
+	return users, nil
+}
+
+func getMaxLastUpdated(where string, queryValues map[string]interface{}, tx *sqlx.Tx) (time.Time, error) {
+	query := selectMaxLastUpdatedQuery(where)
+	var t time.Time
+	rows, err := tx.NamedQuery(query, queryValues)
+	if err != nil {
+		return t, fmt.Errorf("query for max user last updated time: %w", err)
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		if err = rows.Scan(&t); err != nil {
+			return t, fmt.Errorf("scanning user max last updated time: %w", err)
+		}
+	}
+	return t, nil
+}
+
+// Get is the handler for GET requests made to /users.
+func Get(w http.ResponseWriter, r *http.Request) {
+	var query string
+	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()
+
+	api.DefaultSort(inf, "username")
+	params := map[string]dbhelpers.WhereColumnInfo{
+		"id":       {Column: "u.id", Checker: api.IsInt},
+		"role":     {Column: "r.name"},
+		"tenant":   {Column: "t.name"},
+		"username": {Column: "u.username"},
+	}
+	params["company"] = dbhelpers.WhereColumnInfo{Column: "u.company"}
+	params["email"] = dbhelpers.WhereColumnInfo{Column: "u.email"}
+	params["fullName"] = dbhelpers.WhereColumnInfo{Column: "u.full_name"}
+	params["newUser"] = dbhelpers.WhereColumnInfo{Column: "u.new_user"}
+	params["city"] = dbhelpers.WhereColumnInfo{Column: "u.city"}
+	params["stateOrProvince"] = dbhelpers.WhereColumnInfo{Column: "u.state_or_province"}
+	params["country"] = dbhelpers.WhereColumnInfo{Column: "u.country"}
+	params["postalCode"] = dbhelpers.WhereColumnInfo{Column: "u.postal_code"}
+	params["capability"] = dbhelpers.WhereColumnInfo{Column: "rc.cap_name"}
+	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, params)
+	if len(errs) != 0 {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, util.JoinErrs(errs), nil)
+		return
+	}
+
+	tenantIDs, err := tenant.GetUserTenantIDListTx(inf.Tx.Tx, inf.User.TenantID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("getting tenant list for user: %w", err))
+		return
+	}
+	where, queryValues = dbhelpers.AddTenancyCheck(where, queryValues, "u.tenant_id", tenantIDs)
+
+	if inf.Config.UseIMS {
+		runSecond, maxTime := ims.TryIfModifiedSinceQuery(inf.Tx, r.Header, queryValues, selectMaxLastUpdatedQuery(where))
+		if !runSecond {
+			log.Debugln("IMS HIT")
+			w.Header().Add(rfc.LastModified, maxTime.Format(rfc.LastModifiedFormat))
+			w.WriteHeader(http.StatusNotModified)
+			return
+		}
+		log.Debugln("IMS MISS")
+	} else {
+		log.Debugln("Non IMS request")
+	}
+
+	groupBy := "\n" + `GROUP BY u.id, r.name, t.name`
+	orderBy = groupBy + orderBy
+
+	query = readQuery + where + orderBy + pagination
+
+	rows, err := inf.Tx.NamedQuery(query, queryValues)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("querying Users: %w", err))
+		return
+	}
+	defer log.Close(rows, "reading in Users from the database")
+
+	var response interface{}
+	response, err = read(rows)
+
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	if inf.UseIMS() {
+		maxTime, err := getMaxLastUpdated(where, queryValues, inf.Tx)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+			return
+		}
+		w.Header().Add(rfc.LastModified, maxTime.Format(rfc.LastModifiedFormat))
+	}
+	api.WriteResp(w, r, response)
+}
+
+func validate(user TOUser) error {
+	validateErrs := validation.Errors{
+		"email":    validation.Validate(user.Email, validation.Required, is.Email),
+		"fullName": validation.Validate(user.FullName, validation.Required),
+		"role":     validation.Validate(user.Role, validation.Required),
+		"username": validation.Validate(user.Username, validation.Required),
+		"tenantID": validation.Validate(user.TenantID, validation.Required),
+	}
+
+	// Password is not required for update
+	if user.LocalPassword != nil {
+		_, err := auth.IsGoodLoginPair(*user.Username, *user.LocalPassword)
+		if err != nil {
+			return err
+		}
+	}
+
+	return util.JoinErrs(tovalidate.ToErrors(validateErrs))
+}
+
+func validateUserV4(user tc.UserV4) error {
+	validateErrs := validation.Errors{
+		"email":    validation.Validate(user.Email, validation.Required, is.Email),
+		"fullName": validation.Validate(user.FullName, validation.Required),
+		"role":     validation.Validate(user.Role, validation.Required),
+		"username": validation.Validate(user.Username, validation.Required),
+		"tenantID": validation.Validate(user.TenantID, validation.Required),
+	}
+
+	// Password is not required for update
+	if user.LocalPassword != nil {
+		_, err := auth.IsGoodLoginPair(user.Username, *user.LocalPassword)
+		if err != nil {
+			return err
+		}
+	}
+
+	return util.JoinErrs(tovalidate.ToErrors(validateErrs))
+}
+
+func Create(w http.ResponseWriter, r *http.Request) {
+	var userV4 tc.UserV4
+	var err error
+	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
+	if err := json.NewDecoder(r.Body).Decode(&userV4); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	if err := validateUserV4(userV4); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	if err := postValidateV40(userV4); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	toUser := TOUser{
+		APIInfoImpl: api.APIInfoImpl{ReqInfo: inf},
+	}
+	toUser.User = userV4.Downgrade()
+
+	authorized, err := toUser.IsTenantAuthorized(inf.User)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant authorized: "+err.Error()))
+		return
+	}
+	if !authorized {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
+	}
+
+	// Convert password to SCRYPT
+	*userV4.LocalPassword, err = auth.DerivePassword(*userV4.LocalPassword)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	var resultRows *sqlx.Rows
+	_, ok, err := dbhelpers.GetRoleIDFromName(tx, userV4.Role)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error fetching ID from role name: %w", err))
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("role not found"), nil)
+		return
+	}
+
+	var caps []string
+	caps, err = dbhelpers.GetCapabilitiesFromRoleName(tx, userV4.Role)
+
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := inf.User.MissingPermissions(caps...)
+	if len(missing) != 0 {
+		api.HandleErr(w, r, tx, http.StatusForbidden, fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil)
+		return
+	}
+
+	resultRows, err = inf.Tx.NamedQuery(InsertQueryV40(), userV4)
+	if err != nil {
+		userErr, sysErr, statusCode := api.ParseDBError(err)
+		api.HandleErr(w, r, tx, statusCode, userErr, sysErr)
+		return
+	}
+	defer resultRows.Close()
+
+	var id int
+	var lastUpdated time.Time
+	var tenant string
+	var rolename string
+	var changeLogMsg string
+
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err = resultRows.Scan(&id, &lastUpdated, &tenant, &rolename); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("could not scan after insert: %w)", err))
+			return
+		}
+	}
+
+	if rowsAffected == 0 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("no userV4 was inserted, nothing was returned"))
+		return
+	} else if rowsAffected > 1 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("too many rows affected from userV4 insert"))
+		return
+	}
+
+	userV4.ID = &id
+	userV4.LastUpdated = lastUpdated
+	userV4.Tenant = &tenant
+	userV4.Role = rolename
+	userV4.LocalPassword = nil
+
+	userResponse := tc.UserResponseV4{
+		Response: userV4,
+		Alerts:   tc.CreateAlerts(tc.SuccessLevel, "user was created."),
+	}
+	w.Header().Set("Location", fmt.Sprintf("/api/%d.%d/users?id=%d", inf.Version.Major, inf.Version.Minor, *userV4.ID))
+	api.WriteAlertsObj(w, r, http.StatusCreated, userResponse.Alerts, userResponse.Response)
+	changeLogMsg = fmt.Sprintf("USER: %s, ID: %d, ACTION: Created User", userV4.Username, *userV4.ID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+	return
+}
+
+func (user *TOUser) InsertQuery() string {
+	return `
+	INSERT INTO tm_user (
+	username,
+	public_ssh_key,
+	role,
+	company,
+	email,
+	full_name,
+	new_user,
+	address_line1,
+	address_line2,
+	city,
+	state_or_province,
+	phone_number,
+	postal_code,
+	country,
+	tenant_id,
+	local_passwd
+	) VALUES (
+	:username,
+	:public_ssh_key,
+	:role,
+	:company,
+	:email,
+	:full_name,
+	COALESCE(:new_user, FALSE),
+	:address_line1,
+	:address_line2,
+	:city,
+	:state_or_province,
+	:phone_number,
+	:postal_code,
+	:country,
+	:tenant_id,
+	:local_passwd
+	) RETURNING id, last_updated,
+	(SELECT t.name FROM tenant t WHERE id = tm_user.tenant_id),
+	(SELECT r.name FROM role r WHERE id = tm_user.role)`
+}
+
+// Update is the handler for PUT requests made to /users.
+func Update(w http.ResponseWriter, r *http.Request) {
+	var userV4 tc.UserV4
+	var roleID int
+	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()
+
+	idParam, ok := inf.Params["id"]
+	if !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID supplied"), nil)
+		return
+	}
+	id, err := strconv.Atoi(idParam)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("couldn't convert id into an int"), nil)
+		return
+	}
+	if err := json.NewDecoder(r.Body).Decode(&userV4); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	if err := validateUserV4(userV4); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	userV4.ID = &id
+
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(inf.Tx.Tx, userV4.Role)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+	// make sure current userV4 cannot update their own role to a new value
+	if inf.User.ID == *userV4.ID && inf.User.Role != roleID {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("users cannot update their own role"), nil)
+		return
+	}
+
+	toUser := TOUser{
+		APIInfoImpl: api.APIInfoImpl{ReqInfo: inf},
+	}
+
+	toUser.User = userV4.Downgrade()
+
+	authorized, err := toUser.IsTenantAuthorized(inf.User)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant authorized: "+err.Error()))
+		return
+	}
+	if !authorized {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusForbidden, errors.New("not authorized on this tenant"), nil)
+		return
+	}
+	// make sure the userV4 cannot create someone with a higher priv_level than themselves
+	if userErr, sysErr, code := toUser.privCheck(); code != http.StatusOK {
+		api.HandleErr(w, r, tx, code, userErr, sysErr)
+		return
+	}
+
+	if userV4.LocalPassword != nil {
+		// Convert password to SCRYPT
+		*userV4.LocalPassword, err = auth.DerivePassword(*userV4.LocalPassword)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+	}
+
+	var caps []string
+	caps, err = dbhelpers.GetCapabilitiesFromRoleName(tx, userV4.Role)
+
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := inf.User.MissingPermissions(caps...)
+	if len(missing) != 0 {
+		api.HandleErr(w, r, tx, http.StatusForbidden, fmt.Errorf("cannot request more than assigned permissions, current user needs %s permissions", strings.Join(missing, ",")), nil)
+		return
+	}
+
+	userErr, sysErr, errCode = api.CheckIfUnModified(r.Header, inf.Tx, id, "tm_user")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	var resultRows *sqlx.Rows
+	resultRows, err = inf.Tx.NamedQuery(UpdateQueryV40(), userV4)
+
+	if err != nil {
+		api.ParseDBError(err)
+		return
+	}
+	defer resultRows.Close()
+
+	var lastUpdated time.Time
+	var tenant string
+	var rolename string
+	var changeLogMsg string
+
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err := resultRows.Scan(&lastUpdated, &tenant, &rolename); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("could not scan lastUpdated from insert: %s\n", err))
+			return
+		}
+	}
+
+	if rowsAffected != 1 {
+		if rowsAffected < 1 {
+			api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("no user found with this id"), nil)
+			return
+		}
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("this update affected too many rows: %d", rowsAffected))
+		return
+	}
+
+	userV4.LastUpdated = lastUpdated
+	userV4.Tenant = &tenant
+	userV4.Role = rolename
+	userV4.LocalPassword = nil
+
+	userResponse := tc.UserResponseV4{
+		Response: userV4,
+		Alerts:   tc.CreateAlerts(tc.SuccessLevel, "user was updated."),
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, userResponse.Alerts, userResponse.Response)
+	changeLogMsg = fmt.Sprintf("USER: %s, ID: %d, ACTION: Updated User", userV4.Username, *userV4.ID)
+
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
diff --git a/traffic_ops/v4-client/role.go b/traffic_ops/v4-client/role.go
index f55a719..cf831f7 100644
--- a/traffic_ops/v4-client/role.go
+++ b/traffic_ops/v4-client/role.go
@@ -17,7 +17,6 @@ package client
 
 import (
 	"net/url"
-	"strconv"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
@@ -27,36 +26,36 @@ import (
 const apiRoles = "/roles"
 
 // CreateRole creates the given Role.
-func (to *Session) CreateRole(role tc.Role, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) CreateRole(role tc.RoleV4, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	var alerts tc.Alerts
 	reqInf, err := to.post(apiRoles, opts, role, &alerts)
 	return alerts, reqInf, err
 }
 
 // UpdateRole replaces the Role identified by 'id' with the one provided.
-func (to *Session) UpdateRole(id int, role tc.Role, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) UpdateRole(name string, role tc.RoleV4, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	if opts.QueryParameters == nil {
 		opts.QueryParameters = url.Values{}
 	}
-	opts.QueryParameters.Set("id", strconv.Itoa(id))
+	opts.QueryParameters.Set("name", name)
 	var alerts tc.Alerts
 	reqInf, err := to.put(apiRoles, opts, role, &alerts)
 	return alerts, reqInf, err
 }
 
 // GetRoles retrieves Roles from Traffic Ops.
-func (to *Session) GetRoles(opts RequestOptions) (tc.RolesResponse, toclientlib.ReqInf, error) {
-	var data tc.RolesResponse
+func (to *Session) GetRoles(opts RequestOptions) (tc.RolesResponseV4, toclientlib.ReqInf, error) {
+	var data tc.RolesResponseV4
 	reqInf, err := to.get(apiRoles, opts, &data)
 	return data, reqInf, err
 }
 
 // DeleteRole deletes the Role with the given ID.
-func (to *Session) DeleteRole(id int, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) DeleteRole(name string, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	if opts.QueryParameters == nil {
 		opts.QueryParameters = url.Values{}
 	}
-	opts.QueryParameters.Set("id", strconv.Itoa(id))
+	opts.QueryParameters.Set("name", name)
 	var alerts tc.Alerts
 	reqInf, err := to.del(apiRoles, opts, &alerts)
 	return alerts, reqInf, err
diff --git a/traffic_ops/v4-client/user.go b/traffic_ops/v4-client/user.go
index 2c30d2d..eb6edaf 100644
--- a/traffic_ops/v4-client/user.go
+++ b/traffic_ops/v4-client/user.go
@@ -25,11 +25,11 @@ import (
 )
 
 // UserCurrentResponseV4 is an alias to avoid client breaking changes. In-case of a minor or major version change, we replace the below alias with a new structure.
-type UserCurrentResponseV4 = tc.UserCurrentResponseV40
+type UserCurrentResponseV4 = tc.UserCurrentResponseV4
 
 // GetUsers retrieves all (Tenant-accessible) Users stored in Traffic Ops.
-func (to *Session) GetUsers(opts RequestOptions) (tc.UsersResponseV40, toclientlib.ReqInf, error) {
-	data := tc.UsersResponseV40{}
+func (to *Session) GetUsers(opts RequestOptions) (tc.UsersResponseV4, toclientlib.ReqInf, error) {
+	data := tc.UsersResponseV4{}
 	route := "/users"
 	inf, err := to.get(route, opts, &data)
 	return data, inf, err
@@ -43,54 +43,54 @@ func (to *Session) GetUserCurrent(opts RequestOptions) (UserCurrentResponseV4, t
 	return resp, reqInf, err
 }
 
-// UpdateCurrentUser replaces the current user data with the provided tc.UserV40 structure.
-func (to *Session) UpdateCurrentUser(u tc.UserV40, opts RequestOptions) (tc.UpdateUserResponse, toclientlib.ReqInf, error) {
+// UpdateCurrentUser replaces the current user data with the provided tc.UserV4 structure.
+func (to *Session) UpdateCurrentUser(u tc.UserV4, opts RequestOptions) (tc.UpdateUserResponseV4, toclientlib.ReqInf, error) {
 	user := struct {
-		User tc.UserV40 `json:"user"`
+		User tc.UserV4 `json:"user"`
 	}{u}
-	var clientResp tc.UpdateUserResponse
+	var clientResp tc.UpdateUserResponseV4
 	reqInf, err := to.put("/user/current", opts, user, &clientResp)
 	return clientResp, reqInf, err
 }
 
 // CreateUser creates the given user.
-func (to *Session) CreateUser(user tc.UserV40, opts RequestOptions) (tc.CreateUserResponse, toclientlib.ReqInf, error) {
-	if user.TenantID == nil && user.Tenant != nil {
+func (to *Session) CreateUser(user tc.UserV4, opts RequestOptions) (tc.CreateUserResponseV4, toclientlib.ReqInf, error) {
+	if user.Tenant != nil {
 		innerOpts := NewRequestOptions()
 		innerOpts.QueryParameters.Set("name", *user.Tenant)
 		tenant, _, err := to.GetTenants(innerOpts)
 		if err != nil {
-			return tc.CreateUserResponse{Alerts: tenant.Alerts}, toclientlib.ReqInf{}, fmt.Errorf("resolving Tenant name '%s' to an ID: %w", *user.Tenant, err)
+			return tc.CreateUserResponseV4{Alerts: tenant.Alerts}, toclientlib.ReqInf{}, fmt.Errorf("resolving Tenant name '%s' to an ID: %w", *user.Tenant, err)
 		}
 		if len(tenant.Response) < 1 {
-			return tc.CreateUserResponse{Alerts: tenant.Alerts}, toclientlib.ReqInf{}, fmt.Errorf("no such Tenant: '%s'", *user.Tenant)
+			return tc.CreateUserResponseV4{Alerts: tenant.Alerts}, toclientlib.ReqInf{}, fmt.Errorf("no such Tenant: '%s'", *user.Tenant)
 		}
-		user.TenantID = &tenant.Response[0].ID
+		user.TenantID = tenant.Response[0].ID
 	}
 
-	if user.RoleName != nil && *user.RoleName != "" {
+	if user.Role != "" {
 		innerOpts := NewRequestOptions()
-		innerOpts.QueryParameters.Set("name", *user.RoleName)
+		innerOpts.QueryParameters.Set("name", user.Role)
 		roles, _, err := to.GetRoles(innerOpts)
 		if err != nil {
-			return tc.CreateUserResponse{Alerts: roles.Alerts}, toclientlib.ReqInf{}, fmt.Errorf("resolving Role name '%s' to an ID: %w", *user.RoleName, err)
+			return tc.CreateUserResponseV4{Alerts: roles.Alerts}, toclientlib.ReqInf{}, fmt.Errorf("resolving Role name '%s' to an ID: %w", user.Role, err)
 		}
-		if len(roles.Response) == 0 || roles.Response[0].ID == nil {
-			return tc.CreateUserResponse{Alerts: roles.Alerts}, toclientlib.ReqInf{}, fmt.Errorf("no such Role: '%s'", *user.RoleName)
+		if len(roles.Response) == 0 {
+			return tc.CreateUserResponseV4{Alerts: roles.Alerts}, toclientlib.ReqInf{}, fmt.Errorf("no such Role: '%s'", user.Role)
 		}
-		user.Role = roles.Response[0].ID
+		user.Role = roles.Response[0].Name
 	}
 
 	route := "/users"
-	var clientResp tc.CreateUserResponse
+	var clientResp tc.CreateUserResponseV4
 	reqInf, err := to.post(route, opts, user, &clientResp)
 	return clientResp, reqInf, err
 }
 
 // UpdateUser replaces the User identified by 'id' with the one provided.
-func (to *Session) UpdateUser(id int, u tc.UserV40, opts RequestOptions) (tc.UpdateUserResponse, toclientlib.ReqInf, error) {
+func (to *Session) UpdateUser(id int, u tc.UserV4, opts RequestOptions) (tc.UpdateUserResponseV4, toclientlib.ReqInf, error) {
 	route := "/users/" + strconv.Itoa(id)
-	var clientResp tc.UpdateUserResponse
+	var clientResp tc.UpdateUserResponseV4
 	reqInf, err := to.put(route, opts, u, &clientResp)
 	return clientResp, reqInf, err
 }
@@ -105,12 +105,12 @@ func (to *Session) DeleteUser(id int, opts RequestOptions) (tc.Alerts, toclientl
 
 // RegisterNewUser requests the registration of a new user with the given tenant ID and role ID,
 // through their email.
-func (to *Session) RegisterNewUser(tenantID uint, roleID uint, email rfc.EmailAddress, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) RegisterNewUser(tenantID uint, role string, email rfc.EmailAddress, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	var alerts tc.Alerts
-	reqBody := tc.UserRegistrationRequest{
+	reqBody := tc.UserRegistrationRequestV40{
 		Email:    email,
 		TenantID: tenantID,
-		Role:     roleID,
+		Role:     role,
 	}
 	reqInf, err := to.post("/users/register", opts, reqBody, &alerts)
 	return alerts, reqInf, err
diff --git a/traffic_portal/app/src/common/api/RoleService.js b/traffic_portal/app/src/common/api/RoleService.js
index b981441..7fb864e 100644
--- a/traffic_portal/app/src/common/api/RoleService.js
+++ b/traffic_portal/app/src/common/api/RoleService.js
@@ -27,7 +27,7 @@ var RoleService = function($http, messageModel, ENV) {
             function (err) {
                 throw err;
             }
-        )
+        );
     };
 
     this.createRole = function(role) {
@@ -43,7 +43,7 @@ var RoleService = function($http, messageModel, ENV) {
     };
 
     this.updateRole = function(role) {
-        return $http.put(ENV.api['root'] + "roles", role, {params: {id: role.id}}).then(
+        return $http.put(ENV.api['root'] + "roles", role, {params: {name: role.name}}).then(
             function(result) {
                 return result.data;
             },
@@ -54,8 +54,8 @@ var RoleService = function($http, messageModel, ENV) {
         );
     };
 
-    this.deleteRole = function(id) {
-        return $http.delete(ENV.api['root'] + "roles", {params: {id: id}}).then(
+    this.deleteRole = function(name) {
+        return $http.delete(ENV.api['root'] + "roles", {params: {name: name}}).then(
             function(result) {
                 return result.data;
             },
diff --git a/traffic_portal/app/src/common/modules/form/role/edit/FormEditRoleController.js b/traffic_portal/app/src/common/modules/form/role/edit/FormEditRoleController.js
index 2d90c46..080f477 100644
--- a/traffic_portal/app/src/common/modules/form/role/edit/FormEditRoleController.js
+++ b/traffic_portal/app/src/common/modules/form/role/edit/FormEditRoleController.js
@@ -23,7 +23,7 @@ var FormEditRoleController = function(roles, $scope, $controller, $uibModal, $an
 	angular.extend(this, $controller('FormRoleController', { roles: roles, $scope: $scope }));
 
 	var deleteRole = function(role) {
-		roleService.deleteRole(role.id)
+		roleService.deleteRole(role.name)
 			.then(function(result) {
 				messageModel.setMessages(result.alerts, true);
 				locationUtils.navigateToPath('/roles');
diff --git a/traffic_portal/app/src/common/modules/form/role/form.role.tpl.html b/traffic_portal/app/src/common/modules/form/role/form.role.tpl.html
index 2afb57e..bf3b626 100644
--- a/traffic_portal/app/src/common/modules/form/role/form.role.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/role/form.role.tpl.html
@@ -41,16 +41,6 @@ under the License.
                     <span ng-show="hasError(roleForm.name)" class="form-control-feedback"><i class="fa fa-times"></i></span>
                 </div>
             </div>
-            <div class="form-group" ng-class="{'has-error': hasError(roleForm.privLevel), 'has-feedback': hasError(roleForm.privLevel)}">
-                <label class="control-label col-md-2 col-sm-2 col-xs-12">Privilege Level *</label>
-                <div class="col-md-10 col-sm-10 col-xs-12">
-                    <input name="privLevel" type="number" class="form-control" ng-model="role.privLevel" ng-maxlength="3" ng-pattern="/^\d+$/" required autofocus>
-                    <small class="input-error" ng-show="hasPropertyError(roleForm.privLevel, 'required')">Required Whole Number</small>
-                    <small class="input-error" ng-show="hasPropertyError(roleForm.privLevel, 'maxlength')">Too Long</small>
-                    <small class="input-error" ng-show="hasPropertyError(roleForm.privLevel, 'pattern')">Whole Number</small>
-                    <span ng-show="hasError(roleForm.privLevel)" class="form-control-feedback"><i class="fa fa-times"></i></span>
-                </div>
-            </div>
             <div class="form-group" ng-class="{'has-error': hasError(roleForm.description), 'has-feedback': hasError(roleForm.description)}">
                 <label class="control-label col-md-2 col-sm-2 col-xs-12">Description *</label>
                 <div class="col-md-10 col-sm-10 col-xs-12">
diff --git a/traffic_portal/app/src/common/modules/form/user/FormUserController.js b/traffic_portal/app/src/common/modules/form/user/FormUserController.js
index 6d12712..d55a5f6 100644
--- a/traffic_portal/app/src/common/modules/form/user/FormUserController.js
+++ b/traffic_portal/app/src/common/modules/form/user/FormUserController.js
@@ -22,7 +22,7 @@ var FormUserController = function(user, $scope, $location, formUtils, stringUtil
     var getRoles = function() {
         roleService.getRoles()
             .then(function(result) {
-                $scope.roles = _.sortBy(result, 'privLevel').reverse();
+                $scope.roles = _.sortBy(result, 'name').reverse();
             });
     };
 
@@ -40,7 +40,7 @@ var FormUserController = function(user, $scope, $location, formUtils, stringUtil
     $scope.user = user;
 
     $scope.label = function(role) {
-        return role.name + ' (' + role.privLevel + ')';
+        return role.name;
     };
 
     $scope.tenantLabel = function(tenant) {
diff --git a/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html b/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
index ea81aa2..29cd054 100644
--- a/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
@@ -63,9 +63,9 @@ under the License.
                 </div>
             </div>
             <div class="form-group" ng-class="{'has-error': hasError(userForm.role), 'has-feedback': hasError(userForm.role)}">
-                <label class="control-label col-md-2 col-sm-2 col-xs-12">Role (privilege level) *</label>
+                <label class="control-label col-md-2 col-sm-2 col-xs-12">Role*</label>
                 <div class="col-md-10 col-sm-10 col-xs-12">
-                    <select name="role" class="form-control" ng-model="user.role" ng-options="role.id as label(role) for role in roles" required>
+                    <select name="role" class="form-control" ng-model="user.role" ng-options="role.name as label(role) for role in roles" required>
                         <option value="">Select...</option>
                     </select>
                     <small class="input-error" ng-show="hasPropertyError(userForm.role, 'required')">Required</small>
diff --git a/traffic_portal/app/src/common/modules/form/user/register/FormRegisterUserController.js b/traffic_portal/app/src/common/modules/form/user/register/FormRegisterUserController.js
index 195b156..8b526ad 100644
--- a/traffic_portal/app/src/common/modules/form/user/register/FormRegisterUserController.js
+++ b/traffic_portal/app/src/common/modules/form/user/register/FormRegisterUserController.js
@@ -22,7 +22,7 @@ var FormRegisterUserController = function($scope, $location, formUtils, tenantUt
 	var getRoles = function() {
 		roleService.getRoles()
 			.then(function(result) {
-				$scope.roles = _.sortBy(result, 'privLevel').reverse();
+				$scope.roles = _.sortBy(result, 'name');
 			});
 	};
 
@@ -48,7 +48,7 @@ var FormRegisterUserController = function($scope, $location, formUtils, tenantUt
 	};
 
 	$scope.roleLabel = function(role) {
-		return role.name + ' (' + role.privLevel + ')';
+		return role.name;
 	};
 
 	$scope.navigateToPath = locationUtils.navigateToPath;
diff --git a/traffic_portal/app/src/common/modules/form/user/register/form.user.register.tpl.html b/traffic_portal/app/src/common/modules/form/user/register/form.user.register.tpl.html
index 85c6ce5..861c1c6 100644
--- a/traffic_portal/app/src/common/modules/form/user/register/form.user.register.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/user/register/form.user.register.tpl.html
@@ -42,7 +42,7 @@ under the License.
             <div class="form-group" ng-class="{'has-error': hasError(registerForm.role), 'has-feedback': hasError(registerForm.role)}">
                 <label class="control-label col-md-2 col-sm-2 col-xs-12">Role (privilege level) *</label>
                 <div class="col-md-10 col-sm-10 col-xs-12">
-                    <select name="role" class="form-control" ng-model="registration.role" ng-options="role.id as roleLabel(role) for role in roles" required>
+                    <select name="role" class="form-control" ng-model="registration.role" ng-options="role.name as roleLabel(role) for role in roles" required>
                         <option value="">Select...</option>
                     </select>
                     <small class="input-error" ng-show="hasPropertyError(registerForm.role, 'required')">Required</small>
diff --git a/traffic_portal/app/src/common/modules/table/roleCapabilities/table.roleCapabilities.tpl.html b/traffic_portal/app/src/common/modules/table/roleCapabilities/table.roleCapabilities.tpl.html
index 911bff1..8f76528 100644
--- a/traffic_portal/app/src/common/modules/table/roleCapabilities/table.roleCapabilities.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/roleCapabilities/table.roleCapabilities.tpl.html
@@ -21,7 +21,7 @@ under the License.
     <div class="x_title">
         <ol class="breadcrumb pull-left">
             <li><a ng-click="navigateToPath('/roles')">Roles</a></li>
-            <li><a ng-click="navigateToPath('/roles/' + role.id)">{{::role.name}}</a></li>
+            <li><a ng-click="navigateToPath('/roles/' + role.name)">{{::role.name}}</a></li>
             <li class="active">Capabilities</li>
         </ol>
         <div class="pull-right" role="group">
diff --git a/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html b/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html
index 39adb98..d64331f 100644
--- a/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html
@@ -21,7 +21,7 @@ under the License.
     <div class="x_title">
         <ol class="breadcrumb pull-left">
             <li><a ng-click="navigateToPath('/roles')">Roles</a></li>
-            <li><a ng-click="navigateToPath('/roles/' + role.id)">{{::role.name}}</a></li>
+            <li><a ng-click="navigateToPath('/roles/' + role.name)">{{::role.name}}</a></li>
             <li class="active">Users</li>
         </ol>
         <div class="pull-right">
diff --git a/traffic_portal/app/src/common/modules/table/roles/TableRolesController.js b/traffic_portal/app/src/common/modules/table/roles/TableRolesController.js
index 1bb7b97..75f0b9b 100644
--- a/traffic_portal/app/src/common/modules/table/roles/TableRolesController.js
+++ b/traffic_portal/app/src/common/modules/table/roles/TableRolesController.js
@@ -21,8 +21,8 @@ var TableRolesController = function(roles, $scope, $state, locationUtils) {
 
 	$scope.roles = roles;
 
-	$scope.editRole = function(id) {
-		locationUtils.navigateToPath('/roles/' + id);
+	$scope.editRole = function(name) {
+		locationUtils.navigateToPath('/roles/edit/' + name);
 	};
 
 	$scope.createRole = function() {
diff --git a/traffic_portal/app/src/common/modules/table/roles/table.roles.tpl.html b/traffic_portal/app/src/common/modules/table/roles/table.roles.tpl.html
index c27a253..22dcff2 100644
--- a/traffic_portal/app/src/common/modules/table/roles/table.roles.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/roles/table.roles.tpl.html
@@ -34,14 +34,12 @@ under the License.
             <thead>
             <tr class="headings">
                 <th>Name</th>
-                <th>Privilege Level</th>
                 <th>Description</th>
             </tr>
             </thead>
             <tbody>
-            <tr ng-click="editRole(r.id)" ng-repeat="r in ::roles">
+            <tr ng-click="editRole(r.name)" ng-repeat="r in ::roles">
                 <td>{{::r.name}}</td>
-                <td>{{::r.privLevel}}</td>
                 <td>{{::r.description}}</td>
             </tr>
             </tbody>
diff --git a/traffic_portal/app/src/common/modules/table/users/table.users.tpl.html b/traffic_portal/app/src/common/modules/table/users/table.users.tpl.html
index 48bf88e..7a6699b 100644
--- a/traffic_portal/app/src/common/modules/table/users/table.users.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/users/table.users.tpl.html
@@ -63,7 +63,7 @@ under the License.
                 <td name="username" data-search="^{{::u.username}}$">{{::u.username}}</td>
                 <td name="email" data-search="^{{::u.email}}$">{{::u.email}}</td>
                 <td data-search="^{{::u.tenant}}$">{{::u.tenant}}</td>
-                <td data-search="^{{::u.rolename}}$">{{::u.rolename}}</td>
+                <td data-search="^{{::u.role}}$">{{::u.role}}</td>
                 <td data-search="^{{::u.registrationSent}}$">{{::u.registrationSent}}</td>
                 <td data-search="^{{::relativeLoginTime(u.lastAuthenticated)}}$">{{::relativeLoginTime(u.lastAuthenticated)}}</td>
                 <td data-search="^{{::u.changeLogCount}}$">{{::u.changeLogCount}}</td>
diff --git a/traffic_portal/app/src/modules/private/roles/capabilities/index.js b/traffic_portal/app/src/modules/private/roles/capabilities/index.js
index bc012bf..7a5d13f 100644
--- a/traffic_portal/app/src/modules/private/roles/capabilities/index.js
+++ b/traffic_portal/app/src/modules/private/roles/capabilities/index.js
@@ -21,14 +21,14 @@ module.exports = angular.module('trafficPortal.private.roles.capabilities', [])
 	.config(function($stateProvider, $urlRouterProvider) {
 		$stateProvider
 			.state('trafficPortal.private.roles.capabilities', {
-				url: '/{roleId}/capabilities',
+				url: '/{roleName}/capabilities',
 				views: {
 					rolesContent: {
 						templateUrl: 'common/modules/table/roleCapabilities/table.roleCapabilities.tpl.html',
 						controller: 'TableRoleCapabilitiesController',
 						resolve: {
 							roles: function($stateParams, roleService) {
-								return roleService.getRoles({ id: $stateParams.roleId });
+								return roleService.getRoles({ name: $stateParams.roleName });
 							}
 						}
 					}
diff --git a/traffic_portal/app/src/modules/private/roles/edit/index.js b/traffic_portal/app/src/modules/private/roles/edit/index.js
index df12cc6..42dfb8a 100644
--- a/traffic_portal/app/src/modules/private/roles/edit/index.js
+++ b/traffic_portal/app/src/modules/private/roles/edit/index.js
@@ -21,14 +21,14 @@ module.exports = angular.module('trafficPortal.private.roles.edit', [])
 	.config(function($stateProvider, $urlRouterProvider) {
 		$stateProvider
 			.state('trafficPortal.private.roles.edit', {
-				url: '/{roleId:[0-9]{1,8}}',
+				url: '/edit/{roleName}',
 				views: {
 					rolesContent: {
 						templateUrl: 'common/modules/form/role/form.role.tpl.html',
 						controller: 'FormEditRoleController',
 						resolve: {
 							roles: function($stateParams, roleService) {
-								return roleService.getRoles({ id: $stateParams.roleId });
+								return roleService.getRoles({ name: $stateParams.roleName });
 							}
 						}
 					}
diff --git a/traffic_portal/app/src/modules/private/roles/list/index.js b/traffic_portal/app/src/modules/private/roles/list/index.js
index a85bde3..3e8a17c 100644
--- a/traffic_portal/app/src/modules/private/roles/list/index.js
+++ b/traffic_portal/app/src/modules/private/roles/list/index.js
@@ -28,7 +28,7 @@ module.exports = angular.module('trafficPortal.private.roles.list', [])
 						controller: 'TableRolesController',
 						resolve: {
 							roles: function(roleService) {
-								return roleService.getRoles({ orderby: 'privLevel', sortOrder : 'desc' });
+								return roleService.getRoles({ orderby: 'name', sortOrder : 'asc' });
 							}
 						}
 					}
diff --git a/traffic_portal/app/src/modules/private/roles/users/index.js b/traffic_portal/app/src/modules/private/roles/users/index.js
index 524fc09..299a98a 100644
--- a/traffic_portal/app/src/modules/private/roles/users/index.js
+++ b/traffic_portal/app/src/modules/private/roles/users/index.js
@@ -21,14 +21,14 @@ module.exports = angular.module('trafficPortal.private.roles.users', [])
 	.config(function($stateProvider, $urlRouterProvider) {
 		$stateProvider
 			.state('trafficPortal.private.roles.users', {
-				url: '/{roleId}/users',
+				url: '/{roleName}/users',
 				views: {
 					rolesContent: {
 						templateUrl: 'common/modules/table/roleUsers/table.roleUsers.tpl.html',
 						controller: 'TableRoleUsersController',
 						resolve: {
 							roles: function($stateParams, roleService) {
-								return roleService.getRoles({ id: $stateParams.roleId });
+								return roleService.getRoles({ name: $stateParams.roleName });
 							},
 							roleUsers: function(roles, userService) {
 								return userService.getUsers({ role: roles[0].name });
diff --git a/traffic_portal/app/src/modules/private/user/UserController.js b/traffic_portal/app/src/modules/private/user/UserController.js
index f4e6702..e4d29d3 100644
--- a/traffic_portal/app/src/modules/private/user/UserController.js
+++ b/traffic_portal/app/src/modules/private/user/UserController.js
@@ -31,7 +31,7 @@ var UserController = function($scope, $state, $location, $uibModal, formUtils, l
     var getRoles = function() {
         roleService.getRoles()
             .then(function(result) {
-                $scope.roles = _.sortBy(result, 'privLevel').reverse();
+                $scope.roles = _.sortBy(result, 'name');
             });
     };
 
diff --git a/traffic_portal/app/src/modules/private/user/edit/UserEditController.js b/traffic_portal/app/src/modules/private/user/edit/UserEditController.js
index f189931..5c1f322 100644
--- a/traffic_portal/app/src/modules/private/user/edit/UserEditController.js
+++ b/traffic_portal/app/src/modules/private/user/edit/UserEditController.js
@@ -25,7 +25,7 @@ var UserEditController = function($scope) {
     };
 
     $scope.label = function(role) {
-        return role.name + ' (' + role.privLevel + ')';
+        return role.name;
     };
 
     $scope.tenantLabel = function(tenant) {
diff --git a/traffic_portal/test/integration/Data/prerequisites.ts b/traffic_portal/test/integration/Data/prerequisites.ts
index 1afd3da..3210463 100644
--- a/traffic_portal/test/integration/Data/prerequisites.ts
+++ b/traffic_portal/test/integration/Data/prerequisites.ts
@@ -69,7 +69,7 @@ export const prerequisites = [
 				fullName: "TPAdmin",
 				username: "TPAdmin",
 				email: "@test.com",
-				role: 0,
+				role: "admin",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -79,12 +79,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantSame",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "admin",
-						replace: "role"
 					}
 				]
 			},
@@ -92,7 +86,7 @@ export const prerequisites = [
 				fullName: "TPOperator",
 				username: "TPOperator",
 				email: "@test.com",
-				role: 0,
+				role: "operations",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -102,12 +96,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantSame",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "operations",
-						replace: "role"
 					}
 				]
 			},
@@ -115,7 +103,7 @@ export const prerequisites = [
 				fullName: "TPReadOnly",
 				username: "TPReadOnly",
 				email: "@test.com",
-				role: 0,
+				role: "read-only",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -125,12 +113,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantSame",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "read-only",
-						replace: "role"
 					}
 				]
 			},
@@ -138,7 +120,7 @@ export const prerequisites = [
 				fullName: "TPAdminDiff",
 				username: "TPAdminDiff",
 				email: "@test.com",
-				role: 0,
+				role: "admin",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -148,12 +130,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantDifferent",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "admin",
-						replace: "role"
 					}
 				]
 			},
@@ -161,7 +137,7 @@ export const prerequisites = [
 				fullName: "TPOperatorDiff",
 				username: "TPOperatorDiff",
 				email: "@test.com",
-				role: 0,
+				role: "operations",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -171,12 +147,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantDifferent",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "operations",
-						replace: "role"
 					}
 				]
 			},
@@ -184,7 +154,7 @@ export const prerequisites = [
 				fullName: "TPReadOnlyDiff",
 				username: "TPReadOnlyDiff",
 				email: "@test.com",
-				role: 0,
+				role: "read-only",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -194,12 +164,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantDifferent",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "read-only",
-						replace: "role"
 					}
 				]
 			},
@@ -207,7 +171,7 @@ export const prerequisites = [
 				fullName: "TPAdminParent",
 				username: "TPAdminParent",
 				email: "@test.com",
-				role: 0,
+				role: "admin",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -217,12 +181,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantParent",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "admin",
-						replace: "role"
 					}
 				]
 			},
@@ -230,7 +188,7 @@ export const prerequisites = [
 				fullName: "TPOperatorParent",
 				username: "TPOperatorParent",
 				email: "@test.com",
-				role: 0,
+				role: "operations",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -240,12 +198,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantParent",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "operations",
-						replace: "role"
 					}
 				]
 			},
@@ -253,7 +205,7 @@ export const prerequisites = [
 				fullName: "TPReadOnlyParent",
 				username: "TPReadOnlyParent",
 				email: "@test.com",
-				role: 0,
+				role: "read-only",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -263,12 +215,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantParent",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "read-only",
-						replace: "role"
 					}
 				]
 			},
@@ -276,7 +222,7 @@ export const prerequisites = [
 				fullName: "TPAdminChild",
 				username: "TPAdminChild",
 				email: "@test.com",
-				role: 0,
+				role: "admin",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -286,12 +232,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantChild",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "admin",
-						replace: "role"
 					}
 				]
 			},
@@ -299,7 +239,7 @@ export const prerequisites = [
 				fullName: "TPOperatorChild",
 				username: "TPOperatorChild",
 				email: "@test.com",
-				role: 0,
+				role: "operations",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -309,12 +249,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantChild",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "operations",
-						replace: "role"
 					}
 				]
 			},
@@ -322,7 +256,7 @@ export const prerequisites = [
 				fullName: "TPReadOnlyChild",
 				username: "TPReadOnlyChild",
 				email: "@test.com",
-				role: 0,
+				role: "read-only",
 				tenantId: 1,
 				localPasswd: "pa$$word",
 				confirmLocalPasswd: "pa$$word",
@@ -332,12 +266,6 @@ export const prerequisites = [
 						queryKey: "name",
 						queryValue: "tenantChild",
 						replace: "tenantId"
-					},
-					{
-						route: "/roles",
-						queryKey: "name",
-						queryValue: "read-only",
-						replace: "role"
 					}
 				]
 			}