You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@trafficcontrol.apache.org by GitBox <gi...@apache.org> on 2021/08/19 17:07:00 UTC

[GitHub] [trafficcontrol] srijeet0406 opened a new pull request #6124: Role and User struct changes for permission based roles

srijeet0406 opened a new pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124


   <!--
   Thank you for contributing! Please be sure to read our contribution guidelines: https://github.com/apache/trafficcontrol/blob/master/CONTRIBUTING.md
   If this closes or relates to an existing issue, please reference it using one of the following:
   
   Closes: #ISSUE
   Related: #ISSUE
   
   If this PR fixes a security vulnerability, DO NOT submit! Instead, contact
   the Apache Traffic Control Security Team at security@trafficcontrol.apache.org and follow the
   guidelines at https://apache.org/security regarding vulnerability disclosure.
   -->
   
   
   <!-- **^ Add meaningful description above** --><hr>
   
   ## Which Traffic Control components are affected by this PR?
   <!-- Please delete all components from this list that are NOT affected by this PR.
   Feel free to add the name of a tool or script that is affected but not on the list.
   -->
   - Documentation
   - Traffic Control Cache Config (T3C, formerly ORT)
   - Traffic Control Client <!-- Please specify which (Python, Go, or Java) -->
   - Traffic Monitor
   - Traffic Ops
   - Traffic Portal
   - Traffic Router
   - Traffic Stats
   - Grove
   - CDN in a Box
   - Automation <!-- Please specify which (GitHub Actions, Docker images, Ansible Roles, etc.) -->
   - unknown
   
   ## What is the best way to verify this PR?
   <!-- Please include here ALL the steps necessary to test your PR.
   If your PR has tests (and most should), provide the steps needed to run the tests.
   If not, please provide step-by-step instructions to test the PR manually and explain why your PR does not need tests. -->
   
   
   ## If this is a bugfix, which Traffic Control versions contained the bug?
   <!-- Delete this section if the PR is not a bugfix, or if the bug is only in the master branch.
   Examples:
   - 5.1.2
   - 5.1.3 (RC1)
    -->
   
   
   ## PR submission checklist
   - [x] This PR has tests <!-- If not, please delete this text and explain why this PR does not need tests. -->
   - [x] This PR has documentation <!-- If not, please delete this text and explain why this PR does not need documentation. -->
   - [x] This PR has a CHANGELOG.md entry <!-- A fix for a bug from an ATC release, an improvement, or a new feature should have a changelog entry. -->
   - [x] This PR **DOES NOT FIX A SERIOUS SECURITY VULNERABILITY** (see [the Apache Software Foundation's security guidelines](https://apache.org/security) for details)
   
   <!--
   Licensed to the Apache Software Foundation (ASF) under one
   or more contributor license agreements.  See the NOTICE file
   distributed with this work for additional information
   regarding copyright ownership.  The ASF licenses this file
   to you under the Apache License, Version 2.0 (the
   "License"); you may not use this file except in compliance
   with the License.  You may obtain a copy of the License at
   
       http://www.apache.org/licenses/LICENSE-2.0
   
   Unless required by applicable law or agreed to in writing,
   software distributed under the License is distributed on an
   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   KIND, either express or implied.  See the License for the
   specific language governing permissions and limitations
   under the License.
   -->
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#issuecomment-906757453


   If the TP tests don't pass this time we'll need to consider it a real failure - also, conflicts.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r709385225



##########
File path: docs/source/api/v4/roles.rst
##########
@@ -83,19 +80,19 @@ 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: 1608
 
 	{ "response": [
 		{
-			"id": 4,
 			"name": "admin",
-			"description": "super-user",
-			"privLevel": 30,
-			"capabilities": [
+			"description": "Has access to everything.",
+			"permissions": [
 				"all-write",
-				"all-read"
-			]
+				"all-read",
+				...,

Review comment:
       If you're concerned that the list takes up too much space, you could always create a new Role with only one or two Permissions and then show that request/response - or don't, it's fine how it is IMO.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r711243030



##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -100,6 +118,32 @@ func (role *TORole) SetKeys(keys map[string]interface{}) {
 	role.ID = &i
 }
 
+func (role TORole) validate(tx *sqlx.Tx) error {
+	var capabilities *[]string
+	errs := make(map[string]error)
+	errs = validation.Errors{
+		"name":        validation.Validate(role.Name, validation.Required),
+		"description": validation.Validate(role.Description, validation.Required),
+		"privLevel":   validation.Validate(role.PrivLevel, validation.Required),
+	}
+	capabilities = role.Capabilities
+
+	errsToReturn := tovalidate.ToErrors(errs)
+	checkCaps := `SELECT cap FROM UNNEST($1::text[]) AS cap WHERE NOT cap =  ANY(ARRAY(SELECT c.name FROM capability AS c WHERE c.name = ANY($1)))`

Review comment:
       I think this query can be simpler if you do do `WHERE cap NOT IN (SELECT ...)` instead of `WHERE NOT cap = ANY(ARRAY(SELECT ...))`

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	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 roleID int
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+	} else {
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to delete"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to delete"), nil)
+				return
+			}
+		}
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+	if _, err = result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	}
+	// TODO verify expected row count shouldn't be checked?
+	return nil, nil, http.StatusOK
+}
+
+// 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 roleV4 tc.RoleV4
+	var role TORole
+
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleName = roleV4.Name
+		roleDesc = roleV4.Description
+		privLevel = inf.User.PrivLevel
+		roleCapabilities = &roleV4.Permissions

Review comment:
       This should be checking for privilege escalation - you shouldn't be able to create a Role with Permissions you don't have.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)

Review comment:
       If we aren't reading the rows, this can just be an `Exec` - but I think we should be reading back `lastUpdated` at least.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	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 roleID int
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+	} else {
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to delete"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to delete"), nil)
+				return
+			}
+		}
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+	if _, err = result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	}

Review comment:
       Should this cause the request to fail.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -100,6 +118,32 @@ func (role *TORole) SetKeys(keys map[string]interface{}) {
 	role.ID = &i
 }
 
+func (role TORole) validate(tx *sqlx.Tx) error {
+	var capabilities *[]string
+	errs := make(map[string]error)
+	errs = validation.Errors{
+		"name":        validation.Validate(role.Name, validation.Required),
+		"description": validation.Validate(role.Description, validation.Required),
+		"privLevel":   validation.Validate(role.PrivLevel, validation.Required),
+	}
+	capabilities = role.Capabilities
+
+	errsToReturn := tovalidate.ToErrors(errs)
+	checkCaps := `SELECT cap FROM UNNEST($1::text[]) AS cap WHERE NOT cap =  ANY(ARRAY(SELECT c.name FROM capability AS c WHERE c.name = ANY($1)))`
+	var badCaps []string
+	if tx != nil {
+		err := tx.Select(&badCaps, checkCaps, pq.Array(capabilities))
+		if err != nil {
+			log.Errorf("got error from selecting bad capabilities: %v", err)
+			return err

Review comment:
       should this just be returning `fmt.Errorf("got error from selecting bad capabilities: %w", err)`?

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))

Review comment:
       errors constructed from errors should be wrapped using `fmt.Errorf` and `%w`

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))

Review comment:
       same as above, RE: errors

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	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 roleID int
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+	} else {
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to delete"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to delete"), nil)
+				return
+			}
+		}

Review comment:
       nit but this doesn't need to be in an `else` - the preceding `if` ends with a `return`.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	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 roleID int
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+	} else {
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to delete"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to delete"), nil)
+				return
+			}
+		}
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError

Review comment:
       same as above RE: errors

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError

Review comment:
       same as above RE: errors

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))

Review comment:
       same as above RE: errors

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	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 roleID int
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+	} else {
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to delete"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to delete"), nil)
+				return
+			}
+		}
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+	if _, err = result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	}
+	// TODO verify expected row count shouldn't be checked?
+	return nil, nil, http.StatusOK
+}
+
+// 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 roleV4 tc.RoleV4
+	var role TORole
+
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleName = roleV4.Name
+		roleDesc = roleV4.Description
+		privLevel = inf.User.PrivLevel
+		roleCapabilities = &roleV4.Permissions
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+	}
+
+	rows, err := tx.Query(createQuery(), roleName, roleDesc, privLevel)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("creating role: "+err.Error()))
+		return
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var throwaway interface{}
+		if err := rows.Scan(&roleID, &throwaway); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role create: scanning role ID: "+err.Error()))
+			return
+		}
+	}
+
+	if roleCapabilities != nil && 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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, alerts, roleResponse)

