Skip to content

Commit

Permalink
Merge pull request #192 from alces-flight/teams/default-team
Browse files Browse the repository at this point in the history
Create user team on sign up
  • Loading branch information
timalces authored Mar 8, 2024
2 parents b80279a + b5b8869 commit d852130
Show file tree
Hide file tree
Showing 16 changed files with 347 additions and 118 deletions.
25 changes: 25 additions & 0 deletions app/jobs/create_single_user_team_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class CreateSingleUserTeamJob < ApplicationJob
queue_as :default

def perform(user, cloud_service_config)
team = nil
team_role = nil

ActiveRecord::Base.transaction do
team = Team.new(name: "#{user.login}_team", single_user: true)
unless team.save
logger.info("Unable to create team for #{user.login} #{team.errors.details}")
raise ActiveModel::ValidationError, team
end

team_role = TeamRole.new(team: team, user: user, role: "admin")
unless team_role.save
logger.info("Unable to create team role for #{user.login} #{team_role.errors.details}")
logger.info("Rolling back creation of team #{team.name}")
raise ActiveModel::ValidationError, team_role
end
end

CreateTeamThenRoleJob.perform_later(team, team_role, cloud_service_config)
end
end
26 changes: 26 additions & 0 deletions app/jobs/create_team_then_role_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'faraday'

class CreateTeamThenRoleJob < CreateTeamJob
def perform(team, team_role, cloud_service_config, **options)
runner = Runner.new(
team: team,
team_role: team_role,
cloud_service_config: cloud_service_config,
logger: logger,
**options
)
runner.call
end

class Runner < CreateTeamJob::Runner
def initialize(team_role:, **kwargs)
@team_role = team_role
super(**kwargs)
end

def call
super
CreateTeamRoleJob.perform_later(@team_role, @cloud_service_config)
end
end
end
1 change: 1 addition & 0 deletions app/jobs/user_signup_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def call
result = Result.from(response.body)
result.validate!(:cloud)
result.sync(@user, :cloud)
CreateSingleUserTeamJob.perform_later(@user, @cloud_service_config)
rescue ::ActiveModel::ValidationError
@logger.warn("Failed to sync response to user: #{$!.message}")
raise
Expand Down
4 changes: 3 additions & 1 deletion app/models/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def root_abilities

# Don't allow any admin users to be deleted.
cannot :destroy, User, root: true

cannot :manage, TeamRole, team: Team.where(single_user: true)
end

# Abilities for non-root users.
Expand All @@ -51,7 +53,7 @@ def non_root_abilities

can [:read, :update], User, id: @user.id
can :read, Team, id: @user.team_ids
can :manage, TeamRole, team_id: @user.team_roles.where(role: "admin").pluck(:team_id)
can :manage, TeamRole, team: @user.teams_where_admin.where(single_user: false)

# Invoice is an ActiveModel::Model, but not an ActiveRecord::Base. Setting
# abilities like this might not work too well. Or perhaps its fine.
Expand Down
7 changes: 7 additions & 0 deletions app/models/team_role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def self.perform_search(term, search_scope = default_searchable_columns)
validates :user_id, uniqueness: { scope: :team_id, message: "User can only have one role per team" }

validate :user_not_root
validate :one_role_for_single_user_team

######################################
#
Expand Down Expand Up @@ -77,6 +78,12 @@ def user_not_root
self.errors.add(:user, 'must not be super admin') if user&.root?
end

def one_role_for_single_user_team
if team&.single_user && team.team_roles.where.not(id: id).exists?
self.errors.add(:team, 'is a single user team and already has an assigned user')
end
end

# What user can see in irv may have changed
def broadcast_change
BroadcastUserRacksJob.perform_now(self.user_id)
Expand Down
4 changes: 4 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,8 @@ def mark_as_pending_deletion
update(deleted_at: Time.current)
allowlisted_jwts.destroy_all
end

def teams_where_admin
@teams_where_admin ||= teams.where(team_roles: { role: 'admin' })
end
end
8 changes: 8 additions & 0 deletions app/presenters/team_presenter.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
class TeamPresenter < Presenter
include Costed

