Skip to content

Commit

Permalink
Merge pull request #194 from alces-flight/teams/permissions
Browse files Browse the repository at this point in the history
Teams - limit team member permissions
  • Loading branch information
timalces authored Mar 13, 2024
2 parents d852130 + c88f5ff commit b11243c
Show file tree
Hide file tree
Showing 34 changed files with 270 additions and 113 deletions.
4 changes: 4 additions & 0 deletions app/assets/stylesheets/irv/_interactive_canvas_view.scss
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@
-moz-box-shadow: 3px 3px 12px 3px rgba(0,0,0,0.3);
-webkit-box-shadow: 3px 3px 12px 3px rgba(0,0,0,0.3);
box-shadow: 3px 3px 12px 3px rgba(0,0,0,0.3);

.menu-spacer:last-child {
display: none; /* Hide spacer if no subsequent options */
}
}

.disabled_context_menu_item
Expand Down
48 changes: 19 additions & 29 deletions app/controllers/api/v1/users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Api::V1::UsersController < Api::V1::ApplicationController
load_and_authorize_resource :user, :class => User, except: [:current, :can_i?]
load_and_authorize_resource :user, :class => User, except: [:current, :permissions]

def index
@users = @users.map {|user| Api::V1::UserPresenter.new(user)}
Expand Down Expand Up @@ -38,36 +38,26 @@ def destroy
end

#
# GET /api/v1/users/can_i
# GET /api/v1/users/permissions
#
# Endpoint for cancan check - this just passes the "can" request on to the
# cancan ability checker - used to check yourself and your own abilities.
# Endpoint for specifying what permissions each team role/ being root provides.
# This is based on the assumption that such permissions are based purely
# on team role for the given object (or being root).
#
def can_i?
# On the permissions params, this action should receive a structure of the
# following form
#
# {
# "permissions" => {
# "manage" => {"0" => "HwRack", "1" => "Device"},
# "read" => {"0" => "Device"},
# "move" => {"0" => "Device"},
# }
# }

result = {}
params[:permissions].each do |rbac_action,rbac_resources|
result[rbac_action] = {}
rbac_resources.each do |_, rbac_resource|
if rbac_resource == "all"
result[rbac_action][rbac_resource] = current_user.can?(rbac_action.to_sym, :all)
elsif rbac_resource.safe_constantize
result[rbac_action][rbac_resource] = current_user.can?(rbac_action.to_sym, rbac_resource.safe_constantize)
else
result[rbac_action][rbac_resource] = false
end
end
end
def permissions
admins = %w(superAdmin admin)
all = admins + ["member"]
result = {
manage: {
racks: admins, devices: admins, chassis: admins
},
move: {
racks: [], devices: admins, chassis: admins
},
view: {
racks: all, devices: all, chassis: all
}
}
render json: result
end

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/cluster_types_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def index
result = SyncAllClusterTypesJob.perform_now(@cloud_service_config, use_cache)
flash.now.alert = result.error_message unless result.success?
end
@valid_teams = current_user.teams.meets_cluster_credit_requirement
@valid_teams = current_user.teams_where_admin.meets_cluster_credit_requirement
@unavailable_teams = current_user.teams.where.not(id: @valid_teams.pluck(:id))
@all_teams = current_user.teams.reorder(:name)
@team = Team.find(params[:team_id]) if params[:team_id]
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/team_roles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def update

if result.success?
flash[:info] = "Successfully updated team role"
redirect_to team_team_roles_path(@team_role.team, @team_role)
redirect_to @team_role.user == current_user ? teams_path : team_team_roles_path(@team_role.team)
else
flash[:alert] = "Unable to update team role"
render action: :edit
Expand Down
32 changes: 9 additions & 23 deletions app/javascript/canvas/common/util/RBAC.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import Events from 'canvas/common/util/Events';

// RBAC = Rule Based Access Control
//
// RBAC queries the api action /-/api/v1/users/can_i with a
// specific set of permissions (getPermissionsToQuery) on construction time.
// RBAC queries the api action /-/api/v1/users/permissions on construction time.
// Then, via the function can_i, the results obtained from the api call are queried.
// This class is shared between the DCRV and DCPV.
class RBAC {
static PATH = '/api/v1/users/can_i';
static PATH = '/api/v1/users/permissions';

constructor({onSuccess}) {
this.onSuccessCallback = onSuccess;
Expand All @@ -18,7 +16,6 @@ class RBAC {
headers : {'X-CSRF-Token': $$('meta[name="csrf-token"]')[0].getAttribute('content')},
url : RBAC.PATH,
method : 'get',
data : this.getPermissionsToQuery(),
onSuccess : this.permisionsReceived
}).send();
}
Expand All @@ -29,34 +26,23 @@ class RBAC {
}