Review comment:
       Per API guidelines, the response code should be `201 Created`, ideally with a `Location` header that contains a URI for the created resource.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	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 roleID int
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+	} else {
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to delete"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to delete"), nil)
+				return
+			}
+		}
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+	if _, err = result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	}
+	// TODO verify expected row count shouldn't be checked?
+	return nil, nil, http.StatusOK
+}
+
+// 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 roleV4 tc.RoleV4
+	var role TORole
+
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleName = roleV4.Name
+		roleDesc = roleV4.Description
+		privLevel = inf.User.PrivLevel
+		roleCapabilities = &roleV4.Permissions
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+	}
+
+	rows, err := tx.Query(createQuery(), roleName, roleDesc, privLevel)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("creating role: "+err.Error()))

Review comment:
       same as above RE: errors

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	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 roleID int
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+	} else {
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to delete"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to delete"), nil)
+				return
+			}
+		}
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+	if _, err = result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	}
+	// TODO verify expected row count shouldn't be checked?
+	return nil, nil, http.StatusOK
+}
+
+// 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 roleV4 tc.RoleV4
+	var role TORole
+
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleName = roleV4.Name
+		roleDesc = roleV4.Description
+		privLevel = inf.User.PrivLevel
+		roleCapabilities = &roleV4.Permissions
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+	}
+
+	rows, err := tx.Query(createQuery(), roleName, roleDesc, privLevel)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("creating role: "+err.Error()))
+		return
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var throwaway interface{}
+		if err := rows.Scan(&roleID, &throwaway); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role create: scanning role ID: "+err.Error()))

Review comment:
       same as above RE: errors

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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

Review comment:
       This section needs to check for privilege escalation, I think. Right now it looks like it'll let you update your Role to have whatever Permissions you want.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -100,6 +118,32 @@ func (role *TORole) SetKeys(keys map[string]interface{}) {
 	role.ID = &i
 }
 
+func (role TORole) validate(tx *sqlx.Tx) error {
+	var capabilities *[]string
+	errs := make(map[string]error)
+	errs = validation.Errors{
+		"name":        validation.Validate(role.Name, validation.Required),
+		"description": validation.Validate(role.Description, validation.Required),
+		"privLevel":   validation.Validate(role.PrivLevel, validation.Required),
+	}
+	capabilities = role.Capabilities
+
+	errsToReturn := tovalidate.ToErrors(errs)
+	checkCaps := `SELECT cap FROM UNNEST($1::text[]) AS cap WHERE NOT cap =  ANY(ARRAY(SELECT c.name FROM capability AS c WHERE c.name = ANY($1)))`

Review comment:
       but actually, the database no longer stores a set of the Permissions that are valid, so this check just shouldn't be taking place.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	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 roleID int
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+	} else {
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to delete"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to delete"), nil)
+				return
+			}
+		}
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+	if _, err = result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	}
+	// TODO verify expected row count shouldn't be checked?
+	return nil, nil, http.StatusOK
+}
+
+// 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 roleV4 tc.RoleV4
+	var role TORole
+
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleName = roleV4.Name
+		roleDesc = roleV4.Description
+		privLevel = inf.User.PrivLevel
+		roleCapabilities = &roleV4.Permissions
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+	}
+
+	rows, err := tx.Query(createQuery(), roleName, roleDesc, privLevel)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("creating role: "+err.Error()))
+		return
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var throwaway interface{}
+		if err := rows.Scan(&roleID, &throwaway); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role create: scanning role ID: "+err.Error()))
+			return
+		}
+	}
+
+	if roleCapabilities != nil && 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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, 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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	params := make(map[string]dbhelpers.WhereColumnInfo, 0)
+	if version.Major >= 4 {
+		params["name"] = dbhelpers.WhereColumnInfo{Column: "name"}
+	} else {
+		params["name"] = dbhelpers.WhereColumnInfo{Column: "name"}
+		params["id"] = dbhelpers.WhereColumnInfo{Column: "id", Checker: api.IsInt}
+		params["privLevel"] = dbhelpers.WhereColumnInfo{Column: "priv_level", Checker: api.IsInt}
+	}
+
+	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 version.Major >= 4 {
+		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{}
+
+	var role tc.Role
+	roles := []tc.Role{}
+	if version.Major >= 4 {
+		for rows.Next() {
+			throwAway := new(interface{})
+			if err = rows.Scan(throwAway, &roleV4.Name, &roleV4.Description, throwAway, &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)
+	} else {
+		for rows.Next() {
+			throwAway := new(interface{})
+			var capabilities []string
+			if err = rows.Scan(&role.ID, &role.Name, &role.Description, &role.PrivLevel, throwAway, pq.Array(&capabilities)); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning RoleV11 row: %w", err))
+				return
+			}
+			role.Capabilities = &capabilities
+			roles = append(roles, role)
+		}
+		api.WriteResp(w, r, roles)
+	}
+}
+
+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`

Review comment:
       Nit: FROM and AS are SQL keywords, should be ALL_CAPS like the others

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}

Review comment:
       Should these conditions cause the request to fail?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] srijeet0406 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
srijeet0406 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r717967811



##########
File path: traffic_ops/traffic_ops_golang/user/user.go
##########
@@ -508,3 +578,483 @@ 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
+`
+
+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"}
+
+	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, errors.New("no ID exists for the supplied role name"))
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("role not found"), 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: %s\n)", 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."),
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, userResponse.Alerts, userResponse.Response)

Review comment:
       Do you mean a `201 Created`?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 edited a comment on pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 edited a comment on pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#issuecomment-931517873


   TP Role creation form appears to be broken: 
   ![image](https://user-images.githubusercontent.com/6013378/135502138-c3eff588-22a2-4af1-9e17-911511597f82.png)
   
   The controls are all broken functionally, as well as visually


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r710374698



##########
File path: docs/source/api/v4/roles.rst
##########
@@ -327,3 +327,5 @@ Response Structure
 		"text": "role was deleted.",
 		"level": "success"
 	}]}
+
+.. [#permissions] ``permissions`` cannot include permissions that are not included in the permissions of the requesting user.

Review comment:
       This footnote is never referenced, it should be applied to applicable request and/or response properties




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r712409328



##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +292,529 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {

Review comment:
       Based on `routes.go`, this handler is **only** for APIv4 Roles. I think your changes in this file are building off of my branch, but in my branch I removed the old CRUDer stuff and used each method function as the entrypoint for all versions of that request method.
   
   If you're going to leave the CRUDer in and still use it serve old API versions, then there's no reason to handle different API versions separately in these functions, because it actually only handles one API version.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +292,529 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		current, err := auth.GetCurrentUser(r.Context())
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+			return
+		}
+		missing := current.MissingPermissions(roleV4.Permissions...)
+		if len(missing) != 0 {
+			api.HandleErr(w, r, tx, http.StatusForbidden, fmt.Errorf("cannot request more than assigned permissions"), nil)

Review comment:
       this should ideally detail the specific Permissions that the user is lacking which would cause this request to succeed.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r718898212



##########
File path: traffic_ops/traffic_ops_golang/user/user.go
##########
@@ -884,7 +899,8 @@ func Create(w http.ResponseWriter, r *http.Request) {
 		Response: userV4,
 		Alerts:   tc.CreateAlerts(tc.SuccessLevel, "user was created."),
 	}
-	api.WriteAlertsObj(w, r, http.StatusOK, userResponse.Alerts, userResponse.Response)
+	w.Header().Set("Location", fmt.Sprintf("/api/4.0/users?id=%d", *userV4.ID))

Review comment:
       For future-proofing, this should probably just interpolate the `inf.Version`. I'm not sure off the top of my head if there's a `Version.String` or if that's just sitting in one of my branches somewhere, but if there is you probably wanna make use of it instead of using `inf.Version.Major` and `inf.Version.Minor` separately.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] srijeet0406 commented on pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
srijeet0406 commented on pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#issuecomment-931659279


   > TP Role creation form appears to be broken: ![image](https://user-images.githubusercontent.com/6013378/135502138-c3eff588-22a2-4af1-9e17-911511597f82.png)
   > 
   > The controls are all broken functionally, as well as visually
   
   Should be fixed now.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] srijeet0406 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
srijeet0406 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r715809328



##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %w", err), http.StatusInternalServerError
+	}

