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 2018/05/17 02:25:46 UTC

[GitHub] mitchell852 closed pull request #2108: create roles crud endpoints in golang

mitchell852 closed pull request #2108: create roles crud endpoints in golang
URL: https://github.com/apache/incubator-trafficcontrol/pull/2108
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/docs/source/api/v12/api_capability.rst b/docs/source/api/v12/api_capability.rst
index 794dd4faf..618cd528c 100644
--- a/docs/source/api/v12/api_capability.rst
+++ b/docs/source/api/v12/api_capability.rst
@@ -48,7 +48,7 @@ API-Capabilities
   +-------------------+--------+--------------------------------------------------+
   | ``httpMethod``    | enum   | One of: 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'. |
   +-------------------+--------+--------------------------------------------------+
-  | ``route``         | string | API route.                                       |
+  | ``httpRoute``     | string | API route.                                       |
   +-------------------+--------+--------------------------------------------------+
   | ``capability``    | string | Capability name.                                 |
   +-------------------+--------+--------------------------------------------------+
@@ -62,14 +62,14 @@ API-Capabilities
            {
               "id": "6",
               "httpMethod": "GET",
-              "route": "/api/*/asns",
+              "httpRoute": "/api/*/asns",
               "capability": "asn-read",
               "lastUpdated": "2017-04-02 08:22:43"
            },
            {
               "id": "7",
               "httpMethod": "GET",
-              "route": "/api/*/asns/*",
+              "httpRoute": "/api/*/asns/*",
               "capability": "asn-read",
               "lastUpdated": "2017-04-02 08:22:43"
            }
@@ -103,7 +103,7 @@ API-Capabilities
   +-------------------+--------+--------------------------------------------------+
   | ``httpMethod``    | enum   | One of: 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'. |
   +-------------------+--------+--------------------------------------------------+
-  | ``route``         | string | API route.                                       |
+  | ``httpRoute``     | string | API route.                                       |
   +-------------------+--------+--------------------------------------------------+
   | ``capability``    | string | Capability name.                                 |
   +-------------------+--------+--------------------------------------------------+
@@ -117,7 +117,7 @@ API-Capabilities
            {
               "id": "6",
               "httpMethod": "GET",
-              "route": "/api/*/asns",
+              "httpRoute": "/api/*/asns",
               "capability": "asn-read",
               "lastUpdated": "2017-04-02 08:22:43"
            }
@@ -141,7 +141,7 @@ API-Capabilities
   +================+==========+========+==================================================+
   | ``httpMethod`` | yes      | enum   | One of: 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'. |
   +----------------+----------+--------+--------------------------------------------------+
-  | ``route``      | yes      | string | API route.                                       |
+  | ``httpRoute``  | yes      | string | API route.                                       |
   +----------------+----------+--------+--------------------------------------------------+
   | ``capability`` | yes      | string | Capability name                                  |
   +----------------+----------+--------+--------------------------------------------------+
@@ -150,7 +150,7 @@ API-Capabilities
 
     {
         "httpMethod": "POST",
-        "route": "/api/*/cdns",
+        "httpRoute": "/api/*/cdns",
         "capability": "cdn-write"
     }
 
@@ -165,7 +165,7 @@ API-Capabilities
   +--------------------+--------+--------------------------------------------------+
   | ``>httpMethod``    | enum   | One of: 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'. |
   +--------------------+--------+--------------------------------------------------+
-  | ``>route``         | string | API route.                                       |
+  | ``>httpRoute``     | string | API route.                                       |
   +--------------------+--------+--------------------------------------------------+
   | ``>capability``    | string | Capability name                                  |
   +--------------------+--------+--------------------------------------------------+
@@ -185,7 +185,7 @@ API-Capabilities
         "response":{
               "id": "6",
               "httpMethod": "POST",
-              "route": "/api/*/cdns",
+              "httpRoute": "/api/*/cdns",
               "capability": "cdn-write",
               "lastUpdated": "2017-04-02 08:22:43"
         },
@@ -222,7 +222,7 @@ API-Capabilities
   +===================+========+==================================================+
   | ``httpMethod``    | enum   | One of: 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'. |
   +-------------------+--------+--------------------------------------------------+
-  | ``route``         | string | API route.                                       |
+  | ``httpRoute``     | string | API route.                                       |
   +-------------------+--------+--------------------------------------------------+
   | ``capability``    | string | Capability name                                  |
   +-------------------+--------+--------------------------------------------------+
@@ -232,7 +232,7 @@ API-Capabilities
 
     {
         "httpMethod": "GET",
-        "route": "/api/*/cdns",
+        "httpRoute": "/api/*/cdns",
         "capability": "cdn-read"
     }
 
@@ -247,7 +247,7 @@ API-Capabilities
   +--------------------+--------+--------------------------------------------------+
   | ``>httpMethod``    | enum   | One of: 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'. |
   +--------------------+--------+--------------------------------------------------+
-  | ``>route``         | string | API route.                                       |
+  | ``>httpRoute``     | string | API route.                                       |
   +--------------------+--------+--------------------------------------------------+
   | ``>capability``    | string | Capability name                                  |
   +--------------------+--------+--------------------------------------------------+
@@ -266,7 +266,7 @@ API-Capabilities
         "response":{
               "id": "6",
               "httpMethod": "GET",
-              "route": "/api/*/cdns",
+              "httpRoute": "/api/*/cdns",
               "capability": "cdn-read",
               "lastUpdated": "2017-04-02 08:22:43"
         },
diff --git a/lib/go-tc/v13/roles.go b/lib/go-tc/v13/roles.go
new file mode 100644
index 000000000..eb38ba075
--- /dev/null
+++ b/lib/go-tc/v13/roles.go
@@ -0,0 +1,65 @@
+
+package v13
+
+/*
+ * 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.
+ */
+
+// A List of Roles Response
+// swagger:response RolesResponse
+// in: body
+type RolesResponse struct {
+	// in: body
+	Response []Role `json:"response"`
+}
+
+// A Single Role Response for Update and Create to depict what changed
+// swagger:response RoleResponse
+// in: body
+type RoleResponse struct {
+	// in: body
+	Response Role `json:"response"`
+}
+
+// Role ...
+type Role struct {
+	// ID of the Role
+	//
+	// required: true
+	ID *int `json:"id" db:"id"`
+
+	// Name of the Role
+	//
+	// required: true
+	Name *string `json:"name" db:"name"`
+
+	// Description of the Role
+	//
+	// required: true
+	Description *string `json:"description" db:"description"`
+
+	// Priv Level of the Role
+	//
+	// required: true
+	PrivLevel *int `json:"privLevel" db:"priv_level"`
+
+	// Capabilities associated with the Role
+	//
+	// required: true
+	Capabilities *[]string `json:"capabilities" db:"capabilities"`
+}
\ No newline at end of file
diff --git a/traffic_ops/app/lib/API/ApiCapability.pm b/traffic_ops/app/lib/API/ApiCapability.pm
index e143b1c21..66cc6b013 100644
--- a/traffic_ops/app/lib/API/ApiCapability.pm
+++ b/traffic_ops/app/lib/API/ApiCapability.pm
@@ -43,7 +43,7 @@ sub index {
 			@data, {
 				"id"          => $row->id,
 				"httpMethod"  => $row->http_method,
-				"route"       => $row->route,
+				"httpRoute"   => $row->route,
 				"capability"  => $row->capability->name,
 				"lastUpdated" => $row->last_updated
 			}
@@ -62,7 +62,7 @@ sub renderResults {
 			@data, {
 				"id"          => $row->id,
 				"httpMethod"  => $row->http_method,
-				"route"       => $row->route,
+				"httpRoute"   => $row->route,
 				"capability"  => $row->capability->name,
 				"lastUpdated" => $row->last_updated
 			}
@@ -86,7 +86,7 @@ sub is_mapping_valid {
 	my $self        = shift;
 	my $id          = shift;
 	my $http_method = shift;
-	my $route       = shift;
+	my $http_route  = shift;
 	my $capability  = shift;
 
 	if ( !defined($http_method) ) {
@@ -97,7 +97,7 @@ sub is_mapping_valid {
 		return ( undef, "HTTP method \'$http_method\' is invalid. Valid values are: " . join( ", ", sort keys %valid_http_methods ) );
 	}
 
-	if ( !defined($route) or $route eq "" ) {
+	if ( !defined($http_route) or $http_route eq "" ) {
 		return ( undef, "Route is required." );
 	}
 
@@ -112,7 +112,7 @@ sub is_mapping_valid {
 	}
 
 	# search a mapping for the same http_method & route
-	$rs_data = $self->db->resultset("ApiCapability")->search( { 'route' => { 'like', $route } } )->search(
+	$rs_data = $self->db->resultset("ApiCapability")->search( { 'route' => { 'like', $http_route } } )->search(
 		{
 			'http_method' => { '=', $http_method }
 		}
@@ -122,7 +122,7 @@ sub is_mapping_valid {
 	if ( !defined($id) ) {
 		if ( defined($rs_data) ) {
 			my $allocated_capability = $rs_data->capability->name;
-			return ( undef, "HTTP method '$http_method', route '$route' are already mapped to capability: $allocated_capability" );
+			return ( undef, "HTTP method '$http_method', route '$http_route' are already mapped to capability: $allocated_capability" );
 		}
 	}
 	else {
@@ -130,7 +130,7 @@ sub is_mapping_valid {
 			my $lid = $rs_data->id;
 			if ( $lid ne $id ) {
 				my $allocated_capability = $rs_data->capability->name;
-				return ( undef, "HTTP method '$http_method', route '$route' are already mapped to capability: $allocated_capability" );
+				return ( undef, "HTTP method '$http_method', route '$http_route' are already mapped to capability: $allocated_capability" );
 			}
 		}
 	}
@@ -151,18 +151,18 @@ sub create {
 	}
 
 	my $http_method = $params->{httpMethod} if defined( $params->{httpMethod} );
-	my $route       = $params->{route}      if defined( $params->{route} );
+	my $http_route  = $params->{httpRoute}  if defined( $params->{httpRoute} );
 	my $capability  = $params->{capability} if defined( $params->{capability} );
 	my $id          = undef;
 
-	my ( $is_valid, $errStr ) = $self->is_mapping_valid( $id, $http_method, $route, $capability );
+	my ( $is_valid, $errStr ) = $self->is_mapping_valid( $id, $http_method, $http_route, $capability );
 	if ( !$is_valid ) {
 		return $self->alert($errStr);
 	}
 
 	my $values = {
 		http_method => $http_method,
-		route       => $route,
+		route       => $http_route,
 		capability  => $capability
 	};
 
@@ -172,12 +172,12 @@ sub create {
 		my $response;
 		$response->{id}          = $rs->id;
 		$response->{httpMethod}  = $rs->http_method;
-		$response->{route}       = $rs->route;
+		$response->{httpRoute}   = $rs->route;
 		$response->{capability}  = $rs->capability->name;
 		$response->{lastUpdated} = $rs->last_updated;
 
 		&log( $self,
-			"Created API-Capability mapping: '$response->{httpMethod}', '$response->{route}', '$response->{capability}' for id: " . $response->{id},
+			"Created API-Capability mapping: '$response->{httpMethod}', '$response->{httpRoute}', '$response->{capability}' for id: " . $response->{id},
 			"APICHANGE" );
 
 		return $self->success( $response, "API-Capability mapping was created." );
@@ -201,7 +201,7 @@ sub update {
 	}
 
 	my $http_method = $params->{httpMethod} if defined( $params->{httpMethod} );
-	my $route       = $params->{route}      if defined( $params->{route} );
+	my $http_route  = $params->{httpRoute}  if defined( $params->{httpRoute} );
 	my $capability  = $params->{capability} if defined( $params->{capability} );
 
 	my $mapping = $self->db->resultset('ApiCapability')->find( { id => $id } );
@@ -209,14 +209,14 @@ sub update {
 		return $self->not_found();
 	}
 
-	my ( $is_valid, $errStr ) = $self->is_mapping_valid( $id, $http_method, $route, $capability );
+	my ( $is_valid, $errStr ) = $self->is_mapping_valid( $id, $http_method, $http_route, $capability );
 	if ( !$is_valid ) {
 		return $self->alert($errStr);
 	}
 
 	my $values = {
 		http_method => $http_method,
-		route       => $route,
+		route       => $http_route,
 		capability  => $capability
 	};
 
@@ -225,12 +225,12 @@ sub update {
 		my $response;
 		$response->{id}          = $rs->id;
 		$response->{httpMethod}  = $rs->http_method;
-		$response->{route}       = $rs->route;
+		$response->{httpRoute}   = $rs->route;
 		$response->{capability}  = $rs->capability->name;
 		$response->{lastUpdated} = $rs->last_updated;
 
 		&log( $self,
-			"Updated API-Capability mapping: '$response->{httpMethod}', '$response->{route}', '$response->{capability}' for id: " . $response->{id},
+			"Updated API-Capability mapping: '$response->{httpMethod}', '$response->{httpRoute}', '$response->{capability}' for id: " . $response->{id},
 			"APICHANGE" );
 
 		return $self->success( $response, "API-Capability mapping was updated." );
diff --git a/traffic_ops/app/t/api/1.2/api_capabilities.t b/traffic_ops/app/t/api/1.2/api_capabilities.t
index ef672d34e..b17255aa5 100644
--- a/traffic_ops/app/t/api/1.2/api_capabilities.t
+++ b/traffic_ops/app/t/api/1.2/api_capabilities.t
@@ -65,14 +65,14 @@ $t->get_ok("/api/1.2/api_capabilities")->status_is(200)->json_is( "/response", [
 
 # adding valid entry
 my $http_method = "GET";
-my $route = "sample/route";
+my $http_route = "sample/route";
 my $cap_name = "basic-read";
 $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => json => {
-			"httpMethod" => $http_method, "route" => $route, "capability" => $cap_name
+			"httpMethod" => $http_method, "httpRoute" => $http_route, "capability" => $cap_name
 		})->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content};} )
 	->json_is( "/response/id" => 1 )
 	->json_is( "/response/httpMethod" => $http_method )
-	->json_is( "/response/route" => $route )
+	->json_is( "/response/httpRoute" => $http_route )
 	->json_is( "/response/capability" => $cap_name )
 	, 'Does mapping details return?';
 
@@ -80,7 +80,7 @@ $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => jso
 $t->get_ok("/api/1.2/api_capabilities")->status_is(200)
 	->json_is( "/response/0/id" => 1 )
 	->json_is( "/response/0/httpMethod" => $http_method )
-	->json_is( "/response/0/route" => $route )
+	->json_is( "/response/0/httpRoute" => $http_route )
 	->json_is( "/response/0/capability" => $cap_name )
 	->or( sub { diag $t->tx->res->content->asset->{content}; } );;
 
@@ -88,25 +88,25 @@ $t->get_ok("/api/1.2/api_capabilities")->status_is(200)
 $t->get_ok("/api/1.2/api_capabilities/1")->status_is(200)
 	->json_is( "/response/0/id" => 1 )
 	->json_is( "/response/0/httpMethod" => $http_method )
-	->json_is( "/response/0/route" => $route )
+	->json_is( "/response/0/httpRoute" => $http_route )
 	->json_is( "/response/0/capability" => $cap_name )
 	->or( sub { diag $t->tx->res->content->asset->{content}; } );;
 
 #insert the same mapping twice - fails
 $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => json => {
-		"httpMethod" => $http_method, "route" => $route, "capability" => $cap_name
+		"httpMethod" => $http_method, "httpRoute" => $http_route, "capability" => $cap_name
 	})->status_is(400)->or( sub { diag $t->tx->res->content->asset->{content};} )
-	->json_is( "/alerts/0/text" => "HTTP method \'$http_method\', route \'$route\' are already mapped to capability: $cap_name" )
+	->json_is( "/alerts/0/text" => "HTTP method \'$http_method\', route \'$http_route\' are already mapped to capability: $cap_name" )
 	, 'Is same entry twice?';
 
 #edit a mapping
 my $cap_name_updated = "cdn-write";
 $t->put_ok("/api/1.2/api_capabilities/1" => {Accept => 'application/json'} => json => {
-		"httpMethod" => $http_method, "route" => $route, "capability" => $cap_name_updated
+		"httpMethod" => $http_method, "httpRoute" => $http_route, "capability" => $cap_name_updated
 	})->status_is(200)
 	->json_is( "/response/id" => 1 )
 	->json_is( "/response/httpMethod" => $http_method )
-	->json_is( "/response/route" => $route )
+	->json_is( "/response/httpRoute" => $http_route )
 	->json_is( "/response/capability" => $cap_name_updated )
 	, 'Did update succeed?';
 
@@ -114,17 +114,17 @@ $t->put_ok("/api/1.2/api_capabilities/1" => {Accept => 'application/json'} => js
 $t->get_ok("/api/1.2/api_capabilities/1" => {Accept => 'application/json'} )->status_is(200)
 	->json_is( "/response/0/id" => 1 )
 	->json_is( "/response/0/httpMethod" => $http_method )
-	->json_is( "/response/0/route" => $route )
+	->json_is( "/response/0/httpRoute" => $http_route )
 	->json_is( "/response/0/capability" => $cap_name_updated )
 	, 'Did get after update succeed?';
 
 #edit the mapping back
 $t->put_ok("/api/1.2/api_capabilities/1" => {Accept => 'application/json'} => json => {
-		"httpMethod" => $http_method, "route" => $route, "capability" => $cap_name
+		"httpMethod" => $http_method, "httpRoute" => $http_route, "capability" => $cap_name
 	})->status_is(200)
 	->json_is( "/response/id" => 1 )
 	->json_is( "/response/httpMethod" => $http_method )
-	->json_is( "/response/route" => $route )
+	->json_is( "/response/httpRoute" => $http_route )
 	->json_is( "/response/capability" => $cap_name )
 	, 'Did update succeed?';
 
@@ -132,7 +132,7 @@ $t->put_ok("/api/1.2/api_capabilities/1" => {Accept => 'application/json'} => js
 $t->get_ok("/api/1.2/api_capabilities/1" => {Accept => 'application/json'} )->status_is(200)
 	->json_is( "/response/0/id" => 1 )
 	->json_is( "/response/0/httpMethod" => $http_method )
-	->json_is( "/response/0/route" => $route )
+	->json_is( "/response/0/httpRoute" => $http_route )
 	->json_is( "/response/0/capability" => $cap_name )
 	, 'Did get after update back succeed?';
 
@@ -140,11 +140,11 @@ $t->get_ok("/api/1.2/api_capabilities/1" => {Accept => 'application/json'} )->st
 my $http_method_post = "POST";
 my $route_sample2 = "sample/route2";
 $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => json => {
-		"httpMethod" => $http_method_post, "route" => $route_sample2, "capability" => $cap_name
+		"httpMethod" => $http_method_post, "httpRoute" => $route_sample2, "capability" => $cap_name
 	})->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content};} )
 	->json_is( "/response/id" => 2 )
 	->json_is( "/response/httpMethod" => $http_method_post )
-	->json_is( "/response/route" => $route_sample2 )
+	->json_is( "/response/httpRoute" => $route_sample2 )
 	->json_is( "/response/capability" => $cap_name )
 	, 'Does mapping details return?';
 
@@ -152,11 +152,11 @@ $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => jso
 $t->get_ok("/api/1.2/api_capabilities?capability=$cap_name")->status_is(200)
 	->json_is( "/response/0/id" => 1 )
 	->json_is( "/response/0/httpMethod" => $http_method )
-	->json_is( "/response/0/route" => $route )
+	->json_is( "/response/0/httpRoute" => $http_route )
 	->json_is( "/response/0/capability" => $cap_name )
 	->json_is( "/response/1/id" => 2 )
 	->json_is( "/response/1/httpMethod" => $http_method_post )
-	->json_is( "/response/1/route" => $route_sample2 )
+	->json_is( "/response/1/httpRoute" => $route_sample2 )
 	->json_is( "/response/1/capability" => $cap_name )
 	->or( sub { diag $t->tx->res->content->asset->{content}; } );;
 
@@ -172,7 +172,7 @@ $t->get_ok("/api/1.2/api_capabilities/2")->status_is(200)->json_is( "/response",
 
 # adding invalid entry - no httpMethod
 $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => json => {
-		"route" => $route, "capability" => $cap_name
+		"httpRoute" => $http_route, "capability" => $cap_name
 	})->status_is(400)->or( sub { diag $t->tx->res->content->asset->{content};} )
 	->json_is( "/alerts/0/text" => "HTTP method is required." )
 	, 'Was invalid insert (no httpMethod) reject correctly?';
@@ -186,21 +186,21 @@ $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => jso
 
 # adding invalid entry - empty route
 $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => json => {
-		"httpMethod" => $http_method, "capability" => $cap_name, "route" => ""
+		"httpMethod" => $http_method, "capability" => $cap_name, "httpRoute" => ""
 	})->status_is(400)->or( sub { diag $t->tx->res->content->asset->{content};} )
 	->json_is( "/alerts/0/text" => "Route is required." )
 	, 'Was invalid insert (no route) reject correctly?';
 
 # adding invalid entry - no capability
 $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => json => {
-		"httpMethod" => $http_method, "route" => $route
+		"httpMethod" => $http_method, "httpRoute" => $http_route
 	})->status_is(400)->or( sub { diag $t->tx->res->content->asset->{content};} )
 	->json_is( "/alerts/0/text" => "Capability name is required." )
 	, 'Was invalid insert (no capability) reject correctly?';
 
 # adding invalid entry - empty capability
 $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => json => {
-		"httpMethod" => $http_method, "route" => $route, "capability" => ""
+		"httpMethod" => $http_method, "httpRoute" => $http_route, "capability" => ""
 	})->status_is(400)->or( sub { diag $t->tx->res->content->asset->{content};} )
 	->json_is( "/alerts/0/text" => "Capability name is required." )
 	, 'Was invalid insert (no capability) reject correctly?';
@@ -208,7 +208,7 @@ $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => jso
 # adding invalid entry - invalid httpMethod
 my $invalid_http_method = 'BAD';
 $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => json => {
-		"httpMethod" => $invalid_http_method, "route" => $route, "capability" => $cap_name
+		"httpMethod" => $invalid_http_method, "httpRoute" => $http_route, "capability" => $cap_name
 	})->status_is(400)->or( sub { diag $t->tx->res->content->asset->{content};} )
 	->json_is( "/alerts/0/text" => "HTTP method \'$invalid_http_method\' is invalid. Valid values are: DELETE, GET, PATCH, POST, PUT" )
 	, 'Was invalid insert (no capability) reject correctly?';
@@ -216,7 +216,7 @@ $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => jso
 # adding invalid entry - non-existing capability
 my $non_existing_cap = "non-existing";
 $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => json => {
-		"httpMethod" => $http_method, "route" => $route, "capability" => $non_existing_cap
+		"httpMethod" => $http_method, "httpRoute" => $http_route, "capability" => $non_existing_cap
 	})->status_is(400)->or( sub { diag $t->tx->res->content->asset->{content};} )
 	->json_is( "/alerts/0/text" => "Capability \'$non_existing_cap\' does not exist." )
 	, 'Was invalid insert (no capability) reject correctly?';