def name(user)
personal_team_for_user?(user) ? "#{o.name} (your personal team)" : o.name
end

def status
if o.deleted_at.nil?
"Active"
Expand Down Expand Up @@ -49,4 +53,8 @@ def form_hint(attribute)
I18n.t("simple_form.customisations.hints.team.edit.#{attribute}.present")
end
end

def personal_team_for_user?(user)
o.single_user && !user.root && user.teams_where_admin.where(id: o.id).exists?
end
end
7 changes: 6 additions & 1 deletion app/views/teams/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
<% end %>

<% t.attribute_column :id, sortable: true %>
<% t.attribute_column :name, sortable: true %>
<% t.custom_column "Name", sortable: true, db_column: :name do |team| %>
<% presenter_for(team).name(current_user) %>
<% end %>
<% if current_user.root? %>
<% t.attribute_column :single_user, title: "Single user team", sortable: true, tooltip: "A single user team cannot have more than one user assigned" %>
<% end %>
<% TeamRole::VALID_ROLES.each do |role| %>
<% t.custom_column role.pluralize.capitalize do |team| %>
<% presenter_for(team).team_users_list(role) %>
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20240304171607_add_single_user_to_team.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddSingleUserToTeam < ActiveRecord::Migration[7.1]
def change
add_column :teams, :single_user, :boolean, default: false, null: false
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions spec/jobs/create_single_user_team_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require 'rails_helper'

RSpec.describe CreateSingleUserTeamJob, type: :job do
include ActiveJob::TestHelper
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let!(:user) { create(:user, :with_openstack_account, login: "bilbo") }
let(:changes) { {} }
let(:cloud_service_config) { create(:cloud_service_config) }

subject(:job) {
CreateSingleUserTeamJob.perform_now(user, cloud_service_config)
}

before(:each) do
clear_enqueued_jobs
clear_performed_jobs
end

it "creates a single user team with the user's username" do
expect { subject }.to change(Team, :count).by(1)
team = Team.last
expect(team.name).to eq "bilbo_team"
expect(team.single_user).to eq true
end

it "assigns user as team admin" do
expect { subject }.to change(TeamRole, :count).by(1)
role = TeamRole.last
team = Team.last
expect(role.user).to eq user
expect(role.team).to eq team
expect(role.role).to eq 'admin'
end

it "rolls back creation of team if user assignment fails" do
user.root = true
expect { subject rescue nil }.to not_change(Team, :count).and not_change(TeamRole, :count)

expect(CreateTeamThenRoleJob).not_to have_been_enqueued
end

it "enqueues creation of a team in openstack" do
subject
expect(CreateTeamThenRoleJob).to have_been_enqueued.with(Team.last, TeamRole.last, cloud_service_config)
end

it "does not enqueue creation of team in openstack if unsuccessful" do
create(:team, name: "bilbo_team")
expect { subject rescue nil }.not_to change(Team, :count)
expect(CreateTeamThenRoleJob).not_to have_been_enqueued
end

end
107 changes: 2 additions & 105 deletions spec/jobs/create_team_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,110 +7,9 @@

subject(:job_runner) {
CreateTeamJob::Runner.new(team: team, cloud_service_config: cloud_service_config, test_stubs: stubs)
}
}

describe "url" do
let(:team_service_path) { "/create_team" }

subject { super().send(:url) }

it "uses the correct ip, port and path" do
expect(subject).to eq "#{cloud_service_config.user_handler_base_url}#{team_service_path}"
end
end

describe "body" do
subject { super().send(:body).with_indifferent_access }

it "contains the team's name" do
expect(subject["name"]).to eq team.name
end

context "when the team has a project id" do
let(:team) { create(:team, project_id: Faker::Internet.uuid) }

it "contains the team's project id" do
expect(team.project_id).not_to be_nil
expect(subject["project_id"]).to eq team.project_id
end
end

context "when the team does not have a project id" do
it "does not contain the team's project id" do
expect(team.project_id).to be_nil
expect(subject).not_to have_key "project_id"
expect(subject).not_to have_key :project_id
end
end