Review comment:
       Actually, it shouldn't matter if the affected rows is 0 or more than 0, hence, I think we don't need this check anymore at all.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r719581252



##########
File path: traffic_ops/traffic_ops_golang/user/user.go
##########
@@ -899,7 +899,8 @@ func Create(w http.ResponseWriter, r *http.Request) {
 		Response: userV4,
 		Alerts:   tc.CreateAlerts(tc.SuccessLevel, "user was created."),
 	}
-	w.Header().Set("Location", fmt.Sprintf("/api/4.0/users?id=%d", *userV4.ID))
+	version := fmt.Sprintf("%s.%s", strconv.FormatUint(inf.Version.Major, 10), strconv.FormatUint(inf.Version.Minor, 10))

Review comment:
       Since you're using `fmt.Sprintf` already, there's no need to bring `strconv` into it. Just use `%d` instead of `%s`.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] srijeet0406 commented on pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
srijeet0406 commented on pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#issuecomment-920123076


   > I was starting to review this when I realized it still targets API v5, but we're not doing that at this point, per [mailing list discussion](https://lists.apache.org/thread.html/rdf09f89d935597367cd2b5bbdeac20929066b5e2f286e7757cee39a0%40%3Cdev.trafficcontrol.apache.org%3E).
   
   Yeah I'm in the process of backing out the v5 changes. Should have it ready today.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r709349134



##########
File path: docs/source/api/v4/roles.rst
##########
@@ -83,19 +80,19 @@ 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: 1608
 
 	{ "response": [
 		{
-			"id": 4,
 			"name": "admin",
-			"description": "super-user",
-			"privLevel": 30,
-			"capabilities": [
+			"description": "Has access to everything.",
+			"permissions": [
 				"all-write",
-				"all-read"
-			]
+				"all-read",
+				...,

Review comment:
       this line is invalid JSON, which is breaking lexing and therefore syntax highlighting




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r715678650



##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())

Review comment:
       `github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api.APIInfo` contains a reference to the currently authenticated user - you can just use `inf.User` instead of making a new database transaction and query.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest

Review comment:
       This should be an internal server error, because that's what actually happened; there's nothing about the user's request that they can change to make the request succeed. Also client-facing errors should never use database table names, as that's an implementation detail of the API - but that doesn't matter for system errors, which this should be, so no need to change the actual message

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %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 roleID int
+	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
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))

Review comment:
       errors constructed from errors should wrap those underlying errors

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)

Review comment:
       `err` will be `nil` here, so this will just return
   ```json
   {
     "alerts": [
       {
         "level": "error",
         "text": "Bad Request"
       }
     ]
   }
   ```
   It should tell the user why the request failed

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {

Review comment:
       This should cast the `len` to an `int64` instead of `rows` to an `int` to avoid overflow errors from the (potential) loss of precision.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %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 roleID int
+	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
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))

Review comment:
       same as above RE: error-wrapping

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %w", err), http.StatusInternalServerError
+	}

Review comment:
       but actually it doesn't really seem to be checking the resulting rows affected, since that value is thrown away to `_` - should it be doing something with the real information returned by this call?

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}

Review comment:
       If `err` isn't `nil`, it doesn't mean the Role doesn't exist, right? It means an error occurred looking for it. So the response status code should be `500 Internal Server Error`, with no explicit error message sent back to the client.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %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 roleID int
+	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
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}

Review comment:
       If this is actually working already, it means that the DELETE is cascading to the Permission associations already, because it would violate the foreign key constraints on that "join table" for a row to exist with a Role ID that doesn't exist - seems like we don't need this whole block.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)

Review comment:
       same as above RE: error messages including ID when ID wasn't specified and matching other messages for the same error

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %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 roleID int
+	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
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	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 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}

Review comment:
       same as above RE: using the `inf.User` property

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), nil)

Review comment:
       The user didn't specify an ID - this error message should probably be the same as the above error for when the Role cannot be found. Not that I'm particularly attached to that wording either (personally I'd probably do something like `fmt.Errorf("no such Role: %s", roleName)`), but they should probably match.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %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 roleID int
+	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
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)

Review comment:
       I'm fairly sure the Role's ID isn't actually needed in the Go code anywhere, it's only used in queries - where it could just be fetched like: `... WHERE role = (SELECT id FROM public.role WHERE "name"=$1)`. You don't need to change it, but it gets rid of an extra DB query and the two error-handling blocks that follow.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %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 roleID int
+	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
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	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 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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() {
+		var throwaway interface{}
+		if err := rows.Scan(&roleID, &throwaway); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("role create: scanning role ID: %w", err))
+			return
+		}

Review comment:
       `createQuery` is only called in one place, because legacy handlers are using `insertQuery` instead. Therefore, there's no reason to have a throwaway here; the query can just return only and exactly what it needs to - which should include the value for the response's `lastUpdated` field, by the way.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time

Review comment:
       Instead of scanning into a `tc.TimeNoMod` and then storing a reference to a member struct thereof, why not just cut out the middle man and use a `time.Time`?

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %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 roleID int
+	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
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}

Review comment:
       same as above from the `PUT` handler that does the same thing

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %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 roleID int
+	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
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	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

Review comment:
       this being a pointer to a slice is only causing unnecessary referencing/dereferencing and `nil` checks - it should just be immediately set to the value of the requested Permissions when they're available.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %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 roleID int
+	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
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	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 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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() {
+		var throwaway interface{}
+		if err := rows.Scan(&roleID, &throwaway); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("role create: scanning role ID: %w", err))
+			return
+		}
+	}
+
+	if roleCapabilities != nil && 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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		capabilities = *roleCapabilities
+	}
+	roleResponse = tc.RoleV4{
+		Name:        roleName,
+		Permissions: capabilities,
+		Description: roleDesc,
+	}
+	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, 0)
+	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() {
+		throwAway := new(interface{})

Review comment:
       There's no reason we need a throwaway here, since the query used for this request is only used in one place it can just return only what it needs to return for the response.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +301,369 @@ 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, 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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
+	if roleName, ok = inf.Params["name"]; !ok {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	if err == nil && found == false {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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 role found with this ID"), nil)
+		return
+	}
+	lastUpdated := tc.TimeNoMod{}
+	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.Time
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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 := len(*permissions); int(rows) != expected {
+		return fmt.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected), nil, http.StatusBadRequest
+	}
+	return nil, nil, http.StatusOK
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, fmt.Errorf("deleting role capabilities: %w", err), http.StatusInternalServerError
+	}
+	if _, err := result.RowsAffected(); err != nil {
+		return nil, fmt.Errorf("could not check result after inserting role_capability relations: %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 roleID int
+	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
+	}
+	roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+		return
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	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 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
+	}
+	current, err := auth.GetCurrentUser(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	missing := current.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() {
+		var throwaway interface{}
+		if err := rows.Scan(&roleID, &throwaway); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("role create: scanning role ID: %w", err))
+			return
+		}
+	}
+
+	if roleCapabilities != nil && 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{}
+	var capabilities []string
+	if roleCapabilities != nil {
+		capabilities = *roleCapabilities
+	}
+	roleResponse = tc.RoleV4{
+		Name:        roleName,
+		Permissions: capabilities,
+		Description: roleDesc,
+	}
+	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, 0)
+	params["name"] = dbhelpers.WhereColumnInfo{Column: "name"}

Review comment:
       nit but since we know at east one entry is going in that map, using `0` in the `make` is just forcing at least one re-allocation.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] srijeet0406 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
srijeet0406 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r709398292