permisionsReceived(permissions) {
this.debug("recevied permissions");
this.debug("received permissions");
this.permissions = permissions;
if (this.onSuccessCallback) {
this.onSuccessCallback()
}
}

getPermissionsToQuery() {
return {
permissions:
{
manage: ["HwRack", "Device", "Chassis"],
move: ["Device", "Chassis"],
view: ["all"]
}
};
can_i(action, resource, teamRole) {
return this.permissions[action][resource].includes(teamRole);
}

can_i(action, resource) {
return this.permissions[action][resource] === true;
can_i_move_device(device) {
return this.can_i("move", "devices", device.teamRole) || this.can_i("move", "chassis", device.teamRole);
}

can_i_move_devices() {
return this.can_i("move", "Device") || this.can_i("move", "Chassis");
}

can_i_manage_devices() {
return this.can_i("manage", "Device") || this.can_i("manage", "Chassis");
can_i_manage_device(device) {
return this.can_i("manage", "devices", device.teamRole) || this.can_i("manage", "chassis", device.teamRole);
}

debug(...msg) {
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/canvas/irv/view/ContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ class ContextMenu {
for (var option of Array.from(total_options)) {
// If the option has the attribute RBAC defined, then query the @model.RBAC object
// to see if such permission has been granted. Otherwise, continue to the next option.
if (option.RBAC != null) {
if (!this.model.RBAC.can_i(option.RBAC.action,option.RBAC.resource)) { continue; }
if (option.rbacAction != null) {
if (!this.model.RBAC.can_i(option.rbacAction, option_set, device.teamRole)) { continue; }
}

if (option.availableToBuildStatuses !== undefined && option.availableToBuildStatuses.indexOf(device.buildStatus) === -1) {
Expand Down
5 changes: 2 additions & 3 deletions app/javascript/canvas/irv/view/Rack.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ class Rack extends RackObject {
this.selected = false;
this.visible = true;
this.assets = [];

this.addImageLink();

const len = def.chassis.length;
Expand Down Expand Up @@ -307,14 +306,14 @@ class Rack extends RackObject {
})();
}

draw(show_u_labels, show_name_label, show_owner_label) {
draw(show_u_labels, show_name_label) {
Profiler.begin(Profiler.DEBUG, this.draw);
// clear
for (var asset of Array.from(this.assets)) { RackObject.RACK_GFX.remove(asset); }
this.assets = [];

// add labels as necessary
this.showOwnerLabel(show_owner_label);
this.showOwnerLabel(show_name_label);
this.showNameLabel(show_name_label);
this.showULabels(show_u_labels);

Expand Down
4 changes: 4 additions & 0 deletions app/javascript/canvas/irv/view/RackObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ class RackObject extends RackSpaceObject {
this.parent = ko.observable(this.parent);
this.parent.subscribe(this.setLayers);

// Based on the assumption that items in a rack share the same role.
// This will need to change if we have more specific permissions e.g. for individual devices
this.teamRole = def.teamRole || this.parent()?.teamRole || "unknown";

this.setLayers();

this.availableSpaces = [];
Expand Down
11 changes: 4 additions & 7 deletions app/javascript/canvas/irv/view/RackSpace.js
Original file line number Diff line number Diff line change
Expand Up @@ -539,9 +539,8 @@ class RackSpace {

const show_u_labels = this.scale >= RackSpace.U_LBL_SCALE_CUTOFF;
const show_name_label = this.scale >= RackSpace.NAME_LBL_SCALE_CUTOFF;
const show_owner_label = show_name_label;
if (this.model.showingRacks()) {
for (var rack of Array.from(this.racks)) { rack.draw(show_u_labels, show_name_label, show_owner_label); }
for (var rack of Array.from(this.racks)) { rack.draw(show_u_labels, show_name_label); }
this.updateRackImage();
}
if (this.model.showHoldingArea()) {
Expand Down Expand Up @@ -1013,11 +1012,10 @@ class RackSpace {
evHalfFlipped(img_id) {
const show_u_labels = this.scale >= RackSpace.U_LBL_SCALE_CUTOFF;
const show_name_label = this.scale >= RackSpace.NAME_LBL_SCALE_CUTOFF;
const show_owner_label = show_name_label && this.model.RBAC.can_i("view", "all");

// redraw the rack in the (hidden) rack layer, since the rack image in the fx layer is a slice of the rack layer it will automatically
// reflect the changes
this.rackLookup[img_id].draw(show_u_labels, show_name_label, show_owner_label);
this.rackLookup[img_id].draw(show_u_labels, show_name_label);

const x = this.fx.getAttribute(img_id, 'x');
const width = this.fx.getAttribute(img_id, 'sliceWidth');
Expand Down Expand Up @@ -1376,14 +1374,13 @@ class RackSpace {
this.infoGfx.setScale(this.targetScale);
this.alertGfx.setScale(this.targetScale);

// decide wether to show rack labels
// decide whether to show rack labels
const show_name_label = this.targetScale >= RackSpace.NAME_LBL_SCALE_CUTOFF;
const show_u_labels = this.targetScale >= RackSpace.U_LBL_SCALE_CUTOFF;
const show_owner_label = show_name_label && this.model.RBAC.can_i("view", "all");

if (this.model.showingRacks()) {
for (var rack of Array.from(this.racks)) {
if (rack instanceof Rack) { rack.showOwnerLabel(show_owner_label); }
if (rack instanceof Rack) { rack.showOwnerLabel(show_name_label); }
rack.showNameLabel(show_name_label);
if (rack instanceof Rack) { rack.showULabels(show_u_labels, this.targetScale); }

Expand Down
3 changes: 2 additions & 1 deletion app/javascript/canvas/irv/view/RackSpaceDragHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class RackSpaceDragHandler {

if (this.draggee == null || !this.canIMoveThisItem()) {
// If not over a device or we're not permitted to move it abort.
this.draggee = null;
return;
}

Expand Down Expand Up @@ -111,7 +112,7 @@ class RackSpaceDragHandler {
}

doIHavePermissionToMoveOrDrag() {
return ((this.draggee instanceof Chassis || this.draggee instanceof Machine) && this.model.RBAC.can_i_move_devices());
return ((this.draggee instanceof Chassis || this.draggee instanceof Machine) && this.model.RBAC.can_i_move_device(this.draggee));
}

// public method, updates dragging of a draggee box or device
Expand Down
11 changes: 8 additions & 3 deletions app/jobs/broadcast_rack_change_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ def perform(rack_id, team_id, action)
else
msg = rack_content(rack_id, action)
end
user_ids = TeamRole.where(team_id: team_id).pluck(:user_id)
User.where(root: true).or(User.where(id: user_ids)).each do |user|
user_roles = TeamRole.where(team_id: team_id)
role_mapping = user_roles.pluck(:user_id, :role).to_h
User.where(root: true).or(User.where(id: role_mapping.keys)).each do |user|
unless action == "deleted"
role = user.root? ? "superAdmin" : role_mapping[user.id]
msg[:rack][:teamRole] = role
end
InteractiveRackViewChannel.broadcast_to(user, msg)
end
end

def rack_content(rack_id, action)
{ action: action, rack: Irv::HwRackServices::Show.call(rack_id) }
{ action: action, rack: Irv::HwRackServices::Show.call(rack_id) }
end
end
11 changes: 7 additions & 4 deletions app/models/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,17 @@ def non_root_abilities
can :read, InteractiveRackView

can :read, Template
can :manage, Chassis, location: {rack: {team_id: @user.team_ids }}
can :manage, Device, chassis: {location: {rack: {team_id: @user.team_ids }}}
can :manage, HwRack, team_id: @user.team_ids
can :read, Chassis, location: {rack: {team_id: @user.team_ids }}
can :read, Device, chassis: {location: {rack: {team_id: @user.team_ids }}}
can [:read, :devices], HwRack, team_id: @user.team_ids
can :manage, Chassis, location: {rack: {team_id: @user.teams_where_admin.pluck(:id) }}
can :manage, Device, chassis: {location: {rack: {team_id: @user.teams_where_admin.pluck(:id) }}}
can :manage, HwRack, team_id: @user.teams_where_admin.pluck(:id)

can :manage, RackviewPreset, user: @user

can :read, ClusterType
can :create, Cluster, team_id: @user.team_ids
can :create, Cluster, team_id: @user.teams_where_admin.pluck(:id)

can :read, KeyPair, user: @user
can :create, KeyPair, user: @user
Expand Down
13 changes: 12 additions & 1 deletion app/models/interactive_rack_view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ def rack_ids(racks, user)
end
end

def role_query(user)
return unless user

if user.root?
"( SELECT 'superAdmin' as \"teamRole\" ) as \"teamRole\","
else
"( SELECT TR.role AS \"teamRole\" FROM team_roles TR WHERE TR.team_id = R.team_id AND TR.user_id = '#{user.id.to_s}' LIMIT 1) AS \"teamRole\","
end
end

def generate_sql(racks, user)
ids = rack_ids(racks, user)
sanitized_ids = ids.map { |id| "'#{ApplicationRecord.sanitize_sql(id)}'" }.join(',')
Expand All @@ -87,6 +97,7 @@ def generate_sql(racks, user)
R.u_height AS "uHeight" ,
R.status AS "buildStatus" ,
cast(R.cost as money) AS "cost",
#{role_query(user)}
( SELECT id FROM sorted_racks OFFSET (SELECT row_num FROM (SELECT id,row_number() OVER () AS row_num FROM sorted_racks) t WHERE id=R.id) LIMIT 1) AS "nextRackId"),
( SELECT XmlElement( name "owner", XmlAttributes (O.id, O.name))
FROM teams O WHERE O.id = R.team_id LIMIT 1
Expand Down Expand Up @@ -155,7 +166,7 @@ def generate_sql(racks, user)
)
)
)
) FROM sorted_racks R
) FROM sorted_racks R
SQL

ret + condition
Expand Down
2 changes: 1 addition & 1 deletion app/services/irv/hw_rack_services/index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def call
@racks = @user.root? ? HwRack.all : @user.racks
@racks = @racks.where(id: @rack_ids) if @rack_ids&.any?
@racks = @racks.map { |rack| Api::V1::RackPresenter.new(rack) }
renderer = Rabl::Renderer.new('api/v1/irv/racks/index', @racks, view_path: 'app/views', format: 'hash')
renderer = Rabl::Renderer.new('api/v1/irv/racks/index', @racks, view_path: 'app/views', format: 'hash', locals: { user: @user} )
{Racks: {Rack: renderer.render}}
else
# The fast and awkward to understand method.
Expand Down
2 changes: 1 addition & 1 deletion app/views/api/v1/irv/racks/index.rabl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
collection @racks
node do |rack|
partial('api/v1/irv/racks/show', :object => rack)
partial('api/v1/irv/racks/show', object: rack, locals: { user: locals[:user] })
end
7 changes: 7 additions & 0 deletions app/views/api/v1/irv/racks/show.rabl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ object @rack
attributes :id, :name
attribute :currency_cost => :cost
attributes u_height: :uHeight, status: :buildStatus
node(:teamRole) do |rack|
if locals[:user].root
"superAdmin"
else
locals[:user].team_roles.where(team_id: rack.team_id).pluck(:role).first
end
end

child :team, root: 'owner' do
extends 'api/v1/teams/show'
Expand Down
2 changes: 1 addition & 1 deletion app/views/api/v1/racks/index.rabl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
object @racks
node do |rack|
partial('api/v1/racks/show', :object => rack)
partial('api/v1/racks/show', object: rack)
end
2 changes: 1 addition & 1 deletion app/views/cluster_types/_card.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<% title = if !current_user.can?(:create, Cluster)
"You do not have permission to create a cluster"
elsif !available_teams
"Unable to create a cluster - you must belong to a team with at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster"
"Unable to create a cluster - you must be admin for a team with at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster"
else
"Please select a team"
end
Expand Down
4 changes: 2 additions & 2 deletions app/views/cluster_types/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@
</select>
</div>
<div class="constraint-text">
Must have at least <%= Rails.application.config.cluster_credit_requirement %> credits
You must be a team admin and the team have at least <%= Rails.application.config.cluster_credit_requirement %> credits
</div>
</div>
<% else %>
<p>
You must belong to a team with at least <%= Rails.application.config.cluster_credit_requirement %> credits to create a cluster.
You must be an admin for a team with at least <%= Rails.application.config.cluster_credit_requirement %> credits to create a cluster.
</p>
<% end %>
</div>
Expand Down
Loading

0 comments on commit b11243c

Please sign in to comment.