diff --git a/traffic_ops/app/t/api/1.2/capabilities.t b/traffic_ops/app/t/api/1.2/capabilities.t
index 6686a8716..732daab19 100644
--- a/traffic_ops/app/t/api/1.2/capabilities.t
+++ b/traffic_ops/app/t/api/1.2/capabilities.t
@@ -157,13 +157,13 @@ $t->post_ok("/api/1.2/capabilities" => {Accept => 'application/json'} => json =>
 
 # trying to delete a referenced capability. first add a mapping to it.
 my $http_method = "GET";
-my $route = "sample/route";
+my $http_route = "sample/route";
 $t->post_ok("/api/1.2/api_capabilities" => {Accept => 'application/json'} => json => {
-		"httpMethod" => $http_method, "route" => $route, "capability" => $cap_name
+		"httpMethod" => $http_method, "httpRoute" => $http_route, "capability" => $cap_name
 	})->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content};} )
 	->json_is( "/response/id" => 1 )
 	->json_is( "/response/httpMethod" => $http_method )
-	->json_is( "/response/route" => $route )
+	->json_is( "/response/httpRoute" => $http_route )
 	->json_is( "/response/capability" => $cap_name )
 	, 'Does mapping details return?';
 
diff --git a/traffic_ops/client/v13/role.go b/traffic_ops/client/v13/role.go
new file mode 100644
index 000000000..74a24019a
--- /dev/null
+++ b/traffic_ops/client/v13/role.go
@@ -0,0 +1,140 @@
+/*
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package v13
+
+import (
+	"encoding/json"
+	"fmt"
+	"net"
+	"net/http"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc/v13"
+)
+
+const (
+	API_v13_ROLES = "/api/1.3/roles"
+)
+
+// Create a Role
+func (to *Session) CreateRole(region v13.Role) (tc.Alerts, ReqInf, int, error) {
+
+	var remoteAddr net.Addr
+	reqBody, err := json.Marshal(region)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if err != nil {
+		return tc.Alerts{}, reqInf, 0, err
+	}
+	resp, remoteAddr, errClient := to.rawRequest(http.MethodPost, API_v13_ROLES, reqBody)
+	if resp != nil {
+		defer resp.Body.Close()
+		var alerts tc.Alerts
+		if err = json.NewDecoder(resp.Body).Decode(&alerts); err != nil {
+			return alerts, reqInf, resp.StatusCode, err
+		}
+		return alerts, reqInf, resp.StatusCode, errClient
+	}
+	return tc.Alerts{}, reqInf, 0, errClient
+}
+
+// Update a Role by ID
+func (to *Session) UpdateRoleByID(id int, region v13.Role) (tc.Alerts, ReqInf, int, error) {
+
+	var remoteAddr net.Addr
+	reqBody, err := json.Marshal(region)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if err != nil {
+		return tc.Alerts{}, reqInf, 0, err
+	}
+	route := fmt.Sprintf("%s/?id=%d", API_v13_ROLES, id)
+	resp, remoteAddr, errClient := to.rawRequest(http.MethodPut, route, reqBody)
+	if resp != nil {
+		defer resp.Body.Close()
+		var alerts tc.Alerts
+		if err = json.NewDecoder(resp.Body).Decode(&alerts); err != nil {
+			return alerts, reqInf, resp.StatusCode, err
+		}
+		return alerts, reqInf, resp.StatusCode, errClient
+	}
+	return tc.Alerts{}, reqInf, 0, errClient
+}
+
+// Returns a list of roles
+func (to *Session) GetRoles() ([]v13.Role, ReqInf, int, error) {
+	resp, remoteAddr, errClient := to.rawRequest(http.MethodGet, API_v13_ROLES, nil)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if resp != nil {
+		defer resp.Body.Close()
+
+		var data v13.RolesResponse
+		if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+			return data.Response, reqInf, resp.StatusCode, err
+		}
+		return data.Response, reqInf, resp.StatusCode, errClient
+	}
+	return []v13.Role{}, reqInf, 0, errClient
+}
+
+// GET a Role by the Role id
+func (to *Session) GetRoleByID(id int) ([]v13.Role, ReqInf, int, error) {
+	route := fmt.Sprintf("%s/?id=%d", API_v13_ROLES, id)
+	resp, remoteAddr, errClient := to.rawRequest(http.MethodGet, route, nil)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if resp != nil {
+		defer resp.Body.Close()
+
+		var data v13.RolesResponse
+		if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+			return data.Response, reqInf, resp.StatusCode, err
+		}
+		return data.Response, reqInf, resp.StatusCode, errClient
+	}
+	return []v13.Role{}, reqInf, 0, errClient
+}
+
+// GET a Role by the Role name
+func (to *Session) GetRoleByName(name string) ([]v13.Role, ReqInf, int, error) {
+	url := fmt.Sprintf("%s?name=%s", API_v13_ROLES, name)
+	resp, remoteAddr, errClient := to.rawRequest(http.MethodGet, url, nil)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if resp != nil {
+		defer resp.Body.Close()
+
+		var data v13.RolesResponse
+		if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+			return data.Response, reqInf, resp.StatusCode, err
+		}
+		return data.Response, reqInf, resp.StatusCode, errClient
+	}
+	return []v13.Role{}, reqInf, 0, errClient
+}
+
+// DELETE a Role by ID
+func (to *Session) DeleteRoleByID(id int) (tc.Alerts, ReqInf, int, error) {
+	route := fmt.Sprintf("%s/?id=%d", API_v13_ROLES, id)
+	resp, remoteAddr, errClient := to.rawRequest(http.MethodDelete, route, nil)
+	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+	if resp != nil {
+		defer resp.Body.Close()
+
+		var alerts tc.Alerts
+		if err := json.NewDecoder(resp.Body).Decode(&alerts); err != nil {
+			return alerts, reqInf, resp.StatusCode, err
+		}
+		return alerts, reqInf, resp.StatusCode, errClient
+	}
+	return tc.Alerts{}, reqInf, 0, errClient
+}
diff --git a/traffic_ops/testing/api/test/wrapper.go b/traffic_ops/testing/api/test/wrapper.go
index d2a0b4090..db94ef818 100644
--- a/traffic_ops/testing/api/test/wrapper.go
+++ b/traffic_ops/testing/api/test/wrapper.go
@@ -22,6 +22,7 @@ import (
 	"net/http"
 
 	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc/v13"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/client"
 )
 
@@ -30,10 +31,10 @@ var (
 )
 
 //GetCDN returns a Cdn struct
-func GetCDN() (tc.CDN, error) {
+func GetCDN() (v13.CDN, error) {
 	cdns, err := to.CDNs()
 	if err != nil {
-		return *new(tc.CDN), err
+		return *new(v13.CDN), err
 	}
 	cdn := cdns[0]
 	if cdn.Name == "ALL" {
diff --git a/traffic_ops/testing/api/v13/roles_test.go b/traffic_ops/testing/api/v13/roles_test.go
new file mode 100644
index 000000000..a518ca641
--- /dev/null
+++ b/traffic_ops/testing/api/v13/roles_test.go
@@ -0,0 +1,139 @@
+package v13
+
+/*
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+)
+
+const (
+	roleGood         = 0
+	roleInvalidCap   = 1
+	roleNeedCap      = 2
+	roleBadPrivLevel = 3
+)
+
+func TestRoles(t *testing.T) {
+	CreateTestRoles(t)
+	UpdateTestRoles(t)
+	GetTestRoles(t)
+	DeleteTestRoles(t)
+}
+
+func CreateTestRoles(t *testing.T) {
+	expectedAlerts := []tc.Alerts{tc.Alerts{[]tc.Alert{tc.Alert{"role was created.","success"}}}, tc.Alerts{[]tc.Alert{tc.Alert{"can not add non-existent capabilities: [invalid-capability]","error"}}}}
+	for i, role := range testData.Roles {
+		var alerts tc.Alerts
+		alerts, _, status, err := TOSession.CreateRole(role)
+		log.Debugln("Status Code: ", status)
+		log.Debugln("Response: ", alerts)
+		if err != nil {
+			log.Debugf("error: %v", err)
+			//t.Errorf("could not CREATE role: %v\n", err)
+		}
+		if !reflect.DeepEqual(alerts, expectedAlerts[i]) {
+			t.Errorf("got alerts: %v but expected alerts: %v", alerts, expectedAlerts[i])
+		}
+	}
+}
+
+func UpdateTestRoles(t *testing.T) {
+	log.Debugln("entered test")
+	log.Debugf("testData.Roles contains: %++v\n",testData.Roles)
+	firstRole := testData.Roles[0]
+	log.Debugln("got first role from slice")
+	// Retrieve the Role by role so we can get the id for the Update
+	resp, _, status, err := TOSession.GetRoleByName(*firstRole.Name)
+	log.Debugln("Status Code: ", status)
+	if err != nil {
+		t.Errorf("cannot GET Role by role: %v - %v\n", firstRole.Name, err)
+	}
+	log.Debugf("got response: %++v\n", resp)
+	remoteRole := resp[0]
+	expectedRole := "new admin2"
+	remoteRole.Name = &expectedRole
+	var alert tc.Alerts
+	alert, _, status, err = TOSession.UpdateRoleByID(*remoteRole.ID, remoteRole)
+	log.Debugln("Status Code: ", status)
+	if err != nil {
+		t.Errorf("cannot UPDATE Role by id: %v - %v\n", err, alert)
+	}
+
+	// Retrieve the Role to check role got updated
+	resp, _, status, err = TOSession.GetRoleByID(*remoteRole.ID)
+	log.Debugln("Status Code: ", status)
+	if err != nil {
+		t.Errorf("cannot GET Role by role: %v - %v\n", firstRole.Name, err)
+	}
+	respRole := resp[0]
+	if *respRole.Name != expectedRole {
+		t.Errorf("results do not match actual: %s, expected: %s\n", respRole.Name, expectedRole)
+	}
+
+	// Set the name back to the fixture value so we can delete it after
+	remoteRole.Name = firstRole.Name
+	alert, _, status, err = TOSession.UpdateRoleByID(*remoteRole.ID, remoteRole)
+	log.Debugln("Status Code: ", status)
+	if err != nil {
+		t.Errorf("cannot UPDATE Role by id: %v - %v\n", err, alert)
+	}
+
+}
+
+func GetTestRoles(t *testing.T) {
+	role := testData.Roles[roleGood]
+	resp, _, status, err := TOSession.GetRoleByName(*role.Name)
+	log.Debugln("Status Code: ", status)
+	if err != nil {
+		t.Errorf("cannot GET Role by role: %v - %v\n", err, resp)
+	}
+
+}
+
+func DeleteTestRoles(t *testing.T) {
+
+	role := testData.Roles[roleGood]
+	// Retrieve the Role by name so we can get the id
+	resp, _, status, err := TOSession.GetRoleByName(*role.Name)
+	log.Debugln("Status Code: ", status)
+	if err != nil {
+		t.Errorf("cannot GET Role by name: %v - %v\n", role.Name, err)
+	}
+	respRole := resp[0]
+
+	delResp, _, status, err := TOSession.DeleteRoleByID(*respRole.ID)
+	log.Debugln("Status Code: ", status)
+
+	if err != nil {
+		t.Errorf("cannot DELETE Role by role: %v - %v\n", err, delResp)
+	}
+
+	// Retrieve the Role to see if it got deleted
+	roleResp, _, status, err := TOSession.GetRoleByName(*role.Name)
+	log.Debugln("Status Code: ", status)
+
+	if err != nil {
+		t.Errorf("error deleting Role role: %s\n", err.Error())
+	}
+	if len(roleResp) > 0 {
+		t.Errorf("expected Role : %s to be deleted\n", role.Name)
+	}
+
+}
diff --git a/traffic_ops/testing/api/v13/tc-fixtures.json b/traffic_ops/testing/api/v13/tc-fixtures.json
index cecc13e99..57ce71b38 100644
--- a/traffic_ops/testing/api/v13/tc-fixtures.json
+++ b/traffic_ops/testing/api/v13/tc-fixtures.json
@@ -456,6 +456,20 @@
             "name": "region2"
         }
     ],
+    "roles": [
+        {
+            "name":"new_admin",
+            "description":"super-user 2",
+            "privLevel":30,
+            "capabilities":["all-read","all-write", "cdn-read"]
+        },
+        {
+            "name":"bad_admin",
+            "description":"super-user 3",
+            "privLevel":30,
+            "capabilities":["all-read","all-write","invalid-capability"]
+        }
+    ],
     "servers": [
         {
             "domainName": "ga.atlanta.kabletown.net",
diff --git a/traffic_ops/testing/api/v13/todb.go b/traffic_ops/testing/api/v13/todb.go
index a1963a871..303828988 100644
--- a/traffic_ops/testing/api/v13/todb.go
+++ b/traffic_ops/testing/api/v13/todb.go
@@ -61,6 +61,18 @@ func SetupTestData(*sql.DB) error {
 		os.Exit(1)
 	}
 
+	err = SetupCapabilities(db)
+	if err != nil {
+		fmt.Printf("\nError setting up capabilities %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err)
+		os.Exit(1)
+	}
+
+	err = SetupRoleCapabilities(db)
+	if err != nil {
+		fmt.Printf("\nError setting up roleCapabilities %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err)
+		os.Exit(1)
+	}
+
 	err = SetupTmusers(db)
 	if err != nil {
 		fmt.Printf("\nError setting up tm_user %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err)
@@ -88,6 +100,31 @@ INSERT INTO role (id, name, description, priv_level) VALUES (7, 'federation','Ro
 	return nil
 }
 
+func SetupCapabilities(db *sql.DB) error {
+	sqlStmt := `
+INSERT INTO capability (name, description) VALUES ('all-read','Full read access') ON CONFLICT DO NOTHING;
+INSERT INTO capability (name, description) VALUES ('all-write','Full write access') ON CONFLICT DO NOTHING;
+INSERT INTO capability (name, description) VALUES ('cdn-read','View CDN configuration') ON CONFLICT DO NOTHING;
+`
+	err := execSQL(db, sqlStmt, "capability")
+	if err != nil {
+		return fmt.Errorf("exec failed %v", err)
+	}
+	return nil
+}
+
+func SetupRoleCapabilities(db *sql.DB) error {
+	sqlStmt := `
+INSERT INTO role_capability (role_id, cap_name) VALUES (4,'all-write') ON CONFLICT DO NOTHING;
+INSERT INTO role_capability (role_id, cap_name) VALUES (4,'all-read') ON CONFLICT DO NOTHING;
+`
+	err := execSQL(db, sqlStmt, "role_capability")
+	if err != nil {
+		return fmt.Errorf("exec failed %v", err)
+	}
+	return nil
+}
+
 // SetupTmusers ...
 func SetupTmusers(db *sql.DB) error {
 
diff --git a/traffic_ops/testing/api/v13/traffic_control.go b/traffic_ops/testing/api/v13/traffic_control.go
index 5e01a4ac8..71c112409 100644
--- a/traffic_ops/testing/api/v13/traffic_control.go
+++ b/traffic_ops/testing/api/v13/traffic_control.go
@@ -35,6 +35,7 @@ type TrafficControl struct {
 	ProfileParameters              []v13.ProfileParameter              `json:"profileParameters"`
 	PhysLocations                  []v12.PhysLocation                  `json:"physLocations"`
 	Regions                        []v12.Region                        `json:"regions"`
+	Roles                          []v13.Role                          `json:"roles"`
 	Servers                        []v13.Server                        `json:"servers"`
 	Statuses                       []v12.Status                        `json:"statuses"`
 	Tenants                        []v12.Tenant                        `json:"tenants"`
diff --git a/traffic_ops/traffic_ops_golang/asn/asns.go b/traffic_ops/traffic_ops_golang/asn/asns.go
index ff77b4410..4cc4ebef1 100644
--- a/traffic_ops/traffic_ops_golang/asn/asns.go
+++ b/traffic_ops/traffic_ops_golang/asn/asns.go
@@ -20,11 +20,11 @@ package asn
  */
 
 import (
+	"encoding/json"
 	"errors"
-	"net/http"
 	"fmt"
+	"net/http"
 	"strconv"
-	"encoding/json"
 
 	"github.com/apache/incubator-trafficcontrol/lib/go-log"
 	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
@@ -210,7 +210,9 @@ func V11ReadAll(db *sqlx.DB) http.HandlerFunc {
 			Response struct {
 				ASNs []TOASNV12 `json:"asns"`
 			} `json:"response"`
-		}{Response: struct {ASNs []TOASNV12  `json:"asns"` }{ASNs: asns}}
+		}{Response: struct {
+			ASNs []TOASNV12 `json:"asns"`
+		}{ASNs: asns}}
 
 		respBts, err := json.Marshal(resp)
 		if err != nil {
diff --git a/traffic_ops/traffic_ops_golang/auth/authorize.go b/traffic_ops/traffic_ops_golang/auth/authorize.go
index 194be382b..2e5cff705 100644
--- a/traffic_ops/traffic_ops_golang/auth/authorize.go
+++ b/traffic_ops/traffic_ops_golang/auth/authorize.go
@@ -27,13 +27,15 @@ import (
 
 	"github.com/apache/incubator-trafficcontrol/lib/go-log"
 	"github.com/jmoiron/sqlx"
+	"github.com/lib/pq"
 )
 
 type CurrentUser struct {
-	UserName  string `json:"userName" db:"username"`
-	ID        int    `json:"id" db:"id"`
-	PrivLevel int    `json:"privLevel" db:"priv_level"`
-	TenantID  int    `json:"tenantId" db:"tenant_id"`
+	UserName     string         `json:"userName" db:"username"`
+	ID           int            `json:"id" db:"id"`
+	PrivLevel    int            `json:"privLevel" db:"priv_level"`
+	TenantID     int            `json:"tenantId" db:"tenant_id"`
+	Capabilities pq.StringArray `json:"capabilities" db:"capabilities"`
 }
 
 // PrivLevelInvalid - The Default Priv level
@@ -67,10 +69,10 @@ func GetCurrentUserFromDB(CurrentUserStmt *sqlx.Stmt, user string) CurrentUser {
 	switch {
 	case err == sql.ErrNoRows:
 		log.Errorf("checking user %v info: user not in database", user)
-		return CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid}
+		return CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, []string{}}
 	case err != nil:
 		log.Errorf("Error checking user %v info: %v", user, err.Error())
-		return CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid}
+		return CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, []string{}}
 	default:
 		return currentUserInfo
 	}