##########
File path: docs/source/api/v4/roles.rst
##########
@@ -83,19 +80,19 @@ 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: 1608
 
 	{ "response": [
 		{
-			"id": 4,
 			"name": "admin",
-			"description": "super-user",
-			"privLevel": 30,
-			"capabilities": [
+			"description": "Has access to everything.",
+			"permissions": [
 				"all-write",
-				"all-read"
-			]
+				"all-read",
+				...,

Review comment:
       Yeah I just used the `read-only` role instead, because that has lesser permissions than the `admin` role.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#issuecomment-931517873


   TP Role creation form appears to be broken: 
   ![image](https://user-images.githubusercontent.com/6013378/135502138-c3eff588-22a2-4af1-9e17-911511597f82.png)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r709729057



##########
File path: docs/source/api/v4/roles.rst
##########
@@ -300,11 +321,9 @@ 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".

Review comment:
       This should (hopefully) be changed to say that the Permissions granted cannot be any not granted to the requesting user.

##########
File path: lib/go-tc/users.go
##########
@@ -33,6 +33,152 @@ 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)

Review comment:
       GoDoc should end with punctuation

##########
File path: traffic_ops/testing/api/v4/roles_test.go
##########
@@ -63,30 +62,23 @@ 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)

Review comment:
       you're now capturing the error, but the value is never read after this assignment

##########
File path: traffic_ops/testing/api/v4/cachegroups_test.go
##########
@@ -101,26 +101,25 @@ 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)
+		fmt.Println(err)

Review comment:
       Looks like you left in a print statement you used for debugging - if the error was useful, it should probably get logged in the `t.Fatalf` call (and I can't imagine a more useful piece of information that could be logged in the event of a failure here)

##########
File path: lib/go-tc/users.go
##########
@@ -308,9 +617,10 @@ 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 Traffic Ops API version 4.0 variant of UserResponse for api major version 4 and the latest

Review comment:
       it's the latest 4.x variant, not specifically 4.0 - you probably just don't need to mention the version twice

##########
File path: lib/go-tc/users.go
##########
@@ -99,10 +238,120 @@ 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"`
+}
+
+func (u UserCurrentV4) Downgrade() UserCurrent {

Review comment:
       GoDoc?

##########
File path: lib/go-tc/users.go
##########
@@ -33,6 +33,152 @@ 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)

Review comment:
       GoDoc should end with punctuation

##########
File path: lib/go-tc/users.go
##########
@@ -334,6 +644,41 @@ type UserRegistrationRequest struct {
 	TenantID uint `json:"tenantId"`
 }
 
+// UserRegistrationRequestV4 is the alias for the UserRegistrationRequest for api major version 4 and the latest
+// minor version associated with it..

Review comment:
       nit: extra `.`

##########
File path: lib/go-tc/roles.go
##########
@@ -19,6 +28,70 @@ 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"`
+}
+
+func (role RoleV4) Validate() error {

Review comment:
       GoDoc?

##########
File path: lib/go-tc/users.go
##########
@@ -99,10 +238,120 @@ 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

Review comment:
       GoDoc should end with punctuation

##########
File path: lib/go-tc/users.go
##########
@@ -289,12 +585,25 @@ 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 api major version 4 and the latest
+// minor version associated with it..

Review comment:
       nit: extra `.`

##########
File path: traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
##########
@@ -1603,3 +1624,41 @@ 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).

Review comment:
       I think I was being a little cocky when I wrote this docstring - it's only guaranteed to be a valid WHERE clause when `where` is already a valid WHERE clause and `extra` is a valid extra condition statement.
   
   Which is important to note, because it is always `AND`ed - you can't add `OR some = 'thing'` or it'll break. Although tests should probably catch that pretty fast.
   
   I won't ask you to change the GoDoc, but it's a bit more optimistic than it should be.

##########
File path: lib/go-tc/users.go
##########
@@ -334,6 +644,41 @@ type UserRegistrationRequest struct {
 	TenantID uint `json:"tenantId"`
 }
 
+// UserRegistrationRequestV4 is the alias for the UserRegistrationRequest for api major version 4 and the latest
+// minor version associated with it..
+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 - despite being named "Role" - is actually merely the *ID* of a Role to give the new user.

Review comment:
       This is no longer true in APIv4 as of this PR

##########
File path: lib/go-tc/users.go
##########
@@ -33,6 +33,152 @@ 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
+}
+
+// Downgrade converts a UserV4 to a User as seen in API versions 2.x and 3.x
+func (u UserV4) DowngradeToLegacyUser() User {

Review comment:
       This looks like a duplicate of `UserV4.Downgrade`

##########
File path: 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 highest-privilege role assigned to this user

Review comment:
       "highest-privilege"? There's only one Role per user, I'm not sure why it reads like that.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r709204410



##########
File path: lib/go-tc/roles.go
##########
@@ -19,6 +28,70 @@ package tc
  * under the License.
  */
 
+// RoleV5 is an alias for RoleV50.

Review comment:
       While this is technically true, the purpose of the alias is to always point to the latest minor version for its associated major version.

##########
File path: lib/go-tc/roles.go
##########
@@ -19,6 +28,70 @@ package tc
  * under the License.
  */
 
+// RoleV5 is an alias for RoleV50.
+type RoleV5 RoleV50
+
+// RolesResponseV5 is a list of RoleV50 as a response.
+type RolesResponseV5 struct {
+	Response []RoleV5 `json:"response"`
+	Alerts
+}
+
+// RoleResponseV5 is a RoleV50 as a response.

Review comment:
       Nit but these are actually `RoleV5`s as responses.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 merged pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 merged pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] srijeet0406 commented on pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
srijeet0406 commented on pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#issuecomment-920206906


   Working on the test fixes.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r717942848



##########
File path: traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
##########
@@ -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, errors.New("querying role ID from name: " + err.Error())
+	}
+	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, errors.New("querying role name from ID: " + err.Error())

Review comment:
       Same as above RE: error-wrapping

##########
File path: traffic_ops/traffic_ops_golang/login/register.go
##########
@@ -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: %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
+		}
+	} 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, errors.New("no ID exists for the supplied role name"))

Review comment:
       the error shouldn't be dropped, it should be logged to aid in fixing problems

##########
File path: traffic_ops/traffic_ops_golang/user/user.go
##########
@@ -508,3 +578,483 @@ 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
+`
+
+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"}
+
+	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),
+	}

Review comment:
       At some point during creation or update of users, there must be a check that the Role is not changed to one with greater permissions than the requesting user. Old API versions also do `privCheck`, but should also be doing the permissions check (if Permissions are enabled in the configuration? or just always?) so that privileges cannot be escalated by just using older API versions.

##########
File path: traffic_ops/traffic_ops_golang/user/user.go
##########
@@ -508,3 +578,483 @@ 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
+`
+
+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"}
+
+	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, errors.New("no ID exists for the supplied role name"))
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("role not found"), 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: %s\n)", 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."),
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, userResponse.Alerts, userResponse.Response)

Review comment:
       status code for object creation should be `200 OK`, ideally with an HTTP `Location` header set to a URI that can be used to retrieve a representation of the created object.

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +300,332 @@ 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, 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
+	}
+	roleDesc = roleV4.Description
+	roleCapabilities = roleV4.Permissions
+	if roleName, ok = inf.Params["name"]; !ok {

Review comment:
       if a string appears in the "required Parameters" argument of `api.NewInfo`, it'll do this check for you.
   
   So instead of
   ```go
   inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
   ```
   doing
   ```go
   inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
   ```
   will guarantee that `inf.Params` contains `"name"` as a key (or the proper error(s)/response code will be returned from `NewInfo`).

##########
File path: traffic_ops/traffic_ops_golang/user/user.go
##########
@@ -508,3 +578,483 @@ 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
+`
+
+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"}
+
+	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, errors.New("no ID exists for the supplied role name"))

Review comment:
       this shouldn't swallow the error

##########
File path: traffic_ops/traffic_ops_golang/login/register.go
##########
@@ -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: %v", req.Role, err)

Review comment:
       constructing an error from an error should wrap

##########
File path: traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
##########
@@ -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, errors.New("querying role ID from name: " + err.Error())

Review comment:
       Errors constructed from errors should wrap

##########
File path: traffic_ops/traffic_ops_golang/user/user.go
##########
@@ -508,3 +578,483 @@ 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
+`
+
+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"}
+
+	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, errors.New("no ID exists for the supplied role name"))
+		return
+	} else if !ok {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("role not found"), 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: %s\n)", err))

Review comment:
       error strings should not include newlines




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r711267698