context "when the team has a billing account id" do
let(:team) { create(:team, billing_acct_id: Faker::Internet.uuid) }

it "contains the team's billing account id" do
expect(team.billing_acct_id).not_to be_nil
expect(subject["billing_account_id"]).to eq team.billing_acct_id
end
end

context "when the team does not have a billing account id" do
it "does not contain the team's billing account id" do
expect(team.billing_acct_id).to be_nil
expect(subject).not_to have_key "billing_account_id"
expect(subject).not_to have_key :billing_account_id
end
end

it "contains the correct cloud environment config" do
expect(subject[:cloud_env]).to eq({
"auth_url" => cloud_service_config.internal_auth_url,
"user_id" => cloud_service_config.admin_user_id,
"password" => cloud_service_config.admin_foreign_password,
"project_id" => cloud_service_config.admin_project_id
})
end
end

describe "updating the team's details from the response" do
let(:team_service_path) { "/create_team" }
context "when response does not contain expected fields" do
let(:response_body) { {} }

before(:each) do
stubs.post(team_service_path) { |env| [ 201, {}, response_body ] }
end

it "raises ActiveModel::ValidationError" do
expect { subject.call }.to raise_error ActiveModel::ValidationError
end

it "does not update the project_id" do
expect { subject.call rescue nil }.not_to change(team, :project_id).from(nil)
end

it "does not update the billing_acct_id" do
expect { subject.call rescue nil }.not_to change(team, :billing_acct_id).from(nil)
end
end

context "when response contains expected fields" do
let(:project_id) { SecureRandom.uuid }
let(:billing_acct_id) { SecureRandom.uuid }
let(:response_body) {
{project_id: project_id, billing_account_id: billing_acct_id}
.stringify_keys
}

before(:each) do
stubs.post(team_service_path) { |env| [ 201, {}, response_body ] }
end

it "updates the team's project_id and billing_acct_id" do
expect { subject.call }
.to change(team, :project_id).from(nil).to(project_id)
.and change(team, :billing_acct_id).from(nil).to(billing_acct_id)
end
end
end
include_examples 'creating team job'

describe "skipping deleted teams" do
let(:team) { create(:team, deleted_at: Time.current) }
Expand All @@ -120,6 +19,4 @@
described_class.perform_now(team, cloud_service_config, test_stubs: stubs)
end
end

include_examples 'auth token header'
end
55 changes: 55 additions & 0 deletions spec/jobs/create_team_then_role_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
require 'rails_helper'

RSpec.describe CreateTeamThenRoleJob, type: :job do
include ActiveJob::TestHelper
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let(:cloud_service_config) { create(:cloud_service_config) }
let(:team) { create(:team) }
let(:team_role) { create(:team_role, team: team) }

subject(:job_runner) {
CreateTeamThenRoleJob::Runner.new(team: team, team_role: team_role, cloud_service_config: cloud_service_config, test_stubs: stubs)
}

include_examples 'creating team job'

describe "creating role on success" do
let(:team_service_path) { "/create_team" }

before(:each) do
clear_enqueued_jobs
clear_performed_jobs
end

context "when team creation request succeeds" do
let(:project_id) { SecureRandom.uuid }
let(:billing_acct_id) { SecureRandom.uuid }
let(:response_body) {
{project_id: project_id, billing_account_id: billing_acct_id}
.stringify_keys
}

before(:each) do
stubs.post(team_service_path) { |env| [ 201, {}, response_body ] }
end

it "enqueues job to create role" do
subject.call
expect(CreateTeamRoleJob).to have_been_enqueued.with(team_role, cloud_service_config)
end
end

context "when team creation request fails" do
let(:response_body) { {} }

before(:each) do
stubs.post(team_service_path) { |env| [ 201, {}, response_body ] }
end

it "does not enqueue role creation" do
subject.call rescue nil
expect(CreateTeamRoleJob).not_to have_been_enqueued
end
end
end
end
Loading

0 comments on commit d852130

Please sign in to comment.