@@ -86,5 +88,5 @@ func GetCurrentUser(ctx context.Context) (*CurrentUser, error) {
 			return nil, fmt.Errorf("CurrentUser found with bad type: %T", v)
 		}
 	}
-	return &CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid}, errors.New("No user found in Context")
+	return &CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, []string{}}, errors.New("No user found in Context")
 }
diff --git a/traffic_ops/traffic_ops_golang/crconfig/config_test.go b/traffic_ops/traffic_ops_golang/crconfig/config_test.go
index d8e135d41..844f5d628 100644
--- a/traffic_ops/traffic_ops_golang/crconfig/config_test.go
+++ b/traffic_ops/traffic_ops_golang/crconfig/config_test.go
@@ -27,10 +27,10 @@ import (
 	"gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
 
-func ExpectedGetConfigParams(domain string) []CRConfigConfigParameter{
+func ExpectedGetConfigParams(domain string) []CRConfigConfigParameter {
 	return []CRConfigConfigParameter{
 		{"tld.ttls.foo" + *randStr(), *randStr()},
-		{"tld.soa.bar" + *randStr(),  *randStr()},
+		{"tld.soa.bar" + *randStr(), *randStr()},
 		{"domain_name", domain},
 	}
 }
diff --git a/traffic_ops/traffic_ops_golang/role/roles.go b/traffic_ops/traffic_ops_golang/role/roles.go
new file mode 100644
index 000000000..8ab775275
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/role/roles.go
@@ -0,0 +1,437 @@
+package role
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc/v13"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tovalidate"
+	validation "github.com/go-ozzo/ozzo-validation"
+	"github.com/jmoiron/sqlx"
+	"github.com/lib/pq"
+)
+
+//we need a type alias to define functions on
+type TORole v13.Role
+
+//the refType is passed into the handlers where a copy of its type is used to decode the json.
+var refType = TORole{}
+
+func GetRefType() *TORole {
+	return &refType
+}
+
+func (role TORole) GetKeyFieldsInfo() []api.KeyFieldInfo {
+	return []api.KeyFieldInfo{{"id", api.GetIntKey}}
+}
+
+//Implementation of the Identifier, Validator interface functions
+func (role TORole) GetKeys() (map[string]interface{}, bool) {
+	if role.ID == nil {
+		return map[string]interface{}{"id": 0}, false
+	}
+	return map[string]interface{}{"id": *role.ID}, true
+}
+
+func (role TORole) GetAuditName() string {
+	if role.Name != nil {
+		return *role.Name
+	}
+	if role.ID != nil {
+		return strconv.Itoa(*role.ID)
+	}
+	return "0"
+}
+
+func (role TORole) GetType() string {
+	return "role"
+}
+
+func (role *TORole) SetKeys(keys map[string]interface{}) {
+	i, _ := keys["id"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here.
+	role.ID = &i
+}
+
+// Validate fulfills the api.Validator interface
+func (role TORole) Validate(db *sqlx.DB) []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)}
+
+	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 db != nil {
+		err := db.Select(&badCaps, checkCaps, pq.Array(role.Capabilities))
+		if err != nil {
+			log.Errorf("got error from selecting bad capabilities: %v", err)
+			return []error{tc.DBError}
+		}
+		if len(badCaps) > 0 {
+			errsToReturn = append(errsToReturn, fmt.Errorf("can not add non-existent capabilities: %v", badCaps))
+		}
+	}
+	return errsToReturn
+}
+
+//The TORole implementation of the Creator interface
+//all implementations of Creator should use transactions and return the proper errorType
+//ParsePQUniqueConstraintError is used to determine if a role with conflicting values exists
+//if so, it will return an errorType of DataConflict and the type should be appended to the
+//generic error message returned
+//The insert sql returns the id and lastUpdated values of the newly inserted role and have
+//to be added to the struct
+func (role *TORole) Create(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
+	rollbackTransaction := true
+	tx, err := db.Beginx()
+	defer func() {
+		if tx == nil || !rollbackTransaction {
+			return
+		}
+		err := tx.Rollback()
+		if err != nil {
+			log.Errorln(errors.New("rolling back transaction: " + err.Error()))
+		}
+	}()
+
+	if err != nil {
+		log.Error.Printf("could not begin transaction: %v", err)
+		return tc.DBError, tc.SystemError
+	}
+	if *role.PrivLevel > user.PrivLevel {
+		return errors.New("can not create a role with a higher priv level than your own"), tc.ForbiddenError
+	}
+	resultRows, err := tx.NamedQuery(insertQuery(), role)
+	if err != nil {
+		if pqErr, ok := err.(*pq.Error); ok {
+			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+			if eType == tc.DataConflictError {
+				return errors.New("a role with " + err.Error()), eType
+			}
+			return err, eType
+		} else {
+			log.Errorf("received non pq error: %++v from create execution", err)
+			return tc.DBError, tc.SystemError
+		}
+	}
+	defer resultRows.Close()
+
+	var id int
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err := resultRows.Scan(&id); err != nil {
+			log.Error.Printf("could not scan id from insert: %s\n", err)
+			return tc.DBError, tc.SystemError
+		}
+	}
+	if rowsAffected == 0 {
+		err = errors.New("no role was inserted, no id was returned")
+		log.Errorln(err)
+		return tc.DBError, tc.SystemError
+	} else if rowsAffected > 1 {
+		err = errors.New("too many ids returned from role insert")
+		log.Errorln(err)
+		return tc.DBError, tc.SystemError
+	}
+	role.SetKeys(map[string]interface{}{"id": id})
+	//after we have role ID we can associate the capabilities:
+	err, errType := role.createRoleCapabilityAssociations(tx)
+	if err != nil {
+		return err, errType
+	}
+
+	err = tx.Commit()
+	if err != nil {
+		log.Errorln("Could not commit transaction: ", err)
+		return tc.DBError, tc.SystemError
+	}
+	rollbackTransaction = false
+	return nil, tc.NoError
+}
+
+func (role *TORole) createRoleCapabilityAssociations(tx *sqlx.Tx) (error, tc.ApiErrorType) {
+	result, err := tx.Exec(associateCapabilities(), role.ID, pq.Array(role.Capabilities))
+	if err != nil {
+		log.Errorf("received non pq error: %++v from create execution", err)
+		return tc.DBError, tc.SystemError
+	}
+	rows, err := result.RowsAffected()
+	if err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	}
+	expected := len(*role.Capabilities)
+	if int(rows) != expected {
+		log.Errorf("wrong number of role_capability rows created: %d expected: %d", rows, expected)
+	}
+	return nil, tc.NoError
+}
+
+func (role *TORole) deleteRoleCapabilityAssociations(tx *sqlx.Tx) (error, tc.ApiErrorType) {
+	result, err := tx.Exec(deleteAssociatedCapabilities(), role.ID)
+	if err != nil {
+
+		log.Errorf("received error: %++v from create execution", err)
+		return tc.DBError, tc.SystemError
+
+	}
+	_, err = result.RowsAffected()
+	if err != nil {
+		log.Errorf("could not check result after inserting role_capability relations: %v", err)
+	}
+	return nil, tc.NoError
+}
+
+func (role *TORole) Read(db *sqlx.DB, parameters map[string]string, user auth.CurrentUser) ([]interface{}, []error, tc.ApiErrorType) {
+	var rows *sqlx.Rows
+
+	// Query Parameters to Database Query column mappings
+	// see the fields mapped in the SQL query
+	queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+		"name": dbhelpers.WhereColumnInfo{"name", nil},
+		"id":   dbhelpers.WhereColumnInfo{"id", api.IsInt},
+	}
+	where, orderBy, queryValues, errs := dbhelpers.BuildWhereAndOrderBy(parameters, queryParamsToQueryCols)
+	if len(errs) > 0 {
+		return nil, errs, tc.DataConflictError
+	}
+
+	query := selectQuery() + where + orderBy
+	log.Debugln("Query is ", query)
+
+	rows, err := db.NamedQuery(query, queryValues)
+	if err != nil {
+		log.Errorf("Error querying Roles: %v", err)
+		return nil, []error{tc.DBError}, tc.SystemError
+	}
+	defer rows.Close()
+
+	Roles := []interface{}{}
+	for rows.Next() {
+		var r TORole
+		var caps []string
+		if err = rows.Scan(&r.ID, &r.Name, &r.Description, &r.PrivLevel, pq.Array(&caps)); err != nil {
+			log.Errorf("error parsing Role rows: %v", err)
+			return nil, []error{tc.DBError}, tc.SystemError
+		}
+		r.Capabilities = &caps
+		Roles = append(Roles, r)
+	}
+
+	return Roles, []error{}, tc.NoError
+}
+
+//The TORole implementation of the Updater interface
+//all implementations of Updater should use transactions and return the proper errorType
+//ParsePQUniqueConstraintError is used to determine if a role with conflicting values exists
+//if so, it will return an errorType of DataConflict and the type should be appended to the
+//generic error message returned
+func (role *TORole) Update(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
+	rollbackTransaction := true
+	tx, err := db.Beginx()
+	defer func() {
+		if tx == nil || !rollbackTransaction {
+			return
+		}
+		err := tx.Rollback()
+		if err != nil {
+			log.Errorln(errors.New("rolling back transaction: " + err.Error()))
+		}
+	}()
+
+	if err != nil {
+		log.Error.Printf("could not begin transaction: %v", err)
+		return tc.DBError, tc.SystemError
+	}
+
+	if *role.PrivLevel > user.PrivLevel {
+		return errors.New("can not create a role with a higher priv level than your own"), tc.ForbiddenError
+	}
+
+	log.Debugf("about to run exec query: %s with role: %++v\n", updateQuery(), role)
+	result, err := tx.NamedExec(updateQuery(), role)
+	if err != nil {
+		if pqErr, ok := err.(*pq.Error); ok {
+			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+			if eType == tc.DataConflictError {
+				return errors.New("a role with " + err.Error()), eType
+			}
+			return err, eType
+		} else {
+			log.Errorf("received error: %++v from update execution", err)
+			return tc.DBError, tc.SystemError
+		}
+	}
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		log.Errorf("received error: %++v from checking result of update", err)
+		return tc.DBError, tc.SystemError
+	}
+
+	if rowsAffected != 1 {
+		if rowsAffected < 1 {
+			return errors.New("no role found with this id"), tc.DataMissingError
+		} else {
+			return fmt.Errorf("this update affected too many rows: %d", rowsAffected), tc.SystemError
+		}
+	}
+	//remove associations
+	err, errType := role.deleteRoleCapabilityAssociations(tx)
+	if err != nil {
+		return err, errType
+	}
+	//create new associations
+	err, errType = role.createRoleCapabilityAssociations(tx)
+	if err != nil {
+		return err, errType
+	}
+
+	err = tx.Commit()
+	if err != nil {
+		log.Errorln("Could not commit transaction: ", err)
+		return tc.DBError, tc.SystemError
+	}
+	rollbackTransaction = false
+	return nil, tc.NoError
+}
+
+//The Role implementation of the Deleter interface
+//all implementations of Deleter should use transactions and return the proper errorType
+func (role *TORole) Delete(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) {
+	rollbackTransaction := true
+	tx, err := db.Beginx()
+	defer func() {
+		if tx == nil || !rollbackTransaction {
+			return
+		}
+		err := tx.Rollback()
+		if err != nil {
+			log.Errorln(errors.New("rolling back transaction: " + err.Error()))
+		}
+	}()
+
+	if err != nil {
+		log.Error.Printf("could not begin transaction: %v", err)
+		return tc.DBError, tc.SystemError
+	}
+	assignedUsers := 0
+	err = tx.Get(&assignedUsers, "SELECT COUNT(id) FROM tm_user WHERE role=$1", role.ID)
+	if err != nil {
+		log.Errorf("received error: %++v from assigned users check", err)
+		return tc.DBError, tc.SystemError
+	}
+	if assignedUsers != 0 {
+		return fmt.Errorf("can not delete a role with %d assigned users", assignedUsers), tc.DataConflictError
+	}
+
+	log.Debugf("about to run exec query: %s with role: %++v", deleteQuery(), role)
+	result, err := tx.NamedExec(deleteQuery(), role)
+	if err != nil {
+		log.Errorf("received error: %++v from delete execution", err)
+		return tc.DBError, tc.SystemError
+	}
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		return tc.DBError, tc.SystemError
+	}
+	if rowsAffected != 1 {
+		if rowsAffected < 1 {
+			return errors.New("no role with that id found"), tc.DataMissingError
+		} else {
+			return fmt.Errorf("this create affected too many rows: %d", rowsAffected), tc.SystemError
+		}
+	}
+	//remove associations
+	err, errType := role.deleteRoleCapabilityAssociations(tx)
+	if err != nil {
+		return err, errType
+	}
+
+	err = tx.Commit()
+	if err != nil {
+		log.Errorln("Could not commit transaction: ", err)
+		return tc.DBError, tc.SystemError
+	}
+	rollbackTransaction = false
+	return nil, tc.NoError
+}
+
+func selectQuery() string {
+	query := `SELECT
+id,
+name,
+description,
+priv_level,
+ARRAY(SELECT rc.cap_name FROM role_capability AS rc WHERE rc.role_id=id) AS capabilities
+
+FROM role`
+	return query
+}
+
+func updateQuery() string {
+	query := `UPDATE
+role SET
+name=:name,
+description=:description
+WHERE id=:id`
+	return query
+}
+
+func deleteAssociatedCapabilities() string {
+	query := `DELETE FROM role_capability
+WHERE role_id=$1`
+	return query
+}
+
+func associateCapabilities() string {
+	query := `INSERT INTO role_capability (
+role_id,
+cap_name) WITH
+	q1 AS ( SELECT * FROM (VALUES ($1::bigint)) AS role_id ),
+	q2 AS (SELECT UNNEST($2::text[]))
+	SELECT * FROM q1,q2`
+	return query
+}
+
+func insertQuery() string {
+	query := `INSERT INTO role (
+name,
+description,
+priv_level) VALUES (
+:name,
+:description,
+:priv_level) RETURNING id`
+	return query
+}
+
+func deleteQuery() string {
+	query := `DELETE FROM role
+WHERE id=:id`
+	return query
+}
diff --git a/traffic_ops/traffic_ops_golang/role/roles_test.go b/traffic_ops/traffic_ops_golang/role/roles_test.go
new file mode 100644
index 000000000..550a14eec
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/role/roles_test.go
@@ -0,0 +1,119 @@
+package role
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc/v13"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/test"
+)
+
+func stringAddr(s string) *string {
+	return &s
+}
+func intAddr(i int) *int {
+	return &i
+}
+
+func getTestRoles() []v13.Role {
+	roles := []v13.Role{
+		{
+			ID:          intAddr(1),
+			Name:        stringAddr("role1"),
+			Description: stringAddr("the first role"),
+			PrivLevel:   intAddr(30),
+		},
+		{
+			ID:          intAddr(2),
+			Name:        stringAddr("role2"),
+			Description: stringAddr("the second role"),
+			PrivLevel:   intAddr(10),
+		},
+	}
+	return roles
+}
+
+//removed sqlmock based ReadRoles test due to sqlmock / pq.Array() type incompatibility issue.
+
+func TestFuncs(t *testing.T) {
+	if strings.Index(selectQuery(), "SELECT") != 0 {
+		t.Errorf("expected selectQuery to start with SELECT")
+	}
+	if strings.Index(insertQuery(), "INSERT") != 0 {
+		t.Errorf("expected insertQuery to start with INSERT")
+	}
+	if strings.Index(updateQuery(), "UPDATE") != 0 {
+		t.Errorf("expected updateQuery to start with UPDATE")
+	}
+	if strings.Index(deleteQuery(), "DELETE") != 0 {
+		t.Errorf("expected deleteQuery to start with DELETE")
+	}
+
+}
+func TestInterfaces(t *testing.T) {
+	var i interface{}
+	i = &TORole{}
+
+	if _, ok := i.(api.Creator); !ok {
+		t.Errorf("role must be creator")
+	}
+	if _, ok := i.(api.Reader); !ok {
+		t.Errorf("role must be reader")
+	}
+	if _, ok := i.(api.Updater); !ok {
+		t.Errorf("role must be updater")
+	}
+	if _, ok := i.(api.Deleter); !ok {
+		t.Errorf("role must be deleter")
+	}
+	if _, ok := i.(api.Identifier); !ok {
+		t.Errorf("role must be Identifier")
+	}
+}
+
+func TestValidate(t *testing.T) {
+	// invalid name, empty domainname
+	n := "not_a_valid_role"
+	r := TORole{Name: &n}
+	errs := test.SortErrors(r.Validate(nil))
+
+	expectedErrs := []error{
+		errors.New(`'description' cannot be blank`),
+		errors.New(`'privLevel' cannot be blank`),
+	}
+
+	if !reflect.DeepEqual(expectedErrs, errs) {
+		t.Errorf("expected %s, got %s", expectedErrs, errs)
+	}
+
+	//  name,  domainname both valid
+	r = TORole{Name: stringAddr("this is a valid name"), Description: stringAddr("this is a description"), PrivLevel: intAddr(30)}
+	expectedErrs = []error{}
+	errs = r.Validate(nil)
+	if !reflect.DeepEqual(expectedErrs, errs) {
+		t.Errorf("expected %s, got %s", expectedErrs, errs)
+	}
+
+}
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index b82fa59a6..d9737d563 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -47,6 +47,7 @@ import (
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/profile"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/profileparameter"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/region"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/role"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/server"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/status"
 	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/systeminfo"
@@ -210,6 +211,12 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{1.3, http.MethodPut, `deliveryservices/{xmlID}/urisignkeys$`, saveDeliveryServiceURIKeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
 		{1.3, http.MethodDelete, `deliveryservices/{xmlID}/urisignkeys$`, removeDeliveryServiceURIKeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil},
 
+		//Roles
+		{1.3, http.MethodGet, `roles/?(\.json)?$`, api.ReadHandler(role.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.3, http.MethodPut, `roles/?$`, api.UpdateHandler(role.GetRefType(), d.DB), auth.PrivLevelAdmin, Authenticated, nil},
+		{1.3, http.MethodPost, `roles/?$`, api.CreateHandler(role.GetRefType(), d.DB), auth.PrivLevelAdmin, Authenticated, nil},
+		{1.3, http.MethodDelete, `roles/?$`, api.DeleteHandler(role.GetRefType(), d.DB), auth.PrivLevelAdmin, Authenticated, nil},
+
 		//Servers
 		{1.3, http.MethodPost, `servers/{id}/deliveryservices$`, server.AssignDeliveryServicesToServerHandler(d.DB), auth.PrivLevelOperations, Authenticated, nil},
 		{1.3, http.MethodGet, `servers/{host_name}/update_status$`, server.GetServerUpdateStatusHandler(d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
diff --git a/traffic_ops/traffic_ops_golang/routing.go b/traffic_ops/traffic_ops_golang/routing.go
index 7a36a46c3..c01239f62 100644
--- a/traffic_ops/traffic_ops_golang/routing.go
+++ b/traffic_ops/traffic_ops_golang/routing.go
@@ -215,7 +215,7 @@ func RegisterRoutes(d ServerData) error {
 }
 
 func prepareUserInfoStmt(db *sqlx.DB) (*sqlx.Stmt, error) {
-	return db.Preparex("SELECT r.priv_level, u.id, u.username, COALESCE(u.tenant_id, -1) AS tenant_id FROM tm_user AS u JOIN role AS r ON u.role = r.id WHERE u.username = $1")
+	return db.Preparex("SELECT r.priv_level, u.id, u.username, COALESCE(u.tenant_id, -1) AS tenant_id, ARRAY(SELECT rc.cap_name FROM role_capability AS rc WHERE rc.role_id=r.id) AS capabilities FROM tm_user AS u JOIN role AS r ON u.role = r.id WHERE u.username = $1")
 }
 
 func use(h http.HandlerFunc, middlewares []Middleware) http.HandlerFunc {
diff --git a/traffic_portal/app/src/app.js b/traffic_portal/app/src/app.js
index f0c4385b5..161c73c2d 100644
--- a/traffic_portal/app/src/app.js
+++ b/traffic_portal/app/src/app.js
@@ -60,7 +60,11 @@ var trafficPortal = angular.module('trafficPortal', [
         require('./modules/private/cacheChecks').name,
         require('./modules/private/cacheStats').name,
         require('./modules/private/capabilities').name,
+        require('./modules/private/capabilities/new').name, // this must be defined before edit for the url matcher to work
         require('./modules/private/capabilities/list').name,
+        require('./modules/private/capabilities/edit').name,
+        require('./modules/private/capabilities/endpoints').name,
+        require('./modules/private/capabilities/users').name,
         require('./modules/private/cdns').name,
         require('./modules/private/cdns/config').name,
         require('./modules/private/cdns/deliveryServices').name,
@@ -146,7 +150,11 @@ var trafficPortal = angular.module('trafficPortal', [
         require('./modules/private/regions/physLocations').name,
         require('./modules/private/regions/new').name,
         require('./modules/private/roles').name,
+        require('./modules/private/roles/capabilities').name,
+        require('./modules/private/roles/edit').name,
         require('./modules/private/roles/list').name,
+        require('./modules/private/roles/new').name,
+        require('./modules/private/roles/users').name,
         require('./modules/private/servers').name,
         require('./modules/private/servers/configFiles').name,
         require('./modules/private/servers/deliveryServices').name,
@@ -213,6 +221,9 @@ var trafficPortal = angular.module('trafficPortal', [
         require('./common/modules/form/asn').name,
         require('./common/modules/form/asn/edit').name,
         require('./common/modules/form/asn/new').name,
+        require('./common/modules/form/capability').name,
+        require('./common/modules/form/capability/edit').name,
+        require('./common/modules/form/capability/new').name,
         require('./common/modules/form/cdn').name,
         require('./common/modules/form/cdn/edit').name,
         require('./common/modules/form/cdn/new').name,
@@ -253,6 +264,9 @@ var trafficPortal = angular.module('trafficPortal', [
         require('./common/modules/form/region').name,
         require('./common/modules/form/region/edit').name,
         require('./common/modules/form/region/new').name,
+        require('./common/modules/form/role').name,
+        require('./common/modules/form/role/edit').name,
+        require('./common/modules/form/role/new').name,
         require('./common/modules/form/server').name,
         require('./common/modules/form/server/edit').name,
         require('./common/modules/form/server/new').name,
@@ -277,6 +291,8 @@ var trafficPortal = angular.module('trafficPortal', [
         require('./common/modules/table/cacheGroupServers').name,
         require('./common/modules/table/cacheGroupStaticDnsEntries').name,
         require('./common/modules/table/capabilities').name,
+        require('./common/modules/table/capabilityEndpoints').name,
+        require('./common/modules/table/capabilityUsers').name,
         require('./common/modules/table/changeLogs').name,
         require('./common/modules/table/asns').name,
         require('./common/modules/table/cdns').name,
@@ -310,6 +326,8 @@ var trafficPortal = angular.module('trafficPortal', [
         require('./common/modules/table/regions').name,
         require('./common/modules/table/regionPhysLocations').name,
         require('./common/modules/table/roles').name,
+        require('./common/modules/table/roleCapabilities').name,
+        require('./common/modules/table/roleUsers').name,
         require('./common/modules/table/servers').name,
         require('./common/modules/table/serverConfigFiles').name,
         require('./common/modules/table/serverDeliveryServices').name,
diff --git a/traffic_portal/app/src/common/api/CapabilityService.js b/traffic_portal/app/src/common/api/CapabilityService.js
index da8df4ffd..9d0855c93 100644
--- a/traffic_portal/app/src/common/api/CapabilityService.js
+++ b/traffic_portal/app/src/common/api/CapabilityService.js
@@ -17,41 +17,68 @@
  * under the License.
  */
 
-var CapabilityService = function(Restangular, messageModel) {
+var CapabilityService = function(Restangular, $q, $http, messageModel, ENV) {
 
 	this.getCapabilities = function(queryParams) {
 		return Restangular.all('capabilities').getList(queryParams);
 	};
 
-	this.getCapability = function(id) {
-		return Restangular.one("capabilities", id).get();
+	this.getCapability = function(name) {
+		return Restangular.one("capabilities", name).get();
 	};
 
-	this.updateCapability = function(capability) {
-		return capability.put()
+	this.createCapability = function(cap) {
+		var request = $q.defer();
+
+		$http.post(ENV.api['root'] + "capabilities", cap)
 			.then(
-				function() {
-					messageModel.setMessages([ { level: 'success', text: 'Capability updated' } ], false);
+				function(result) {
+					request.resolve(result.data);
 				},
 				function(fault) {
 					messageModel.setMessages(fault.data.alerts, false);
+					request.reject(fault);
 				}
 			);
+
+		return request.promise;
 	};
 
-	this.deleteCapability = function(capability) {
-		return capability.remove()
+	this.updateCapability = function(cap) {
+		var request = $q.defer();
+
+		$http.put(ENV.api['root'] + "capabilities/" + cap.name, cap)
 			.then(
-				function() {
-					messageModel.setMessages([ { level: 'success', text: 'Capability deleted' } ], true);
+				function(result) {
+					request.resolve(result.data);
 				},
 				function(fault) {
-					messageModel.setMessages(fault.data.alerts, true);
+					messageModel.setMessages(fault.data.alerts, false);
+					request.reject();
 				}
 			);
+
+		return request.promise;
+	};
+
+	this.deleteCapability = function(cap) {
+		var request = $q.defer();
+
+		$http.delete(ENV.api['root'] + "capabilities/" + cap.name)
+			.then(
+				function(result) {
+					request.resolve(result.data);
+				},
+				function(fault) {
+					messageModel.setMessages(fault.data.alerts, false);
+					request.reject(fault);
+				}
+			);
+
+		return request.promise;
 	};
 
 };
 
-CapabilityService.$inject = ['Restangular', 'messageModel'];
+CapabilityService.$inject = ['Restangular', '$q', '$http', 'messageModel', 'ENV'];
 module.exports = CapabilityService;
diff --git a/traffic_portal/app/src/common/api/EndpointService.js b/traffic_portal/app/src/common/api/EndpointService.js
new file mode 100644
index 000000000..49d498342
--- /dev/null
+++ b/traffic_portal/app/src/common/api/EndpointService.js
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+var EndpointService = function(Restangular) {
+
+	this.getEndpoints = function(queryParams) {
+		return Restangular.all('api_capabilities').getList(queryParams);
+	};
+
+};
+
+EndpointService.$inject = ['Restangular'];
+module.exports = EndpointService;
diff --git a/traffic_portal/app/src/common/api/RoleService.js b/traffic_portal/app/src/common/api/RoleService.js
index 70b77bc6f..07d63c88b 100644
--- a/traffic_portal/app/src/common/api/RoleService.js
+++ b/traffic_portal/app/src/common/api/RoleService.js
@@ -17,41 +17,64 @@
  * under the License.
  */
 
-var RoleService = function(Restangular, messageModel) {
+var RoleService = function(Restangular, $http, $q, messageModel, ENV) {
 
     this.getRoles = function(queryParams) {
         return Restangular.all('roles').getList(queryParams);
     };
 
-    this.getRole = function(id) {
-        return Restangular.one("roles", id).get();
+    this.createRole = function(role) {
+        var request = $q.defer();
+
+        $http.post(ENV.api['root'] + "roles", role)
+            .then(
+                function(result) {
+                    request.resolve(result.data);
+                },
+                function(fault) {
+                    messageModel.setMessages(fault.data.alerts, false);
+                    request.reject(fault);
+                }
+            );
+
+        return request.promise;
     };
 
     this.updateRole = function(role) {
-        return role.put()
+        var request = $q.defer();
+
+        $http.put(ENV.api['root'] + "roles?id=" + role.id, role)
             .then(
-                function() {
-                    messageModel.setMessages([ { level: 'success', text: 'Role updated' } ], false);
+                function(result) {
+                    request.resolve(result.data);
                 },
                 function(fault) {
                     messageModel.setMessages(fault.data.alerts, false);
+                    request.reject();
                 }
-        );
+            );
+
+        return request.promise;
     };
 
-    this.deleteRole = function(role) {
-        return role.remove()
+    this.deleteRole = function(id) {
+        var request = $q.defer();
+
+        $http.delete(ENV.api['root'] + "roles?id=" + id)
             .then(
-                function() {
-                    messageModel.setMessages([ { level: 'success', text: 'Role deleted' } ], true);
+                function(result) {
+                    request.resolve(result.data);
                 },
                 function(fault) {
-                    messageModel.setMessages(fault.data.alerts, true);
+                    messageModel.setMessages(fault.data.alerts, false);
+                    request.reject(fault);
                 }
-        );
+            );
+
+        return request.promise;
     };
 
 };
 
-RoleService.$inject = ['Restangular', 'messageModel'];
+RoleService.$inject = ['Restangular', '$http', '$q', 'messageModel', 'ENV'];
 module.exports = RoleService;
diff --git a/traffic_portal/app/src/common/api/index.js b/traffic_portal/app/src/common/api/index.js
index 9da91f6f5..aa7ee5928 100644
--- a/traffic_portal/app/src/common/api/index.js
+++ b/traffic_portal/app/src/common/api/index.js
@@ -34,6 +34,7 @@ module.exports = angular.module('trafficPortal.api', [])
     .service('deliveryServiceSslKeysService', require('./DeliveryServiceSslKeysService'))
 	.service('divisionService', require('./DivisionService'))
 	.service('federationService', require('./FederationService'))
+	.service('endpointService', require('./EndpointService'))
 	.service('federationResolverService', require('./FederationResolverService'))
     .service('httpService', require('./HttpService'))
     .service('jobService', require('./JobService'))
diff --git a/traffic_portal/app/src/common/modules/form/capability/FormCapabilityController.js b/traffic_portal/app/src/common/modules/form/capability/FormCapabilityController.js
new file mode 100644
index 000000000..89445ca3c
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/capability/FormCapabilityController.js
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+var FormCapabilityController = function(capability, $scope, formUtils, locationUtils) {
+
+	$scope.capability = capability;
+
+	$scope.navigateToPath = locationUtils.navigateToPath;
+
+	$scope.hasError = formUtils.hasError;
+
+	$scope.hasPropertyError = formUtils.hasPropertyError;
+
+};
+
+FormCapabilityController.$inject = ['capability', '$scope', 'formUtils', 'locationUtils'];
+module.exports = FormCapabilityController;
diff --git a/traffic_portal/app/src/common/modules/form/capability/edit/FormEditCapabilityController.js b/traffic_portal/app/src/common/modules/form/capability/edit/FormEditCapabilityController.js
new file mode 100644
index 000000000..a0186e585
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/capability/edit/FormEditCapabilityController.js
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+var FormEditCapabilityController = function(capability, $scope, $controller, $uibModal, $anchorScroll, $location, locationUtils, capabilityService, messageModel) {
+
+	// extends the FormCapabilityController to inherit common methods
+	angular.extend(this, $controller('FormCapabilityController', { capability: capability, $scope: $scope }));
+
+	var deleteCapability = function(cap) {
+		capabilityService.deleteCapability(cap)
+			.then(function(result) {
+				messageModel.setMessages(result.alerts, true);
+				locationUtils.navigateToPath('/capabilities');
+			});
+	};
+
+	var save = function(cap) {
+		capabilityService.updateCapability(cap).
+			then(function(result) {
+				$scope.capName = angular.copy(cap.name);
+				messageModel.setMessages(result.alerts, false);
+				$anchorScroll(); // scrolls window to top
+			});
+	};
+
+	$scope.capName = angular.copy($scope.capability.name);
+
+	$scope.settings = {
+		isNew: false,
+		saveLabel: 'Update'
+	};
+
+	$scope.viewEndpoints = function() {
+		$location.path($location.path() + '/endpoints');
+	};
+
+	$scope.viewUsers = function() {
+		$location.path($location.path() + '/users');
+	};
+
+	$scope.confirmSave = function(cap) {
+		var params = {
+			title: 'Update Capability?',
+			message: 'Are you sure you want to update the capability?'
+		};
+
+		var modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+			controller: 'DialogConfirmController',
+			size: 'md',
+			resolve: {
+				params: function () {
+					return params;
+				}
+			}
+		});
+		modalInstance.result.then(function() {
+			save(cap);
+		}, function () {
+			// do nothing
+		});
+	};
+
+	$scope.confirmDelete = function(cap) {
+		var params = {
+			title: 'Delete Capability: ' + cap.name,
+			key: cap.name
+		};
+		var modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/delete/dialog.delete.tpl.html',
+			controller: 'DialogDeleteController',
+			size: 'md',
+			resolve: {
+				params: function () {
+					return params;
+				}
+			}
+		});
+		modalInstance.result.then(function() {
+			deleteCapability(cap);
+		}, function () {
+			// do nothing
+		});
+	};
+
+};
+
+FormEditCapabilityController.$inject = ['capability', '$scope', '$controller', '$uibModal', '$anchorScroll', '$location', 'locationUtils', 'capabilityService', 'messageModel'];
+module.exports = FormEditCapabilityController;
diff --git a/traffic_portal/app/src/common/modules/form/capability/edit/index.js b/traffic_portal/app/src/common/modules/form/capability/edit/index.js
new file mode 100644
index 000000000..52b7e6109
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/capability/edit/index.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.form.capability.edit', [])
+	.controller('FormEditCapabilityController', require('./FormEditCapabilityController'));
diff --git a/traffic_portal/app/src/common/modules/form/capability/form.capability.tpl.html b/traffic_portal/app/src/common/modules/form/capability/form.capability.tpl.html
new file mode 100644
index 000000000..07a716a58
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/capability/form.capability.tpl.html
@@ -0,0 +1,58 @@
+<!--
+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.
+-->
+
+<div class="x_panel">
+    <div class="x_title">
+        <ol class="breadcrumb pull-left">
+            <li><a ng-click="navigateToPath('/capabilities')">Capabilities</a></li>
+            <li class="active">{{capName}}</li>
+        </ol>
+        <div class="pull-right" role="group" ng-show="!settings.isNew">
+            <button class="btn btn-primary" title="View Endpoints" ng-click="viewEndpoints()">View Endpoints</button>
+            <!--<button class="btn btn-primary" title="View Users" ng-click="viewUsers()">View Users</button>-->
+        </div>
+        <div class="clearfix"></div>
+    </div>
+    <div class="x_content">
+        <br>
+        <form name="capForm" class="form-horizontal form-label-left" novalidate>
+            <div class="form-group" ng-class="{'has-error': hasError(capForm.name), 'has-feedback': hasError(capForm.name)}">
+                <label class="control-label col-md-2 col-sm-2 col-xs-12">Name *</label>
+                <div class="col-md-10 col-sm-10 col-xs-12">
+                    <input name="name" type="text" class="form-control" placeholder="Example: foo-read or foo-write" ng-disabled="!settings.isNew" ng-model="capability.name" ng-pattern="/^\S*$/" required autofocus>
+                    <small class="input-error" ng-show="hasPropertyError(capForm.name, 'required')">Required</small>
+                    <small class="input-error" ng-show="hasPropertyError(capForm.name, 'pattern')">No spaces</small>
+                    <span ng-show="hasError(capForm.name)" class="form-control-feedback"><i class="fa fa-times"></i></span>
+                </div>
+            </div>
+            <div class="form-group" ng-class="{'has-error': hasError(capForm.description), 'has-feedback': hasError(capForm.description)}">
+                <label class="control-label col-md-2 col-sm-2 col-xs-12">Description *</label>
+                <div class="col-md-10 col-sm-10 col-xs-12">
+                    <textarea name="description" type="text" class="form-control" ng-model="capability.description" rows="10" required autofocus></textarea>
+                    <small class="input-error" ng-show="hasPropertyError(capForm.description, 'required')">Required</small>
+                    <span ng-show="hasError(capForm.description)" class="form-control-feedback"><i class="fa fa-times"></i></span>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-danger" ng-show="!settings.isNew" ng-click="confirmDelete(capability)">Delete</button>
+                <button type="button" class="btn btn-success" ng-disabled="capForm.$pristine || capForm.$invalid" ng-click="confirmSave(capability)">{{settings.saveLabel}}</button>
+            </div>
+        </form>
+    </div>
+</div>
diff --git a/traffic_portal/app/src/common/modules/form/capability/index.js b/traffic_portal/app/src/common/modules/form/capability/index.js
new file mode 100644
index 000000000..fea4d080e
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/capability/index.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.form.capability', [])
+	.controller('FormCapabilityController', require('./FormCapabilityController'));
diff --git a/traffic_portal/app/src/common/modules/form/capability/new/FormNewCapabilityController.js b/traffic_portal/app/src/common/modules/form/capability/new/FormNewCapabilityController.js
new file mode 100644
index 000000000..b023c37ba
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/capability/new/FormNewCapabilityController.js
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+var FormNewCapabilityController = function(capability, $scope, $controller, locationUtils, capabilityService, messageModel) {
+
+	// extends the FormCapabilityController to inherit common methods
+	angular.extend(this, $controller('FormCapabilityController', { capability: capability, $scope: $scope }));
+
+	$scope.capName = 'New';
+
+	$scope.settings = {
+		isNew: true,
+		saveLabel: 'Create'
+	};
+
+	$scope.confirmSave = function(cap) {
+		capabilityService.createCapability(cap).
+			then(function(result) {
+				messageModel.setMessages(result.alerts, true);
+				locationUtils.navigateToPath('/capabilities');
+			});
+	};
+
+};
+
+FormNewCapabilityController.$inject = ['capability', '$scope', '$controller', 'locationUtils', 'capabilityService', 'messageModel'];
+module.exports = FormNewCapabilityController;
diff --git a/traffic_portal/app/src/common/modules/form/capability/new/index.js b/traffic_portal/app/src/common/modules/form/capability/new/index.js
new file mode 100644
index 000000000..96ed51db0
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/capability/new/index.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.form.capability.new', [])
+	.controller('FormNewCapabilityController', require('./FormNewCapabilityController'));
diff --git a/traffic_portal/app/src/common/modules/form/role/FormRoleController.js b/traffic_portal/app/src/common/modules/form/role/FormRoleController.js
new file mode 100644
index 000000000..757c5f9a4
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/role/FormRoleController.js
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+var FormRoleController = function(roles, $scope, formUtils, locationUtils) {
+
+	$scope.role = roles[0];
+
+	$scope.navigateToPath = locationUtils.navigateToPath;
+
+	$scope.hasError = formUtils.hasError;
+
+	$scope.hasPropertyError = formUtils.hasPropertyError;
+
+};
+
+FormRoleController.$inject = ['roles', '$scope', 'formUtils', 'locationUtils'];
+module.exports = FormRoleController;
diff --git a/traffic_portal/app/src/common/modules/form/role/edit/FormEditRoleController.js b/traffic_portal/app/src/common/modules/form/role/edit/FormEditRoleController.js
new file mode 100644
index 000000000..cbcc5721b
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/role/edit/FormEditRoleController.js
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+var FormEditRoleController = function(roles, useCapabilities, $scope, $controller, $uibModal, $anchorScroll, $location, locationUtils, roleService, messageModel) {
+
+	// extends the FormRoleController to inherit common methods
+	angular.extend(this, $controller('FormRoleController', { roles: roles, $scope: $scope }));
+
+	var deleteRole = function(role) {
+		roleService.deleteRole(role.id)
+			.then(function(result) {
+				messageModel.setMessages(result.alerts, true);
+				locationUtils.navigateToPath('/roles');
+			});
+	};
+
+	var save = function(role) {
+		roleService.updateRole(role).
+			then(function(result) {
+				$scope.roleName = angular.copy(role.name);
+				messageModel.setMessages(result.alerts, false);
+				$anchorScroll(); // scrolls window to top
+			});
+	};
+
+	$scope.useCapabilities = (useCapabilities[0]) ? useCapabilities[0].value : 0;
+
+	$scope.roleName = angular.copy($scope.role.name);
+
+	$scope.settings = {
+		isNew: false,
+		saveLabel: 'Update'
+	};
+
+	$scope.viewCapabilities = function() {
+		$location.path($location.path() + '/capabilities');
+	};
+
+	$scope.viewUsers = function() {
+		$location.path($location.path() + '/users');
+	};
+
+	$scope.confirmSave = function(role) {
+		var params = {
+			title: 'Update Role?',
+			message: 'Are you sure you want to update the role?'
+		};
+
+		var modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+			controller: 'DialogConfirmController',
+			size: 'md',
+			resolve: {
+				params: function () {
+					return params;
+				}
+			}
+		});
+		modalInstance.result.then(function() {
+			save(role);
+		}, function () {
+			// do nothing
+		});
+	};
+
+	$scope.confirmDelete = function(role) {
+		var params = {
+			title: 'Delete Role: ' + role.name,
+			key: role.name
+		};
+		var modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/delete/dialog.delete.tpl.html',
+			controller: 'DialogDeleteController',
+			size: 'md',
+			resolve: {
+				params: function () {
+					return params;
+				}
+			}
+		});
+		modalInstance.result.then(function() {
+			deleteRole(role);
+		}, function () {
+			// do nothing
+		});
+	};
+
+};
+
+FormEditRoleController.$inject = ['roles', 'useCapabilities', '$scope', '$controller', '$uibModal', '$anchorScroll', '$location', 'locationUtils', 'roleService', 'messageModel'];
+module.exports = FormEditRoleController;
diff --git a/traffic_portal/app/src/common/modules/form/role/edit/index.js b/traffic_portal/app/src/common/modules/form/role/edit/index.js
new file mode 100644
index 000000000..e5d59b168
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/role/edit/index.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.form.role.edit', [])
+	.controller('FormEditRoleController', require('./FormEditRoleController'));
diff --git a/traffic_portal/app/src/common/modules/form/role/form.role.tpl.html b/traffic_portal/app/src/common/modules/form/role/form.role.tpl.html
new file mode 100644
index 000000000..bc425be90
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/role/form.role.tpl.html
@@ -0,0 +1,68 @@
+<!--
+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.
+-->
+
+<div class="x_panel">
+    <div class="x_title">
+        <ol class="breadcrumb pull-left">
+            <li><a ng-click="navigateToPath('/roles')">Roles</a></li>
+            <li class="active">{{roleName}}</li>
+        </ol>
+        <div class="pull-right" role="group" ng-show="!settings.isNew">
+            <button class="btn btn-primary" title="View Capabilities" ng-if="useCapabilities == '1'" ng-click="viewCapabilities()">View Capabilities</button>
+            <!--<button class="btn btn-primary" title="View Users" ng-click="viewUsers()">View Users</button>-->
+        </div>
+        <div class="clearfix"></div>
+    </div>
+    <div class="x_content">
+        <br>
+        <form name="roleForm" class="form-horizontal form-label-left" novalidate>
+            <div class="form-group" ng-class="{'has-error': hasError(roleForm.name), 'has-feedback': hasError(roleForm.name)}">
+                <label class="control-label col-md-2 col-sm-2 col-xs-12">Name *</label>
+                <div class="col-md-10 col-sm-10 col-xs-12">
+                    <input name="name" type="text" class="form-control" ng-model="role.name" ng-pattern="/^\S*$/" required autofocus>
+                    <small class="input-error" ng-show="hasPropertyError(roleForm.name, 'required')">Required</small>
+                    <small class="input-error" ng-show="hasPropertyError(roleForm.name, 'pattern')">No spaces</small>
+                    <span ng-show="hasError(roleForm.name)" class="form-control-feedback"><i class="fa fa-times"></i></span>
+                </div>
+            </div>
+            <div class="form-group" ng-class="{'has-error': hasError(roleForm.privLevel), 'has-feedback': hasError(roleForm.privLevel)}">
+                <label class="control-label col-md-2 col-sm-2 col-xs-12">Privilege Level *</label>
+                <div class="col-md-10 col-sm-10 col-xs-12">
+                    <input name="privLevel" type="number" class="form-control" ng-model="role.privLevel" ng-maxlength="3" ng-pattern="/^\d+$/" required autofocus>
+                    <small class="input-error" ng-show="hasPropertyError(roleForm.privLevel, 'required')">Required Whole Number</small>
+                    <small class="input-error" ng-show="hasPropertyError(roleForm.privLevel, 'maxlength')">Too Long</small>
+                    <small class="input-error" ng-show="hasPropertyError(roleForm.privLevel, 'pattern')">Whole Number</small>
+                    <span ng-show="hasError(roleForm.privLevel)" class="form-control-feedback"><i class="fa fa-times"></i></span>
+                </div>
+            </div>
+            <div class="form-group" ng-class="{'has-error': hasError(roleForm.description), 'has-feedback': hasError(roleForm.description)}">
+                <label class="control-label col-md-2 col-sm-2 col-xs-12">Description *</label>
+                <div class="col-md-10 col-sm-10 col-xs-12">
+                    <textarea name="description" type="text" class="form-control" ng-model="role.description" rows="10" required autofocus></textarea>
+                    <small class="input-error" ng-show="hasPropertyError(roleForm.description, 'required')">Required</small>
+                    <span ng-show="hasError(roleForm.description)" class="form-control-feedback"><i class="fa fa-times"></i></span>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-danger" ng-show="!settings.isNew" ng-click="confirmDelete(role)">Delete</button>
+                <button type="button" class="btn btn-success" ng-disabled="roleForm.$pristine || roleForm.$invalid" ng-click="confirmSave(role)">{{settings.saveLabel}}</button>
+            </div>
+        </form>
+    </div>
+</div>
diff --git a/traffic_portal/app/src/common/modules/form/role/index.js b/traffic_portal/app/src/common/modules/form/role/index.js
new file mode 100644
index 000000000..ab14d809d
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/role/index.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.form.role', [])
+	.controller('FormRoleController', require('./FormRoleController'));
diff --git a/traffic_portal/app/src/common/modules/form/role/new/FormNewRoleController.js b/traffic_portal/app/src/common/modules/form/role/new/FormNewRoleController.js
new file mode 100644
index 000000000..182b0e963
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/role/new/FormNewRoleController.js
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+var FormNewRoleController = function(roles, $scope, $controller, locationUtils, roleService, messageModel) {
+
+	// extends the FormRoleController to inherit common methods
+	angular.extend(this, $controller('FormRoleController', { roles: roles, $scope: $scope }));
+
+	$scope.roleName = 'New';
+
+	$scope.settings = {
+		isNew: true,
+		saveLabel: 'Create'
+	};
+
+	$scope.confirmSave = function(role) {
+		roleService.createRole(role).
+			then(function(result) {
+				messageModel.setMessages(result.alerts, true);
+				locationUtils.navigateToPath('/roles');
+		});
+	};
+
+};
+
+FormNewRoleController.$inject = ['roles', '$scope', '$controller', 'locationUtils', 'roleService', 'messageModel'];
+module.exports = FormNewRoleController;
diff --git a/traffic_portal/app/src/common/modules/form/role/new/index.js b/traffic_portal/app/src/common/modules/form/role/new/index.js
new file mode 100644
index 000000000..3f94017f0
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/role/new/index.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.form.role.new', [])
+	.controller('FormNewRoleController', require('./FormNewRoleController'));
diff --git a/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html b/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
index 2a6031ee3..875e2a890 100644
--- a/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
@@ -68,6 +68,7 @@
                         <option value="">Select...</option>
                     </select>
                     <small class="input-error" ng-show="hasPropertyError(userForm.role, 'required')">Required</small>
+                    <small ng-show="user.role"><a href="/#!/roles/{{user.role}}" target="_blank">View Details&nbsp;&nbsp;<i class="fa fs-xs fa-external-link"></i></a></small>
                 </div>
             </div>
             <div class="form-group" ng-class="{'has-error': hasError(userForm.tenantId), 'has-feedback': hasError(userForm.tenantId)}">
diff --git a/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html b/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html
index b5356139c..d57ef47e8 100644
--- a/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html
+++ b/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html
@@ -65,7 +65,6 @@
                 </li>
                 <li class="side-menu-category"><a href="javascript:void(0);"><i class="fa fa-sm fa-chevron-right"></i> User Admin</span></a>
                     <ul class="nav child_menu" style="display: none">
-                        <!--<li class="side-menu-category-item" ng-class="{'current-page': isState('trafficPortal.private.capabilities')}"><a href="/#!/capabilities">Capabilities</a></li>-->
                         <li class="side-menu-category-item" ng-class="{'current-page': isState('trafficPortal.private.users')}"><a href="/#!/users">Users</a></li>
                         <li class="side-menu-category-item" ng-class="{'current-page': isState('trafficPortal.private.tenants')}"><a href="/#!/tenants">Tenants</a></li>
                         <li class="side-menu-category-item" ng-class="{'current-page': isState('trafficPortal.private.roles')}"><a href="/#!/roles">Roles</a></li>
diff --git a/traffic_portal/app/src/common/modules/table/capabilities/TableCapabilitiesController.js b/traffic_portal/app/src/common/modules/table/capabilities/TableCapabilitiesController.js
index 0184f41c2..9fabc212c 100644
--- a/traffic_portal/app/src/common/modules/table/capabilities/TableCapabilitiesController.js
+++ b/traffic_portal/app/src/common/modules/table/capabilities/TableCapabilitiesController.js
@@ -17,10 +17,18 @@
  * under the License.
  */
 
-var TableCapabilitiesController = function(capabilities, $scope, $state) {
+var TableCapabilitiesController = function(capabilities, $scope, $state, locationUtils) {
 
 	$scope.capabilities = capabilities;
 
+	$scope.editCapability = function(name) {
+		locationUtils.navigateToPath('/capabilities/' + name);
+	};
+
+	$scope.createCapability = function() {
+		locationUtils.navigateToPath('/capabilities/new');
+	};
+
 	$scope.refresh = function() {
 		$state.reload(); // reloads all the resolves for the view
 	};
@@ -35,5 +43,5 @@ var TableCapabilitiesController = function(capabilities, $scope, $state) {
 
 };
 
-TableCapabilitiesController.$inject = ['capabilities', '$scope', '$state'];
+TableCapabilitiesController.$inject = ['capabilities', '$scope', '$state', 'locationUtils'];
 module.exports = TableCapabilitiesController;
diff --git a/traffic_portal/app/src/common/modules/table/capabilities/table.capabilities.tpl.html b/traffic_portal/app/src/common/modules/table/capabilities/table.capabilities.tpl.html
index bf43e04a3..256f1ab99 100644
--- a/traffic_portal/app/src/common/modules/table/capabilities/table.capabilities.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/capabilities/table.capabilities.tpl.html
@@ -23,6 +23,7 @@
             <li class="active">Capabilities</li>
         </ol>
         <div class="pull-right">
+            <button class="btn btn-primary" title="Create Capability" ng-click="createCapability()"><i class="fa fa-plus"></i></button>
             <button class="btn btn-default" title="Refresh" ng-click="refresh()"><i class="fa fa-refresh"></i></button>
         </div>
         <div class="clearfix"></div>
@@ -37,9 +38,9 @@
             </tr>
             </thead>
             <tbody>
-            <tr ng-repeat="capability in ::capabilities">
-                <td>{{::capability.name}}</td>
-                <td>{{::capability.description}}</td>
+            <tr ng-click="editCapability(c.name)" ng-repeat="c in ::capabilities">
+                <td>{{::c.name}}</td>
+                <td>{{::c.description}}</td>
             </tr>
             </tbody>
         </table>
diff --git a/traffic_portal/app/src/common/modules/table/capabilityEndpoints/TableCapabilityEndpointsController.js b/traffic_portal/app/src/common/modules/table/capabilityEndpoints/TableCapabilityEndpointsController.js
new file mode 100644
index 000000000..be099eff9
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/capabilityEndpoints/TableCapabilityEndpointsController.js
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+var TableCapabilityEndpointsController = function(capability, capEndpoints, $scope, $state, locationUtils) {
+
+	$scope.capability = capability;
+
+	$scope.capEndpoints = capEndpoints;
+
+	$scope.refresh = function() {
+		$state.reload(); // reloads all the resolves for the view
+	};
+
+	$scope.navigateToPath = locationUtils.navigateToPath;
+
+	angular.element(document).ready(function () {
+		$('#capEndpointsTable').dataTable({
+			"aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
+			"iDisplayLength": 25,
+			"aaSorting": []
+		});
+	});
+
+};
+
+TableCapabilityEndpointsController.$inject = ['capability', 'capEndpoints', '$scope', '$state', 'locationUtils'];
+module.exports = TableCapabilityEndpointsController;
diff --git a/traffic_portal/app/src/common/modules/table/capabilityEndpoints/index.js b/traffic_portal/app/src/common/modules/table/capabilityEndpoints/index.js
new file mode 100644
index 000000000..3291862bd
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/capabilityEndpoints/index.js
@@ -0,0 +1,22 @@
+
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.table.capabilityEndpoints', [])
+	.controller('TableCapabilityEndpointsController', require('./TableCapabilityEndpointsController'));
diff --git a/traffic_portal/app/src/common/modules/table/capabilityEndpoints/table.capabilityEndpoints.tpl.html b/traffic_portal/app/src/common/modules/table/capabilityEndpoints/table.capabilityEndpoints.tpl.html
new file mode 100644
index 000000000..43075880c
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/capabilityEndpoints/table.capabilityEndpoints.tpl.html
@@ -0,0 +1,52 @@
+<!--
+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.
+-->
+
+<div class="x_panel">
+    <div class="x_title">
+        <ol class="breadcrumb pull-left">
+            <li><a ng-click="navigateToPath('/capabilities')">Capabilities</a></li>
+            <li><a ng-click="navigateToPath('/capabilities/' + capability.id)">{{::capability.name}}</a></li>
+            <li class="active">Endpoints</li>
+        </ol>
+        <div class="pull-right">
+            <button class="btn btn-default" title="Refresh" ng-click="refresh()"><i class="fa fa-refresh"></i></button>
+        </div>
+        <div class="clearfix"></div>
+    </div>
+    <div class="x_content">
+        <br>
+        <table id="capEndpointsTable" class="table responsive-utilities jambo_table">
+            <thead>
+            <tr class="headings">
+                <th>HTTP Method</th>
+                <th>Endpoint</th>
+                <th>Capability</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr ng-repeat="e in ::capEndpoints">
+                <td data-search="^{{::e.httpMethod}}$">{{::e.httpMethod}}</td>
+                <td data-search="^{{::e.httpRoute}}$">{{::e.httpRoute}}</td>
+                <td data-search="^{{::e.capability}}$">{{::e.capability}}</td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+
diff --git a/traffic_portal/app/src/common/modules/table/capabilityUsers/TableCapabilityUsersController.js b/traffic_portal/app/src/common/modules/table/capabilityUsers/TableCapabilityUsersController.js
new file mode 100644
index 000000000..48b6372cd
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/capabilityUsers/TableCapabilityUsersController.js
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+var TableCapabilityUsersController = function(capability, capUsers, $scope, $state, locationUtils) {
+
+	$scope.capability = capability;
+
+	$scope.capUsers = capUsers;
+
+	$scope.editUser = function(id) {
+		locationUtils.navigateToPath('/users/' + id);
+	};
+
+	$scope.refresh = function() {
+		$state.reload(); // reloads all the resolves for the view
+	};
+
+	$scope.navigateToPath = locationUtils.navigateToPath;
+
+	angular.element(document).ready(function () {
+		$('#capUsersTable').dataTable({
+			"aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
+			"iDisplayLength": 25,
+			"aaSorting": []
+		});
+	});
+
+};
+
+TableCapabilityUsersController.$inject = ['capability', 'capUsers', '$scope', '$state', 'locationUtils'];
+module.exports = TableCapabilityUsersController;
diff --git a/traffic_portal/app/src/common/modules/table/capabilityUsers/index.js b/traffic_portal/app/src/common/modules/table/capabilityUsers/index.js
new file mode 100644
index 000000000..a1b336e97
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/capabilityUsers/index.js
@@ -0,0 +1,22 @@
+
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.table.capabilityUsers', [])
+	.controller('TableCapabilityUsersController', require('./TableCapabilityUsersController'));
diff --git a/traffic_portal/app/src/common/modules/table/capabilityUsers/table.capabilityUsers.tpl.html b/traffic_portal/app/src/common/modules/table/capabilityUsers/table.capabilityUsers.tpl.html
new file mode 100644
index 000000000..50c6f2df2
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/capabilityUsers/table.capabilityUsers.tpl.html
@@ -0,0 +1,56 @@
+<!--
+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.
+-->
+
+<div class="x_panel">
+    <div class="x_title">
+        <ol class="breadcrumb pull-left">
+            <li><a ng-click="navigateToPath('/capabilities')">Capabilities</a></li>
+            <li><a ng-click="navigateToPath('/capabilities/' + capability.id)">{{::capability.name}}</a></li>
+            <li class="active">Users</li>
+        </ol>
+        <div class="pull-right">
+            <button class="btn btn-default" title="Refresh" ng-click="refresh()"><i class="fa fa-refresh"></i></button>
+        </div>
+        <div class="clearfix"></div>
+    </div>
+    <div class="x_content">
+        <br>
+        <table id="capUsersTable" class="table responsive-utilities jambo_table">
+            <thead>
+            <tr class="headings">
+                <th>Full Name</th>
+                <th>Username</th>
+                <th>Email</th>
+                <th>Tenant</th>
+                <th>Role</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr ng-click="editUser(u.id)" ng-repeat="u in ::capUsers">
+                <td data-search="^{{::u.fullName}}$">{{::u.fullName}}</td>
+                <td data-search="^{{::u.username}}$">{{::u.username}}</td>
+                <td data-search="^{{::u.email}}$">{{::u.email}}</td>
+                <td data-search="^{{::u.tenant}}$">{{::u.tenant}}</td>
+                <td data-search="^{{::u.rolename}}$">{{::u.rolename}}</td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+
diff --git a/traffic_portal/app/src/common/modules/table/roleCapabilities/TableAssignCapabilitiesController.js b/traffic_portal/app/src/common/modules/table/roleCapabilities/TableAssignCapabilitiesController.js
new file mode 100644
index 000000000..66f6e7efe
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/roleCapabilities/TableAssignCapabilitiesController.js
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+var TableAssignCapabilitiesController = function(role, capabilities, assignedCapabilities, $scope, $uibModalInstance) {
+
+	var selectedCapabilities = [];
+
+	var addAll = function() {
+		markVisibleCapabilities(true);
+	};
+
+	var removeAll = function() {
+		markVisibleCapabilities(false);
+	};
+
+	var markVisibleCapabilities = function(selected) {
+		var visibleCapabilityNames = $('#assignCapabilitiesTable tr.cap-row').map(
+			function() {
+				return $(this).attr('id'); // the cap name is being stored as the id on the row
+			}).get();
+		$scope.selectedCapabilities = _.map(capabilities, function(c) {
+			if (visibleCapabilityNames.includes(c.name)) {
+				c['selected'] = selected;
+			}
+			return c;
+		});
+		updateSelectedCount();
+	};
+
+	var updateSelectedCount = function() {
+		selectedCapabilities = _.filter($scope.selectedCapabilities, function(c) { return c['selected'] == true; } );
+		$('div.selected-count').html('<b>' + selectedCapabilities.length + ' capabilities selected</b>');
+	};
+
+	$scope.role = role;
+
+	$scope.selectedCapabilities = _.map(capabilities, function(c) {
+		var isAssigned = _.find(assignedCapabilities, function(assignedCap) {
+			return assignedCap == c.name
+		});
+		if (isAssigned) {
+			c['selected'] = true;
+		}
+		return c;
+	});
+
+	$scope.selectAll = function($event) {
+		var checkbox = $event.target;
+		if (checkbox.checked) {
+			addAll();
+		} else {
+			removeAll();
+		}
+	};
+
+	$scope.onChange = function() {
+		updateSelectedCount();
+	};
+
+	$scope.submit = function() {
+		var selectedCapabilityNames = _.pluck(selectedCapabilities, 'name');
+		$uibModalInstance.close(selectedCapabilityNames);
+	};
+
+	$scope.cancel = function () {
+		$uibModalInstance.dismiss('cancel');
+	};
+
+	angular.element(document).ready(function () {
+		var assignCapabilitiesTable = $('#assignCapabilitiesTable').dataTable({
+			"scrollY": "60vh",
+			"paging": false,
+			"order": [[ 1, 'asc' ]],
+			"dom": '<"selected-count">frtip',
+			"columnDefs": [
+				{ 'orderable': false, 'targets': 0 },
+				{ "width": "5%", "targets": 0 }
+			],
+			"stateSave": false
+		});
+		assignCapabilitiesTable.on( 'search.dt', function () {
+			$("#selectAllCB").removeAttr("checked"); // uncheck the all box when filtering
+		} );
+		updateSelectedCount();
+	});
+
+};
+
+TableAssignCapabilitiesController.$inject = ['role', 'capabilities', 'assignedCapabilities', '$scope', '$uibModalInstance'];
+module.exports = TableAssignCapabilitiesController;
diff --git a/traffic_portal/app/src/common/modules/table/roleCapabilities/TableRoleCapabilitiesController.js b/traffic_portal/app/src/common/modules/table/roleCapabilities/TableRoleCapabilitiesController.js
new file mode 100644
index 000000000..7b226586e
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/roleCapabilities/TableRoleCapabilitiesController.js
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+var TableRoleCapabilitiesController = function(roles, $scope, $state, $uibModal, locationUtils, capabilityService, roleService, messageModel) {
+
+	$scope.role = roles[0];
+
+	$scope.editCapability = function(name) {
+		locationUtils.navigateToPath('/capabilities/' + name);
+	};
+
+	$scope.selectCapabilities = function() {
+		var modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/table/roleCapabilities/table.assignCapabilities.tpl.html',
+			controller: 'TableAssignCapabilitiesController',
+			size: 'lg',
+			resolve: {
+				role: function() {
+					return $scope.role;
+				},
+				capabilities: function(capabilityService) {
+					return capabilityService.getCapabilities();
+				},
+				assignedCapabilities: function() {
+					return $scope.role.capabilities;
+				}
+			}
+		});
+		modalInstance.result.then(function(selectedCapabilities) {
+			$scope.role.capabilities = selectedCapabilities;
+			roleService.updateRole($scope.role)
+				.then(
+					function(result) {
+						messageModel.setMessages(result.alerts, false);
+						$scope.refresh();
+					}
+				);
+		}, function () {
+			// do nothing
+		});
+	};
+
+	$scope.confirmRemoveCapability = function(capToRemove, $event) {
+		$event.stopPropagation(); // this kills the click event so it doesn't trigger anything else
+
+		var params = {
+			title: 'Remove Capabilty from Role?',
+			message: 'Are you sure you want to remove ' + capToRemove + ' from this role?'
+		};
+		var modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+			controller: 'DialogConfirmController',
+			size: 'md',
+			resolve: {
+				params: function () {
+					return params;
+				}
+			}
+		});
+		modalInstance.result.then(function() {
+			$scope.role.capabilities = _.filter($scope.role.capabilities, function(cap) { return cap != capToRemove; });
+			roleService.updateRole($scope.role)
+				.then(
+					function(result) {
+						messageModel.setMessages(result.alerts, false);
+						$scope.refresh();
+					}
+				);
+		}, function () {
+			// do nothing
+		});
+	};
+
+	$scope.refresh = function() {
+		$state.reload(); // reloads all the resolves for the view
+	};
+
+	$scope.navigateToPath = locationUtils.navigateToPath;
+
+	angular.element(document).ready(function () {
+		$('#capabilitiesTable').dataTable({
+			"aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
+			"iDisplayLength": 25,
+			"columnDefs": [
+				{ 'orderable': false, 'targets': 1 }
+			],
+			"aaSorting": []
+		});
+	});
+
+};
+
+TableRoleCapabilitiesController.$inject = ['roles', '$scope', '$state', '$uibModal', 'locationUtils', 'capabilityService', 'roleService', 'messageModel'];
+module.exports = TableRoleCapabilitiesController;
diff --git a/traffic_portal/app/src/common/modules/table/roleCapabilities/index.js b/traffic_portal/app/src/common/modules/table/roleCapabilities/index.js
new file mode 100644
index 000000000..e8455b218
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/roleCapabilities/index.js
@@ -0,0 +1,23 @@
+
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.table.roleCapabilities', [])
+	.controller('TableRoleCapabilitiesController', require('./TableRoleCapabilitiesController'))
+	.controller('TableAssignCapabilitiesController', require('./TableAssignCapabilitiesController'));
diff --git a/traffic_portal/app/src/common/modules/table/roleCapabilities/table.assignCapabilities.tpl.html b/traffic_portal/app/src/common/modules/table/roleCapabilities/table.assignCapabilities.tpl.html
new file mode 100644
index 000000000..81bc58a5e
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/roleCapabilities/table.assignCapabilities.tpl.html
@@ -0,0 +1,43 @@
+<!--
+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.
+-->
+
+<div class="modal-header">
+    <button type="button" class="close" ng-click="cancel()"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+    <h3 class="modal-title">Assign capabilities to the {{::role.name}} role</h3>
+</div>
+<div class="modal-body">
+    <table id="assignCapabilitiesTable" class="table responsive-utilities jambo_table" style="table-layout:fixed; width:100%;">
+        <thead>
+        <tr class="headings">
+            <th style="padding-left: 10px;"><input id="selectAllCB" type="checkbox" ng-click="selectAll($event)"></th>
+            <th>Name</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr id="{{::c.name}}" class="cap-row" ng-repeat="c in ::selectedCapabilities">
+            <td><input type="checkbox" ng-checked="c.selected" ng-model="c.selected" ng-change="onChange()"></td>
+            <td data-search="^{{::c.name}}$">{{::c.name}}</td>
+        </tr>
+        </tbody>
+    </table>
+</div>
+<div class="modal-footer">
+    <button class="btn btn-link" ng-click="cancel()">cancel</button>
+    <button class="btn btn-primary" ng-click="submit()">Submit</button>
+</div>
diff --git a/traffic_portal/app/src/common/modules/table/roleCapabilities/table.roleCapabilities.tpl.html b/traffic_portal/app/src/common/modules/table/roleCapabilities/table.roleCapabilities.tpl.html
new file mode 100644
index 000000000..911bff104
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/roleCapabilities/table.roleCapabilities.tpl.html
@@ -0,0 +1,53 @@
+<!--
+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.
+-->
+
+<div class="x_panel">
+    <div class="x_title">
+        <ol class="breadcrumb pull-left">
+            <li><a ng-click="navigateToPath('/roles')">Roles</a></li>
+            <li><a ng-click="navigateToPath('/roles/' + role.id)">{{::role.name}}</a></li>
+            <li class="active">Capabilities</li>
+        </ol>
+        <div class="pull-right" role="group">
+            <button class="btn btn-primary" title="Add Capabilties to Role" ng-click="selectCapabilities()"><i class="fa fa-plus"></i></button>
+            <button class="btn btn-default" title="Refresh" ng-click="refresh()"><i class="fa fa-refresh"></i></button>
+        </div>
+        <div class="clearfix"></div>
+    </div>
+    <div class="x_content">
+        <br>
+        <table id="capabilitiesTable" class="table responsive-utilities jambo_table">
+            <thead>
+            <tr class="headings">
+                <th>Name</th>
+                <th style="text-align: right;">Actions</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr ng-click="editCapability(c)" ng-repeat="c in ::role.capabilities">
+                <td data-search="^{{::c}}$">{{::c}}</td>
+                <td style="text-align: right;">
+                    <a class="link action-link" title="Remove Capability from Role" ng-click="confirmRemoveCapability(c, $event)"><i class="fa fa-sm fa-chain-broken"></i></a>
+                </td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+
diff --git a/traffic_portal/app/src/common/modules/table/roleUsers/TableRoleUsersController.js b/traffic_portal/app/src/common/modules/table/roleUsers/TableRoleUsersController.js
new file mode 100644
index 000000000..7685ab646
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/roleUsers/TableRoleUsersController.js
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+var TableRoleUsersController = function(roles, roleUsers, $scope, $state, locationUtils) {
+
+	$scope.role = roles[0];
+
+	$scope.roleUsers = roleUsers;
+
+	$scope.editUser = function(id) {
+		locationUtils.navigateToPath('/users/' + id);
+	};
+
+	$scope.refresh = function() {
+		$state.reload(); // reloads all the resolves for the view
+	};
+
+	$scope.navigateToPath = locationUtils.navigateToPath;
+
+	angular.element(document).ready(function () {
+		$('#roleUsersTable').dataTable({
+			"aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
+			"iDisplayLength": 25,
+			"aaSorting": []
+		});
+	});
+
+};
+
+TableRoleUsersController.$inject = ['roles', 'roleUsers', '$scope', '$state', 'locationUtils'];
+module.exports = TableRoleUsersController;
diff --git a/traffic_portal/app/src/common/modules/table/roleUsers/index.js b/traffic_portal/app/src/common/modules/table/roleUsers/index.js
new file mode 100644
index 000000000..cfa775f7f
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/roleUsers/index.js
@@ -0,0 +1,22 @@
+
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.table.roleUsers', [])
+	.controller('TableRoleUsersController', require('./TableRoleUsersController'));
diff --git a/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html b/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html
new file mode 100644
index 000000000..c1e506374
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/roleUsers/table.roleUsers.tpl.html
@@ -0,0 +1,56 @@
+<!--
+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.
+-->
+
+<div class="x_panel">
+    <div class="x_title">
+        <ol class="breadcrumb pull-left">
+            <li><a ng-click="navigateToPath('/roles')">Roles</a></li>
+            <li><a ng-click="navigateToPath('/roles/' + role.id)">{{::role.name}}</a></li>
+            <li class="active">Users</li>
+        </ol>
+        <div class="pull-right">
+            <button class="btn btn-default" title="Refresh" ng-click="refresh()"><i class="fa fa-refresh"></i></button>
+        </div>
+        <div class="clearfix"></div>
+    </div>
+    <div class="x_content">
+        <br>
+        <table id="roleUsersTable" class="table responsive-utilities jambo_table">
+            <thead>
+            <tr class="headings">
+                <th>Full Name</th>
+                <th>Username</th>
+                <th>Email</th>
+                <th>Tenant</th>
+                <th>Role</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr ng-click="editUser(u.id)" ng-repeat="u in ::roleUsers">
+                <td data-search="^{{::u.fullName}}$">{{::u.fullName}}</td>
+                <td data-search="^{{::u.username}}$">{{::u.username}}</td>
+                <td data-search="^{{::u.email}}$">{{::u.email}}</td>
+                <td data-search="^{{::u.tenant}}$">{{::u.tenant}}</td>
+                <td data-search="^{{::u.rolename}}$">{{::u.rolename}}</td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+
diff --git a/traffic_portal/app/src/common/modules/table/roles/TableRolesController.js b/traffic_portal/app/src/common/modules/table/roles/TableRolesController.js
index 22d3cb157..1bb7b9753 100644
--- a/traffic_portal/app/src/common/modules/table/roles/TableRolesController.js
+++ b/traffic_portal/app/src/common/modules/table/roles/TableRolesController.js
@@ -17,10 +17,18 @@
  * under the License.
  */
 
-var TableRolesController = function(roles, $scope, $state) {
+var TableRolesController = function(roles, $scope, $state, locationUtils) {
 
 	$scope.roles = roles;
 
+	$scope.editRole = function(id) {
+		locationUtils.navigateToPath('/roles/' + id);
+	};
+
+	$scope.createRole = function() {
+		locationUtils.navigateToPath('/roles/new');
+	};
+
 	$scope.refresh = function() {
 		$state.reload(); // reloads all the resolves for the view
 	};
@@ -35,5 +43,5 @@ var TableRolesController = function(roles, $scope, $state) {
 
 };
 
-TableRolesController.$inject = ['roles', '$scope', '$state'];
+TableRolesController.$inject = ['roles', '$scope', '$state', 'locationUtils'];
 module.exports = TableRolesController;
diff --git a/traffic_portal/app/src/common/modules/table/roles/table.roles.tpl.html b/traffic_portal/app/src/common/modules/table/roles/table.roles.tpl.html
index a3193a54a..c27a25370 100644
--- a/traffic_portal/app/src/common/modules/table/roles/table.roles.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/roles/table.roles.tpl.html
@@ -23,6 +23,7 @@
             <li class="active">Roles</li>
         </ol>
         <div class="pull-right">
+            <button class="btn btn-primary" title="Create Role" ng-click="createRole()"><i class="fa fa-plus"></i></button>
             <button class="btn btn-default" title="Refresh" ng-click="refresh()"><i class="fa fa-refresh"></i></button>
         </div>
         <div class="clearfix"></div>
@@ -32,16 +33,16 @@
         <table id="rolesTable" class="table responsive-utilities jambo_table">
             <thead>
             <tr class="headings">
-                <th>name</th>
-                <th>privilege level</th>
-                <th>description</th>
+                <th>Name</th>
+                <th>Privilege Level</th>
+                <th>Description</th>
             </tr>
             </thead>
             <tbody>
-            <tr ng-repeat="role in ::roles">
-                <td>{{::role.name}}</td>
-                <td>{{::role.privLevel}}</td>
-                <td>{{::role.description}}</td>
+            <tr ng-click="editRole(r.id)" ng-repeat="r in ::roles">
+                <td>{{::r.name}}</td>
+                <td>{{::r.privLevel}}</td>
+                <td>{{::r.description}}</td>
             </tr>
             </tbody>
         </table>
diff --git a/traffic_portal/app/src/modules/private/capabilities/edit/index.js b/traffic_portal/app/src/modules/private/capabilities/edit/index.js
new file mode 100644
index 000000000..b51cd6627
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/capabilities/edit/index.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.private.capabilities.edit', [])
+	.config(function($stateProvider, $urlRouterProvider) {
+		$stateProvider
+			.state('trafficPortal.private.capabilities.edit', {
+				url: '/{capName}',
+				views: {
+					capabilitiesContent: {
+						templateUrl: 'common/modules/form/capability/form.capability.tpl.html',
+						controller: 'FormEditCapabilityController',
+						resolve: {
+							capability: function($stateParams, capabilityService) {
+								return capabilityService.getCapability($stateParams.capName);
+							}
+						}
+					}
+				}
+			})
+		;
+		$urlRouterProvider.otherwise('/');
+	});
diff --git a/traffic_portal/app/src/modules/private/capabilities/endpoints/index.js b/traffic_portal/app/src/modules/private/capabilities/endpoints/index.js
new file mode 100644
index 000000000..323a9fb11
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/capabilities/endpoints/index.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.private.capabilities.endpoints', [])
+	.config(function($stateProvider, $urlRouterProvider) {
+		$stateProvider
+			.state('trafficPortal.private.capabilities.endpoints', {
+				url: '/{capName}/endpoints',
+				views: {
+					capabilitiesContent: {
+						templateUrl: 'common/modules/table/capabilityEndpoints/table.capabilityEndpoints.tpl.html',
+						controller: 'TableCapabilityEndpointsController',
+						resolve: {
+							capability: function($stateParams, capabilityService) {
+								return capabilityService.getCapability($stateParams.capName);
+							},
+							capEndpoints: function($stateParams, endpointService) {
+								return endpointService.getEndpoints({ capability: $stateParams.capName });
+							}
+						}
+					}
+				}
+			})
+		;
+		$urlRouterProvider.otherwise('/');
+	});
diff --git a/traffic_portal/app/src/modules/private/capabilities/new/index.js b/traffic_portal/app/src/modules/private/capabilities/new/index.js
new file mode 100644
index 000000000..c6980e3a0
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/capabilities/new/index.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.private.capabilities.new', [])
+	.config(function($stateProvider, $urlRouterProvider) {
+		$stateProvider
+			.state('trafficPortal.private.capabilities.new', {
+				url: '/new',
+				views: {
+					capabilitiesContent: {
+						templateUrl: 'common/modules/form/capability/form.capability.tpl.html',
+						controller: 'FormNewCapabilityController',
+						resolve: {
+							capability: function() {
+								return {};
+							}
+						}
+					}
+				}
+			})
+		;
+		$urlRouterProvider.otherwise('/');
+	});
diff --git a/traffic_portal/app/src/modules/private/capabilities/users/index.js b/traffic_portal/app/src/modules/private/capabilities/users/index.js
new file mode 100644
index 000000000..42f4d554d
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/capabilities/users/index.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.private.capabilities.users', [])
+	.config(function($stateProvider, $urlRouterProvider) {
+		$stateProvider
+			.state('trafficPortal.private.capabilities.users', {
+				url: '/{capName}/users',
+				views: {
+					capabilitiesContent: {
+						templateUrl: 'common/modules/table/capabilityUsers/table.capabilityUsers.tpl.html',
+						controller: 'TableCapabilityUsersController',
+						resolve: {
+							capability: function($stateParams, capabilityService) {
+								return capabilityService.getCapability($stateParams.capName);
+							},
+							capUsers: function($stateParams, userService) {
+								return userService.getUsers({ capability: $stateParams.capName });
+							}
+						}
+					}
+				}
+			})
+		;
+		$urlRouterProvider.otherwise('/');
+	});
diff --git a/traffic_portal/app/src/modules/private/roles/capabilities/index.js b/traffic_portal/app/src/modules/private/roles/capabilities/index.js
new file mode 100644
index 000000000..bc012bfef
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/roles/capabilities/index.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.private.roles.capabilities', [])
+	.config(function($stateProvider, $urlRouterProvider) {
+		$stateProvider
+			.state('trafficPortal.private.roles.capabilities', {
+				url: '/{roleId}/capabilities',
+				views: {
+					rolesContent: {
+						templateUrl: 'common/modules/table/roleCapabilities/table.roleCapabilities.tpl.html',
+						controller: 'TableRoleCapabilitiesController',
+						resolve: {
+							roles: function($stateParams, roleService) {
+								return roleService.getRoles({ id: $stateParams.roleId });
+							}
+						}
+					}
+				}
+			})
+		;
+		$urlRouterProvider.otherwise('/');
+	});
diff --git a/traffic_portal/app/src/modules/private/roles/edit/index.js b/traffic_portal/app/src/modules/private/roles/edit/index.js
new file mode 100644
index 000000000..64eeb38b6
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/roles/edit/index.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.private.roles.edit', [])
+	.config(function($stateProvider, $urlRouterProvider) {
+		$stateProvider
+			.state('trafficPortal.private.roles.edit', {
+				url: '/{roleId:[0-9]{1,8}}',
+				views: {
+					rolesContent: {
+						templateUrl: 'common/modules/form/role/form.role.tpl.html',
+						controller: 'FormEditRoleController',
+						resolve: {
+							roles: function($stateParams, roleService) {
+								return roleService.getRoles({ id: $stateParams.roleId });
+							},
+							useCapabilities: function(parameterService) {
+								return parameterService.getParameters({ name: 'use_capabilities', configFile: 'global' });
+							}
+						}
+					}
+				}
+			})
+		;
+		$urlRouterProvider.otherwise('/');
+	});
diff --git a/traffic_portal/app/src/modules/private/roles/new/index.js b/traffic_portal/app/src/modules/private/roles/new/index.js
new file mode 100644
index 000000000..941a69743
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/roles/new/index.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.private.roles.new', [])
+	.config(function($stateProvider, $urlRouterProvider) {
+		$stateProvider
+			.state('trafficPortal.private.roles.new', {
+				url: '/new',
+				views: {
+					rolesContent: {
+						templateUrl: 'common/modules/form/role/form.role.tpl.html',
+						controller: 'FormNewRoleController',
+						resolve: {
+							roles: function() {
+								return [ {} ];
+							}
+						}
+					}
+				}
+			})
+		;
+		$urlRouterProvider.otherwise('/');
+	});
diff --git a/traffic_portal/app/src/modules/private/roles/users/index.js b/traffic_portal/app/src/modules/private/roles/users/index.js
new file mode 100644
index 000000000..38509f7ea
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/roles/users/index.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+module.exports = angular.module('trafficPortal.private.roles.users', [])
+	.config(function($stateProvider, $urlRouterProvider) {
+		$stateProvider
+			.state('trafficPortal.private.roles.users', {
+				url: '/{roleId}/users',
+				views: {
+					rolesContent: {
+						templateUrl: 'common/modules/table/roleUsers/table.roleUsers.tpl.html',
+						controller: 'TableRoleUsersController',
+						resolve: {
+							roles: function($stateParams, roleService) {
+								return roleService.getRoles({ id: $stateParams.roleId });
+							},
+							roleUsers: function($stateParams, userService) {
+								return userService.getUsers({ roleId: $stateParams.roleId });
+							}
+						}
+					}
+				}
+			})
+		;
+		$urlRouterProvider.otherwise('/');
+	});


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services