##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +317,502 @@ 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 privLevel int
+	var roleCapabilities *[]string
+	var roleV4 tc.RoleV4
+	var role TORole
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 4 {
+		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
+		}
+		roleDesc = roleV4.Description
+		roleCapabilities = &roleV4.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV4.LastUpdated = &lastUpdated.Time
+		}
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := role.validate(inf.Tx); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+				return
+			}
+		}
+		if privLevel > inf.User.PrivLevel {
+			api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+			return
+		}
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+			return
+		}
+		rows.Close()
+	}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 4 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV4{
+			Name:        roleName,
+			Permissions: capabilities,
+			Description: roleDesc,
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	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 id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	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 roleID int
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 4 {
+		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
+		}
+		roleID, ok, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		} else if !ok {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no such role"), nil)
+			return
+		}
+	} else {
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to delete"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to delete"), nil)
+				return
+			}
+		}
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	changeLogMsg := fmt.Sprintf("ROLE: %s, ID: %d, ACTION: Deleted Role", roleName, roleID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+	if _, err = result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	}

Review comment:
       Should this cause the request to fail?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] srijeet0406 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
srijeet0406 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r693055877



##########
File path: lib/go-tc/users.go
##########
@@ -33,6 +33,249 @@ import (
 	"github.com/go-ozzo/ozzo-validation/is"
 )
 
+// A UserV50 is a representation of a Traffic Ops user as it appears in version
+// 5.0 of Traffic Ops's API.
+type UserV50 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"`
+}
+
+// UsersResponseV50 is the type of a response from Traffic Ops to requests made
+// to /users which return more than one user.
+type UsersResponseV50 struct {
+	Response []UserV50 `json:"response"`
+	Alerts
+}
+
+// UserResponseV50 is the type of a response from Traffic Ops to requests made
+// to /users which return one user.
+type UserResponseV50 struct {
+	Response UserV50 `json:"response"`
+	Alerts
+}
+
+// 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
+}
+
+// UpgradeFromLegacyUser converts a User to a UserV50 (as seen in API versions 5.x)
+func (u User) UpgradeFromLegacyUser() UserV50 {
+	var ret UserV50
+	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
+}
+
+// UpgradeFromUserV40 converts a UserV40 to a UserV50 (as seen in API versions 5.x)
+func (u UserV40) UpgradeFromUserV40() UserV50 {
+	var ret UserV50
+	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.ChangeLogCount = copyIntIfNotNil(u.ChangeLogCount)
+	if u.LastAuthenticated != nil {
+		ret.LastAuthenticated = *u.LastAuthenticated
+	}
+	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 UserV50 to a UserV40 (as seen in API versions 4.x) and User (as seen in API versions < 4.0)
+func (u UserV50) Downgrade() (UserV40, User) {

Review comment:
       Since most of the struct params in `User` and `UserV40` are the same, I took care of both downgrades in one function, instead of duplicating the code. I don't mind changing it to what you're suggesting. Thoughts?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r692377505



##########
File path: lib/go-tc/roles.go
##########
@@ -19,6 +19,55 @@ package tc
  * under the License.
  */
 
+// RolesResponseV5 is a list of RoleV50 as a response.
+type RolesResponseV5 struct {
+	Response []RoleV50 `json:"response"`
+	Alerts
+}
+
+// RoleResponseV5 is a RoleV50 as a response.
+type RoleResponseV5 struct {
+	Response RoleV50 `json:"response"`
+	Alerts
+}
+
+// RoleV50 is the structure used to depict roles in API v5.0

Review comment:
       GoDoc should end with punctuation

##########
File path: lib/go-tc/users.go
##########
@@ -33,6 +33,249 @@ import (
 	"github.com/go-ozzo/ozzo-validation/is"
 )
 
+// A UserV50 is a representation of a Traffic Ops user as it appears in version
+// 5.0 of Traffic Ops's API.
+type UserV50 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"`
+}
+
+// UsersResponseV50 is the type of a response from Traffic Ops to requests made
+// to /users which return more than one user.
+type UsersResponseV50 struct {
+	Response []UserV50 `json:"response"`
+	Alerts
+}
+
+// UserResponseV50 is the type of a response from Traffic Ops to requests made
+// to /users which return one user.
+type UserResponseV50 struct {
+	Response UserV50 `json:"response"`
+	Alerts
+}

Review comment:
       same as above RE: V5 type allias

##########
File path: lib/go-tc/roles.go
##########
@@ -19,6 +19,55 @@ package tc
  * under the License.
  */
 
+// RolesResponseV5 is a list of RoleV50 as a response.
+type RolesResponseV5 struct {
+	Response []RoleV50 `json:"response"`
+	Alerts
+}
+
+// RoleResponseV5 is a RoleV50 as a response.
+type RoleResponseV5 struct {
+	Response RoleV50 `json:"response"`
+	Alerts
+}
+
+// RoleV50 is the structure used to depict roles in API v5.0
+type RoleV50 struct {
+	Name        *string    `json:"name" db:"name"`
+	Permissions []string   `json:"permissions" db:"permissions"`
+	Description *string    `json:"description" db:"description"`
+	LastUpdated *TimeNoMod `json:"lastUpdated,omitempty" db:"last_updated"`
+}

Review comment:
       We should have a type for `RoleV5` that, for now, is just an alias of `RoleV50`, and `RolesResponseV5` and `RoleResponseV5` should use that instead of `RoleV50`. That way, if we have to make a `RoleV51` we can update the alias and don't need to make breaking changes to Go client call signatures.

##########
File path: traffic_ops/testing/api/v4/cdn_locks_test.go
##########
@@ -219,8 +218,7 @@ func AdminCdnLocks(t *testing.T) {
 	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.Error())

Review comment:
       nit but you don't need to call `.Error` when you're formatting with `%v`

##########
File path: lib/go-tc/users.go
##########
@@ -334,6 +665,15 @@ type UserRegistrationRequest struct {
 	TenantID uint `json:"tenantId"`
 }
 
+// UserRegistrationRequestV50 is the request submitted by operators when they want to register a new
+// user in api V5.
+type UserRegistrationRequestV50 struct {
+	Email rfc.EmailAddress `json:"email"`
+	// Role - despite being named "Role" - is actually merely the *ID* of a Role to give the new user.
+	Role     string `json:"role"`
+	TenantID uint   `json:"tenantId"`
+}
+

Review comment:
       Same as above RE: V5 alias

##########
File path: lib/go-tc/roles.go
##########
@@ -19,6 +19,55 @@ package tc
  * under the License.
  */
 
+// RolesResponseV5 is a list of RoleV50 as a response.
+type RolesResponseV5 struct {
+	Response []RoleV50 `json:"response"`
+	Alerts
+}
+
+// RoleResponseV5 is a RoleV50 as a response.
+type RoleResponseV5 struct {
+	Response RoleV50 `json:"response"`
+	Alerts
+}
+
+// RoleV50 is the structure used to depict roles in API v5.0
+type RoleV50 struct {
+	Name        *string    `json:"name" db:"name"`
+	Permissions []string   `json:"permissions" db:"permissions"`
+	Description *string    `json:"description" db:"description"`
+	LastUpdated *TimeNoMod `json:"lastUpdated,omitempty" db:"last_updated"`

Review comment:
       Since `Name` and `Description` are not actually allowed to be `null` or undefined, do those need to be pointers?
   Also, we should use a `time.Time` here instead of a `TimeNoMod` so that we can have RFC3339-formatted timestamps as per the API guidelines.

##########
File path: lib/go-tc/users.go
##########
@@ -33,6 +33,249 @@ import (
 	"github.com/go-ozzo/ozzo-validation/is"
 )
 
+// A UserV50 is a representation of a Traffic Ops user as it appears in version
+// 5.0 of Traffic Ops's API.
+type UserV50 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"`
+}
+
+// UsersResponseV50 is the type of a response from Traffic Ops to requests made
+// to /users which return more than one user.
+type UsersResponseV50 struct {
+	Response []UserV50 `json:"response"`
+	Alerts
+}
+
+// UserResponseV50 is the type of a response from Traffic Ops to requests made
+// to /users which return one user.
+type UserResponseV50 struct {
+	Response UserV50 `json:"response"`
+	Alerts
+}
+
+// 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
+}
+
+// UpgradeFromLegacyUser converts a User to a UserV50 (as seen in API versions 5.x)
+func (u User) UpgradeFromLegacyUser() UserV50 {

Review comment:
       nit: Since this can be only be called from a `User`, we already know it's upgrading "from a legacy user", those three words make the name longer without giving us any extra information. Could just be `Upgrade`.

##########
File path: lib/go-tc/users.go
##########
@@ -105,6 +348,33 @@ type UserCurrentV40 struct {
 	LastAuthenticated *time.Time `json:"lastAuthenticated" db:"last_authenticated"`
 }
 
+// UserCurrentV50 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 UserCurrentV50 struct {
+	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"`

Review comment:
       `UserV50` has non-pointer types for `LastUpdated`, `LastAuthenticated`, `Role`, and `UserName`, and I think those ought to match here

##########
File path: lib/go-tc/users.go
##########
@@ -33,6 +33,249 @@ import (
 	"github.com/go-ozzo/ozzo-validation/is"
 )
 
+// A UserV50 is a representation of a Traffic Ops user as it appears in version
+// 5.0 of Traffic Ops's API.
+type UserV50 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"`
+}
+
+// UsersResponseV50 is the type of a response from Traffic Ops to requests made
+// to /users which return more than one user.
+type UsersResponseV50 struct {
+	Response []UserV50 `json:"response"`
+	Alerts
+}
+
+// UserResponseV50 is the type of a response from Traffic Ops to requests made
+// to /users which return one user.
+type UserResponseV50 struct {
+	Response UserV50 `json:"response"`
+	Alerts
+}
+
+// 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
+}
+
+// UpgradeFromLegacyUser converts a User to a UserV50 (as seen in API versions 5.x)
+func (u User) UpgradeFromLegacyUser() UserV50 {
+	var ret UserV50
+	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
+}
+
+// UpgradeFromUserV40 converts a UserV40 to a UserV50 (as seen in API versions 5.x)
+func (u UserV40) UpgradeFromUserV40() UserV50 {
+	var ret UserV50
+	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.ChangeLogCount = copyIntIfNotNil(u.ChangeLogCount)
+	if u.LastAuthenticated != nil {
+		ret.LastAuthenticated = *u.LastAuthenticated
+	}
+	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 UserV50 to a UserV40 (as seen in API versions 4.x) and User (as seen in API versions < 4.0)
+func (u UserV50) Downgrade() (UserV40, User) {

Review comment:
       If we already have a `DowngradeToLegacy`, why the second return value for a legacy `User` here? Would it help if, instead of having ways to upgrade from/downgrade to a regular `User` straight from a `UserV40` if there was a `UserV50.Downgrade` that downgrades to `UserV40`, a `UserV40.Downgrade` that downgrades a V40 to a `User`, a `User.Upgrade` that upgrades a `User` to V40, and a `UserV40.Upgrade` that upgrades a `UserV40` to V5?
   
   That way, if you need a UserV40 and a User generated from the same UserV50, then you can do `userV4 = userV5.Downgrade()` and then when you need a `User` you can just `Downgrade` again.

##########
File path: lib/go-tc/users.go
##########
@@ -33,6 +33,249 @@ import (
 	"github.com/go-ozzo/ozzo-validation/is"
 )
 
+// A UserV50 is a representation of a Traffic Ops user as it appears in version
+// 5.0 of Traffic Ops's API.
+type UserV50 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"`
+}
+
+// UsersResponseV50 is the type of a response from Traffic Ops to requests made
+// to /users which return more than one user.
+type UsersResponseV50 struct {
+	Response []UserV50 `json:"response"`
+	Alerts
+}
+
+// UserResponseV50 is the type of a response from Traffic Ops to requests made
+// to /users which return one user.
+type UserResponseV50 struct {
+	Response UserV50 `json:"response"`
+	Alerts
+}
+
+// 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
+}
+
+// UpgradeFromLegacyUser converts a User to a UserV50 (as seen in API versions 5.x)
+func (u User) UpgradeFromLegacyUser() UserV50 {
+	var ret UserV50
+	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
+}
+
+// UpgradeFromUserV40 converts a UserV40 to a UserV50 (as seen in API versions 5.x)
+func (u UserV40) UpgradeFromUserV40() UserV50 {

Review comment:
       nit: Since this can be only be called from a `UserV40`, we already know it's upgrading "from a UserV40", those three words make the name longer without giving us any extra information. Could just be `Upgrade`.

##########
File path: lib/go-tc/users.go
##########
@@ -136,9 +406,49 @@ type CurrentUserUpdateRequestUser struct {
 	Username           json.RawMessage `json:"username"`
 }
 
+func (u UserCurrentV40) UpgradeUserCurrent() UserCurrentV50 {

Review comment:
       GoDoc?
   
   Also, nit: Since this can be only be called from a `UserCurrentV40`, we already know it's upgrading "from a UserCurrentV40", those two words make the name longer without giving us any extra information. Could just be `Upgrade`.

##########
File path: traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
##########
@@ -1603,3 +1624,35 @@ 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).
+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

Review comment:
       GoDoc should end with punctuation.
   
   Also, most functions like this return a `bool` indicating whether or not the identified object could be found, should this one?

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +291,522 @@ RETURNING id, last_updated`
 func deleteQuery() string {
 	return `DELETE FROM role WHERE id = :id`
 }
+
+func Update(w http.ResponseWriter, r *http.Request) {
+	var roleID int
+	var roleName string
+	var roleDesc string
+	var privLevel int
+	var roleCapabilities *[]string
+	var roleV50 tc.RoleV50
+	var role tc.Role
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 5 {
+		if err := json.NewDecoder(r.Body).Decode(&roleV50); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := Validate(inf.Tx, role, roleV50, version); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleDesc = *roleV50.Description
+		roleCapabilities = &roleV50.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV50.LastUpdated = &lastUpdated
+		}
+	}
+	//else {
+	//	if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+	//		return
+	//	}
+	//	if err := Validate(inf.Tx, role, roleV50, version); err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+	//		return
+	//	}
+	//	roleName = *role.Name
+	//	roleDesc = *role.Description
+	//	privLevel = *role.PrivLevel
+	//	roleCapabilities = role.Capabilities
+	//	if roleIDParam, ok := inf.Params["id"]; !ok {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+	//		return
+	//	} else {
+	//		roleID, err = strconv.Atoi(roleIDParam)
+	//		if err != nil {
+	//			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+	//			return
+	//		}
+	//	}
+	//	if privLevel > inf.User.PrivLevel {
+	//		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+	//		return
+	//	}
+	//	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	//	if err == nil && found == false {
+	//		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+	//	if err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+	//		return
+	//	}
+	//	rows.Close()
+	//}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 5 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV50{
+			Name:        util.StrPtr(roleName),
+			Permissions: capabilities,
+			Description: util.StrPtr(roleDesc),
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, alerts, roleResponse)
+}
+
+func deleteRoleQuery() string {
+	return `DELETE FROM role WHERE id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	return nil, nil, http.StatusOK
+}
+
+func Validate(tx *sqlx.Tx, role tc.Role, roleV50 tc.RoleV50, version *api.Version) error {

Review comment:
       Does this need to be exported out of the package?
   
   Instead of using different arguments based on the API version, could we not just have a different validation func for each version of a Role? I think what you're trying to do here is something like
   ```typescript
   export function Validate(tx: sqlx.Tx, role: tc.Role | tc.RoleV50, version: api.Version) {
   ```
   but the reality is that things like that don't work well in Go.

##########
File path: traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
##########
@@ -1603,3 +1624,35 @@ 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).
+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, error) {
+	var id int
+	if err := tx.QueryRow(`SELECT id FROM role WHERE name = $1`, roleName).Scan(&id); err != nil {
+		return -1, errors.New("querying role ID from name: " + err.Error())
+	}
+	return id, nil
+}
+
+// GetRoleNameFromID returns the name of the role associated with the supplied ID

Review comment:
       same as the above comments on GetRoleIDFromName

##########
File path: traffic_ops/traffic_ops_golang/login/register.go
##########
@@ -164,6 +164,15 @@ func RegisterUser(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
 		return
 	}
+	// ToDo: uncomment this once the perm based roles and config options are implemented
+	//var reqV50 tc.UserRegistrationRequestV50
+	//if inf.Version.Major >= 5 {
+	//	if userErr = api.Parse(r.Body, tx, &reqV50); userErr != nil {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil)
+	//		return
+	//	}
+	//	tenantID = reqV50.TenantID
+	//}

Review comment:
       Why can't we do this now? Or if you want to do it in another PR, why leave the comment in?

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +291,522 @@ RETURNING id, last_updated`
 func deleteQuery() string {
 	return `DELETE FROM role WHERE id = :id`
 }
+
+func Update(w http.ResponseWriter, r *http.Request) {
+	var roleID int
+	var roleName string
+	var roleDesc string
+	var privLevel int
+	var roleCapabilities *[]string
+	var roleV50 tc.RoleV50
+	var role tc.Role
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 5 {
+		if err := json.NewDecoder(r.Body).Decode(&roleV50); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := Validate(inf.Tx, role, roleV50, version); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleDesc = *roleV50.Description
+		roleCapabilities = &roleV50.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV50.LastUpdated = &lastUpdated
+		}
+	}
+	//else {
+	//	if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+	//		return
+	//	}
+	//	if err := Validate(inf.Tx, role, roleV50, version); err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+	//		return
+	//	}
+	//	roleName = *role.Name
+	//	roleDesc = *role.Description
+	//	privLevel = *role.PrivLevel
+	//	roleCapabilities = role.Capabilities
+	//	if roleIDParam, ok := inf.Params["id"]; !ok {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+	//		return
+	//	} else {
+	//		roleID, err = strconv.Atoi(roleIDParam)
+	//		if err != nil {
+	//			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+	//			return
+	//		}
+	//	}
+	//	if privLevel > inf.User.PrivLevel {
+	//		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+	//		return
+	//	}
+	//	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	//	if err == nil && found == false {
+	//		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+	//	if err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+	//		return
+	//	}
+	//	rows.Close()
+	//}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 5 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV50{
+			Name:        util.StrPtr(roleName),
+			Permissions: capabilities,
+			Description: util.StrPtr(roleDesc),
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, alerts, roleResponse)
+}
+
+func deleteRoleQuery() string {
+	return `DELETE FROM role WHERE id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	return nil, nil, http.StatusOK
+}
+
+func Validate(tx *sqlx.Tx, role tc.Role, roleV50 tc.RoleV50, version *api.Version) error {
+	var capabilities *[]string
+	errs := make(map[string]error)
+	if version.Major >= 5 {
+		errs = validation.Errors{
+			"name":        validation.Validate(roleV50.Name, validation.Required),
+			"description": validation.Validate(roleV50.Description, validation.Required),
+		}
+		capabilities = &roleV50.Permissions
+	} else {
+		errs = validation.Errors{
+			"name":        validation.Validate(role.Name, validation.Required),
+			"description": validation.Validate(role.Description, validation.Required),
+			"privLevel":   validation.Validate(role.PrivLevel, validation.Required),
+		}
+		capabilities = role.Capabilities
+	}
+
+	errsToReturn := tovalidate.ToErrors(errs)
+	checkCaps := `SELECT cap FROM UNNEST($1::text[]) AS cap WHERE NOT cap =  ANY(ARRAY(SELECT c.name FROM capability AS c WHERE c.name = ANY($1)))`

Review comment:
       We shouldn't do this, the source of truth for what Permissions exist is just the API documentation itself, not the database. That's why I got rid of the foreign key constraint, which enforced this at a database level.

##########
File path: lib/go-tc/users.go
##########
@@ -105,6 +348,33 @@ type UserCurrentV40 struct {
 	LastAuthenticated *time.Time `json:"lastAuthenticated" db:"last_authenticated"`
 }
 
+// UserCurrentV50 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 UserCurrentV50 struct {
+	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"`

Review comment:
       Also same as above RE: V5 alias

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +291,522 @@ RETURNING id, last_updated`
 func deleteQuery() string {
 	return `DELETE FROM role WHERE id = :id`
 }
+
+func Update(w http.ResponseWriter, r *http.Request) {
+	var roleID int
+	var roleName string
+	var roleDesc string
+	var privLevel int
+	var roleCapabilities *[]string
+	var roleV50 tc.RoleV50
+	var role tc.Role
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 5 {
+		if err := json.NewDecoder(r.Body).Decode(&roleV50); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := Validate(inf.Tx, role, roleV50, version); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleDesc = *roleV50.Description
+		roleCapabilities = &roleV50.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV50.LastUpdated = &lastUpdated
+		}
+	}
+	//else {
+	//	if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+	//		return
+	//	}
+	//	if err := Validate(inf.Tx, role, roleV50, version); err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+	//		return
+	//	}
+	//	roleName = *role.Name
+	//	roleDesc = *role.Description
+	//	privLevel = *role.PrivLevel
+	//	roleCapabilities = role.Capabilities
+	//	if roleIDParam, ok := inf.Params["id"]; !ok {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+	//		return
+	//	} else {
+	//		roleID, err = strconv.Atoi(roleIDParam)
+	//		if err != nil {
+	//			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+	//			return
+	//		}
+	//	}
+	//	if privLevel > inf.User.PrivLevel {
+	//		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+	//		return
+	//	}
+	//	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	//	if err == nil && found == false {
+	//		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+	//	if err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+	//		return
+	//	}
+	//	rows.Close()
+	//}

Review comment:
       doesn't this being commented-out break all PUT requests to `/roles` in API versions earlier than 5.0?

##########
File path: traffic_ops/traffic_ops_golang/user/user.go
##########
@@ -508,3 +519,426 @@ 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 + `
+	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
+`
+
+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
+}
+
+func read(rows *sqlx.Rows) ([]tc.UserV50, error) {
+	if rows == nil {
+		return nil, errors.New("cannot read from nil rows")
+	}
+
+	users := []tc.UserV50{}
+	for rows.Next() {
+		var user tc.UserV50
+		if err := rows.StructScan(&user); err != nil {
+			return nil, fmt.Errorf("scanning UserV5 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) {
+	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"}
+
+	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: %v\n", err))

Review comment:
       Errors should not contain newlines, also formatting for errors should use `%w` instead of `%v` so that it wraps

##########
File path: traffic_ops/traffic_ops_golang/role/roles.go
##########
@@ -273,3 +291,522 @@ RETURNING id, last_updated`
 func deleteQuery() string {
 	return `DELETE FROM role WHERE id = :id`
 }
+
+func Update(w http.ResponseWriter, r *http.Request) {
+	var roleID int
+	var roleName string
+	var roleDesc string
+	var privLevel int
+	var roleCapabilities *[]string
+	var roleV50 tc.RoleV50
+	var role tc.Role
+	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()
+
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+	tx := inf.Tx.Tx
+	if version.Major >= 5 {
+		if err := json.NewDecoder(r.Body).Decode(&roleV50); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := Validate(inf.Tx, role, roleV50, version); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleDesc = *roleV50.Description
+		roleCapabilities = &roleV50.Permissions
+		if roleName, ok = inf.Params["name"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleID, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		}
+
+		existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+		if err == nil && found == false {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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, errors.New("updating role: "+err.Error()))
+			return
+		}
+		defer rows.Close()
+		if !rows.Next() {
+			api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with this ID"), nil)
+			return
+		}
+		lastUpdated := tc.TimeNoMod{}
+		for rows.Next() {
+			if err := rows.Scan(&lastUpdated); err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning lastUpdated from role update: "+err.Error()))
+				return
+			}
+			roleV50.LastUpdated = &lastUpdated
+		}
+	}
+	//else {
+	//	if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+	//		return
+	//	}
+	//	if err := Validate(inf.Tx, role, roleV50, version); err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("name and/or description and/or privLevel can not be empty"), nil)
+	//		return
+	//	}
+	//	roleName = *role.Name
+	//	roleDesc = *role.Description
+	//	privLevel = *role.PrivLevel
+	//	roleCapabilities = role.Capabilities
+	//	if roleIDParam, ok := inf.Params["id"]; !ok {
+	//		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to update"), nil)
+	//		return
+	//	} else {
+	//		roleID, err = strconv.Atoi(roleIDParam)
+	//		if err != nil {
+	//			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to update"), nil)
+	//			return
+	//		}
+	//	}
+	//	if privLevel > inf.User.PrivLevel {
+	//		api.HandleErr(w, r, tx, http.StatusForbidden, errors.New("can not create a role with a higher priv level than your own"), nil)
+	//		return
+	//	}
+	//	existingLastUpdated, found, err := api.GetLastUpdated(inf.Tx, roleID, "role")
+	//	if err == nil && found == false {
+	//		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no role found with that ID"), 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(updateLegacyRoleQuery(), roleName, roleDesc, roleID)
+	//	if err != nil {
+	//		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("updating role here: "+err.Error()))
+	//		return
+	//	}
+	//	rows.Close()
+	//}
+
+	if roleCapabilities != nil && *roleCapabilities != nil {
+		userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+		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{}
+	if version.Major >= 5 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV50{
+			Name:        util.StrPtr(roleName),
+			Permissions: capabilities,
+			Description: util.StrPtr(roleDesc),
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, alerts, roleResponse)
+}
+
+func deleteRoleQuery() string {
+	return `DELETE FROM role WHERE id = $1`
+}
+
+func readQuery() string {
+	return `SELECT
+id,
+name,
+description,
+priv_level,
+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, errors.New("creating role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if rows, err := result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	} else if expected := len(*permissions); int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	return nil, nil, http.StatusOK
+}
+
+func Validate(tx *sqlx.Tx, role tc.Role, roleV50 tc.RoleV50, version *api.Version) error {
+	var capabilities *[]string
+	errs := make(map[string]error)
+	if version.Major >= 5 {
+		errs = validation.Errors{
+			"name":        validation.Validate(roleV50.Name, validation.Required),
+			"description": validation.Validate(roleV50.Description, validation.Required),
+		}
+		capabilities = &roleV50.Permissions
+	} else {
+		errs = validation.Errors{
+			"name":        validation.Validate(role.Name, validation.Required),
+			"description": validation.Validate(role.Description, validation.Required),
+			"privLevel":   validation.Validate(role.PrivLevel, validation.Required),
+		}
+		capabilities = role.Capabilities
+	}
+
+	errsToReturn := tovalidate.ToErrors(errs)
+	checkCaps := `SELECT cap FROM UNNEST($1::text[]) AS cap WHERE NOT cap =  ANY(ARRAY(SELECT c.name FROM capability AS c WHERE c.name = ANY($1)))`
+	var badCaps []string
+	if tx != nil {
+		err := tx.Select(&badCaps, checkCaps, pq.Array(capabilities))
+		if err != nil {
+			log.Errorf("got error from selecting bad capabilities: %v", err)
+			return err
+		}
+		if len(badCaps) > 0 {
+			errsToReturn = append(errsToReturn, fmt.Errorf("can not add non-existent capabilities: %v", badCaps))
+		}
+	}
+	return util.JoinErrs(errsToReturn)
+}
+
+func Delete(w http.ResponseWriter, r *http.Request) {
+	var roleName string
+	var ok bool
+	var roleID int
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 5 {
+		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
+		}
+		roleID, err = dbhelpers.GetRoleIDFromName(tx, roleName)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("no ID exists for the supplied role name"), nil)
+			return
+		}
+	} else {
+		if roleIDParam, ok := inf.Params["id"]; !ok {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply a role ID to delete"), nil)
+			return
+		} else {
+			roleID, err = strconv.Atoi(roleIDParam)
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("must supply an integral role ID to delete"), nil)
+				return
+			}
+		}
+	}
+
+	assignedUsers := 0
+	if err := inf.Tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", roleID); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role delete counting assigned users: "+err.Error()))
+		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(), roleID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("deleting role: "+err.Error()))
+		return
+	}
+	rows.Close()
+	userErr, sysErr, errCode = deleteRoleCapabilityAssociations(inf.Tx, roleID)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "role was deleted.")
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+}
+
+func deleteRoleCapabilityAssociations(tx *sqlx.Tx, roleID int) (error, error, int) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), roleID)
+	if err != nil {
+		return nil, errors.New("deleting role capabilities: " + err.Error()), http.StatusInternalServerError
+	}
+	if _, err = result.RowsAffected(); err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	}
+	// TODO verify expected row count shouldn't be checked?
+	return nil, nil, http.StatusOK
+}
+
+func Create(w http.ResponseWriter, r *http.Request) {
+	var roleID int
+	var roleName string
+	var roleDesc string
+	var privLevel int
+	var roleCapabilities *[]string
+	var roleV50 tc.RoleV50
+	var role tc.Role
+
+	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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	if version.Major >= 5 {
+		if err := json.NewDecoder(r.Body).Decode(&roleV50); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := Validate(inf.Tx, role, roleV50, version); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleName = *roleV50.Name
+		roleDesc = *roleV50.Description
+		privLevel = inf.User.PrivLevel
+		roleCapabilities = &roleV50.Permissions
+	} else {
+		if err := json.NewDecoder(r.Body).Decode(&role); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		if err := Validate(inf.Tx, role, roleV50, version); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		roleName = *role.Name
+		roleDesc = *role.Description
+		privLevel = *role.PrivLevel
+		roleCapabilities = role.Capabilities
+	}
+
+	rows, err := tx.Query(createQuery(), roleName, roleDesc, privLevel)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("creating role: "+err.Error()))
+		return
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var throwaway interface{}
+		if err := rows.Scan(&roleID, &throwaway); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("role create: scanning role ID: "+err.Error()))
+			return
+		}
+	}
+
+	if roleCapabilities != nil && 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{}
+	if version.Major >= 5 {
+		var capabilities []string
+		if roleCapabilities != nil {
+			capabilities = *roleCapabilities
+		}
+		roleResponse = tc.RoleV50{
+			Name:        util.StrPtr(roleName),
+			Permissions: capabilities,
+			Description: util.StrPtr(roleDesc),
+		}
+	} else {
+		roleResponse = tc.Role{
+			RoleV11: tc.RoleV11{
+				ID:          util.IntPtr(roleID),
+				Name:        util.StrPtr(roleName),
+				Description: util.StrPtr(roleDesc),
+				PrivLevel:   util.IntPtr(privLevel),
+			},
+			Capabilities: roleCapabilities,
+		}
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, alerts, roleResponse)
+}
+
+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
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
+
+	params := make(map[string]dbhelpers.WhereColumnInfo, 0)
+	if version.Major >= 5 {
+		params["name"] = dbhelpers.WhereColumnInfo{Column: "name"}
+	} else {
+		params["name"] = dbhelpers.WhereColumnInfo{Column: "name"}
+		params["id"] = dbhelpers.WhereColumnInfo{Column: "id", Checker: api.IsInt}
+		params["privLevel"] = dbhelpers.WhereColumnInfo{Column: "priv_level", Checker: api.IsInt}
+	}
+
+	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 version.Major >= 5 {
+		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)
+			api.WriteResp(w, r, []interface{}{})

Review comment:
       Is this `api.WriteResp` call actually necessary? I see other IMS hits doing it, but idk why, since the whole point is that clients will just ignore it and use their cache.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] srijeet0406 commented on pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
srijeet0406 commented on pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#issuecomment-906897976


   > If the TP tests don't pass this time we'll need to consider it a real failure - also, conflicts.
   
   Everything seems to be passing now, and the merge conflicts have been resolved.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [trafficcontrol] ocket8888 commented on a change in pull request #6124: Role and User struct changes for permission based roles

Posted by GitBox <gi...@apache.org>.
ocket8888 commented on a change in pull request #6124:
URL: https://github.com/apache/trafficcontrol/pull/6124#discussion_r718898212



##########
File path: traffic_ops/traffic_ops_golang/user/user.go
##########
@@ -884,7 +899,8 @@ func Create(w http.ResponseWriter, r *http.Request) {
 		Response: userV4,
 		Alerts:   tc.CreateAlerts(tc.SuccessLevel, "user was created."),
 	}
-	api.WriteAlertsObj(w, r, http.StatusOK, userResponse.Alerts, userResponse.Response)
+	w.Header().Set("Location", fmt.Sprintf("/api/4.0/users?id=%d", *userV4.ID))

Review comment:
       For future-proofing, this should probably just interpolate the `inf.Version`. I'm not sure off the top of my head if there's a `Version.String` or if that's just sitting in one of my branches somewhere, but if there is you probably wanna make use of it instead of using `inf.Version.Major` and `inf.Version.Minor` separately.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org